diff --git a/barecode/.gitignore b/barecode/.gitignore new file mode 100644 index 0000000..7ab91cf --- /dev/null +++ b/barecode/.gitignore @@ -0,0 +1,3 @@ +{src/ +build/ +BareCodeAUR diff --git a/barecode/BareCode.desktop b/barecode/BareCode.desktop new file mode 100644 index 0000000..ade1b56 --- /dev/null +++ b/barecode/BareCode.desktop @@ -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 diff --git a/barecode/BareCode.rc b/barecode/BareCode.rc new file mode 100644 index 0000000..8650ce3 --- /dev/null +++ b/barecode/BareCode.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "resources/BareCode.ico" diff --git a/barecode/CMakeLists.txt b/barecode/CMakeLists.txt new file mode 100644 index 0000000..a0ef7c2 --- /dev/null +++ b/barecode/CMakeLists.txt @@ -0,0 +1,159 @@ +cmake_minimum_required(VERSION 3.16) + +project(BareCode + VERSION 1.2.0 + DESCRIPTION "A modular code editor built with Qt6" + LANGUAGES CXX +) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# --------------------------------------------------------------------------- +# Plattform-Erkennung +# --------------------------------------------------------------------------- +if(WIN32) + set(PLATFORM_WINDOWS TRUE) + add_compile_definitions(PLATFORM_WINDOWS) + set(CMAKE_WIN32_EXECUTABLE ON) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Haiku") + set(PLATFORM_HAIKU TRUE) + add_compile_definitions(PLATFORM_HAIKU) +elseif(APPLE) + set(PLATFORM_MACOS TRUE) + add_compile_definitions(PLATFORM_MACOS) +elseif(UNIX) + set(PLATFORM_LINUX TRUE) + add_compile_definitions(PLATFORM_LINUX) +endif() + +# --------------------------------------------------------------------------- +# Qt6 +# --------------------------------------------------------------------------- +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Concurrent) +qt_standard_project_setup() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +add_subdirectory(src) + +# --------------------------------------------------------------------------- +# Übersetzungen — lrelease → .qm → ins Build-Verzeichnis +# --------------------------------------------------------------------------- +find_program(LRELEASE_EXECUTABLE + NAMES lrelease lrelease-qt6 + HINTS "${Qt6_DIR}/../../../bin" + REQUIRED +) + +set(TS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_de.ts + ${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_en.ts +) + +set(QM_FILES) +foreach(TS_FILE ${TS_FILES}) + get_filename_component(TS_NAME ${TS_FILE} NAME_WE) + set(QM_FILE "${CMAKE_CURRENT_BINARY_DIR}/translations/${TS_NAME}.qm") + add_custom_command( + OUTPUT "${QM_FILE}" + COMMAND ${CMAKE_COMMAND} -E make_directory + "${CMAKE_CURRENT_BINARY_DIR}/translations" + COMMAND ${LRELEASE_EXECUTABLE} "${TS_FILE}" -qm "${QM_FILE}" + DEPENDS "${TS_FILE}" + COMMENT "lrelease: ${TS_NAME}.qm" + VERBATIM + ) + list(APPEND QM_FILES "${QM_FILE}") +endforeach() + +add_custom_target(BareCode_translations ALL DEPENDS ${QM_FILES}) + +# --------------------------------------------------------------------------- +# Resources +# --------------------------------------------------------------------------- +qt_add_resources(BARECODE_RESOURCES resources/resources.qrc) + +# --------------------------------------------------------------------------- +# Executable +# --------------------------------------------------------------------------- +if(PLATFORM_WINDOWS) + qt_add_executable(BareCode main.cpp BareCode.rc ${BARECODE_RESOURCES}) +else() + qt_add_executable(BareCode main.cpp ${BARECODE_RESOURCES}) +endif() + +add_dependencies(BareCode BareCode_translations) + +target_link_libraries(BareCode PRIVATE + BareCode_Core + BareCode_Editor + BareCode_FileTree + BareCode_Highlighter + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +# --------------------------------------------------------------------------- +# Installation +# --------------------------------------------------------------------------- +include(GNUInstallDirs) + +install(TARGETS BareCode + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install(FILES LICENSE + DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode +) + +# .qm-Dateien installieren +install(FILES ${QM_FILES} + DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode/translations +) + +if(PLATFORM_LINUX) + foreach(SIZE 16 32 48 64 128 256 512) + install(FILES resources/icon_${SIZE}.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${SIZE}x${SIZE}/apps + RENAME barecode.png + ) + endforeach() + install(FILES BareCode.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications + ) +endif() + +if(PLATFORM_HAIKU) + install(FILES resources/icon_256.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode + RENAME BareCode.png + ) + install(CODE " + execute_process( + COMMAND mimeset -f \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/BareCode\" + RESULT_VARIABLE _mimeset_result + ) + if(NOT _mimeset_result EQUAL 0) + message(WARNING \"mimeset konnte nicht ausgeführt werden.\") + endif() + ") +endif() + +if(PLATFORM_MACOS) + set_target_properties(BareCode PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME "BareCode" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION} + ) +endif() diff --git a/barecode/LICENSE b/barecode/LICENSE new file mode 100644 index 0000000..2fbdd76 --- /dev/null +++ b/barecode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/barecode/PKGBUILD b/barecode/PKGBUILD new file mode 100644 index 0000000..bd36be7 --- /dev/null +++ b/barecode/PKGBUILD @@ -0,0 +1,36 @@ +# Maintainer: Dany Thinnes +# 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" +} diff --git a/barecode/PKGBUILD-git b/barecode/PKGBUILD-git new file mode 100644 index 0000000..566f386 --- /dev/null +++ b/barecode/PKGBUILD-git @@ -0,0 +1,43 @@ +# Maintainer: Dany Thinnes +# 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" +} diff --git a/barecode/README.md b/barecode/README.md new file mode 100644 index 0000000..94dacb8 --- /dev/null +++ b/barecode/README.md @@ -0,0 +1,200 @@ +# BareCode + +**Schlanker, modularer Code-Editor für Web-Entwickler** +Version 1.2.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de) + +Entwickelt mit **C++17**, **Qt6** und **CMake**. +Kompiliert auf **Linux**, **Windows** und **Haiku**. + +--- + +## Installation + +### Arch Linux / Manjaro (AUR) + +```bash +# Mit yay +yay -S barecode + +# Mit paru +paru -S barecode + +# Immer aktuellster Git-Stand +yay -S barecode-git +``` + +### Aus dem Quellcode bauen + +Siehe Abschnitt **Bauen** weiter unten. + +--- + +## Features + +### Editor +| Feature | Details | +|---|---| +| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare über Leerzeilen | +| Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar | +| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen | +| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben | +| Einrück-Führungslinien | Vertikale Linien wie in VS Code | +| Klammerzugehörigkeit | Zusammengehörige Klammern werden grün hervorgehoben, fehlende rot | +| Auto-Indent | Einrückungstiefe wird bei Enter übernommen | +| Tab / Shift+Tab | Ein- und Ausrücken für einzelne Zeilen und Selektionen | +| Smart Backspace | Springt zur vorherigen Tab-Stop-Position | +| Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt | +| Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog | +| Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen | +| Variablen-Popup | Vorschläge für $variablen beim Tippen (nur Anzeige, kein Autocomplete) | + +### Navigation +| Feature | Details | +|---|---| +| Projektbaum | Linkes Panel — zeigt nur das gewählte Projektverzeichnis | +| Neue Datei / Ordner | Rechtsklick im Dateibaum | +| Doppelklick-Navigation | Doppelklick auf einen Funktionsnamen springt zur Definition | +| Suchen & Ersetzen | Einzeln, Alle, In Auswahl, Regex, Live-Hervorhebung (Strg+F) | +| In Dateien suchen | Rekursive Projektsuche mit Ergebnisliste (Strg+Shift+F) | + +### Projektanalyse +| Feature | Details | +|---|---| +| Projektfunktionen | Alle definierten Funktionen als durchsuchbare Liste (Strg+Shift+P) | +| Tote Funktionen | Analyse ungenutzter Funktionen mit Exclude-Verzeichnissen (Strg+Shift+T) | +| Export | Analyseergebnisse als TXT exportieren | + +### Allgemein +| Feature | Details | +|---|---| +| Speichern | Speichern, Speichern unter, Alles speichern | +| Dark / Light Mode | Umschaltbar (Strg+Shift+D), wird gespeichert | +| Session | Geöffnete Dateien und aktiver Tab werden wiederhergestellt | + +--- + +## Tastaturkürzel + +| Aktion | Kürzel | +|---|---| +| Datei öffnen | Strg+O | +| Neue Datei | Strg+N | +| Projekt öffnen | Strg+Shift+O | +| Speichern | Strg+S | +| Speichern unter | Strg+Shift+A | +| Alles speichern | Strg+Shift+S | +| Suchen / Ersetzen | Strg+F | +| In Dateien suchen | Strg+Shift+F | +| Projektfunktionen | Strg+Shift+P | +| Tote Funktionen | Strg+Shift+T | +| Dark Mode | Strg+Shift+D | + +--- + +## Projektstruktur + +``` +BareCode/ +├── CMakeLists.txt +├── main.cpp +├── LICENSE +├── PKGBUILD # Stabiles AUR-Paket +├── PKGBUILD-git # AUR-Paket (Git-Stand) +├── BareCode.desktop # Linux-Anwendungsmenü +├── resources/ +│ ├── resources.qrc +│ ├── icon_*.png +│ └── php_functions.json +└── src/ + ├── core/ # MainWindow, ProjectManager, Settings, + │ # ThemeManager, AboutDialog, IPlugin + ├── editor/ # EditorPanel, EditorTab, CodeEditor, + │ # SearchPanel, FileSearchPanel, + │ # ColorIndicator, SignatureHelper, + │ # VariableCompleter, FunctionScanner, + │ # FunctionIndex, FunctionListPanel, + │ # FunctionListDialog, DeadCodeAnalyzer, + │ # DeadCodeDialog + ├── filetree/ # FileTreePanel + └── highlighter/ # SyntaxHighlighter, HighlighterFactory +``` + +Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek. + +--- + +## Abhängigkeiten & Bauen + +### Linux + +**Ubuntu / Debian:** +```bash +sudo apt install cmake ninja-build qt6-base-dev qt6-base-dev-tools +``` + +**Arch / Manjaro:** +```bash +sudo pacman -S cmake ninja qt6-base +``` + +**Bauen:** +```bash +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build +./build/BareCode +``` + +**Installieren:** +```bash +sudo cmake --install build +``` + +--- + +### Windows + +**Abhängigkeiten:** +- [Qt6](https://www.qt.io/download) — Komponenten: Qt6 Base, Qt6 Concurrent +- CMake ≥ 3.16 +- Visual Studio 2019 oder neuer + +```bat +cmake -B build -G "Visual Studio 17 2022" -A x64 +cmake --build build --config Release +``` + +--- + +### Haiku + +```bash +pkgman install qt6_base qt6_base_devel +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +./build/BareCode +``` + +> Getestet auf Haiku R1/beta5. + +--- + +## Neue Sprache hinzufügen + +1. Subklasse von `SyntaxHighlighter` in `src/highlighter/` erstellen +2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage) +3. Dateiendung in `HighlighterFactory.cpp` registrieren + +--- + +## Lizenz + +MIT License — Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei + +--- + +## Links + +- Website: [www.projekt-hirnfrei.de](https://www.projekt-hirnfrei.de) +- Discord: [discord.projekt-hirnfrei.de](https://discord.projekt-hirnfrei.de) +- Quellcode: [git.projekt-hirnfrei.de/diabolus/BareCode](https://git.projekt-hirnfrei.de/diabolus/BareCode) +- AUR: [aur.archlinux.org/packages/barecode](https://aur.archlinux.org/packages/barecode) diff --git a/barecode/barecode.desktop b/barecode/barecode.desktop new file mode 100644 index 0000000..e6b348c --- /dev/null +++ b/barecode/barecode.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=BareCode +GenericName=Code-Editor +Comment=Modularer Code-Editor von Projekt Hirnfrei +Exec=BareCode %F +Icon=barecode +Terminal=false +Categories=Development;TextEditor;IDE; +MimeType=text/plain;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr; +Keywords=editor;code;programmierung;entwicklung; +StartupNotify=true +StartupWMClass=BareCode diff --git a/barecode/bauen.sh b/barecode/bauen.sh new file mode 100755 index 0000000..1eec33a --- /dev/null +++ b/barecode/bauen.sh @@ -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 diff --git a/barecode/main.cpp b/barecode/main.cpp new file mode 100644 index 0000000..af1377f --- /dev/null +++ b/barecode/main.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/MainWindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + app.setApplicationName("BareCode"); + app.setApplicationVersion("1.2.0"); + app.setOrganizationName("BareCode"); + + // Icon + QIcon appIcon; + appIcon.addFile(":/icon_16.png", QSize(16, 16)); + appIcon.addFile(":/icon_32.png", QSize(32, 32)); + appIcon.addFile(":/icon_48.png", QSize(48, 48)); + appIcon.addFile(":/icon_64.png", QSize(64, 64)); + appIcon.addFile(":/icon_128.png", QSize(128, 128)); + appIcon.addFile(":/icon_256.png", QSize(256, 256)); + appIcon.addFile(":/icon_512.png", QSize(512, 512)); + app.setWindowIcon(appIcon); + + // --------------------------------------------------------------------------- + // Übersetzung laden + // .qm-Dateien liegen im Unterverzeichnis "translations" neben der Binary + // oder unter /usr/share/BareCode/translations nach Installation + // --------------------------------------------------------------------------- + QSettings settings(QSettings::IniFormat, QSettings::UserScope, + "BareCode", "BareCode"); + QString locale = settings.value("language/locale", QString()).toString(); + + if (locale.isEmpty()) + { + locale = QLocale::system().name(); // z.B. "de_DE", "en_US" + } + + const QString shortLocale = locale.left(2); // "de", "en" + + // Suchpfade für .qm-Dateien + QStringList searchPaths; + searchPaths << QCoreApplication::applicationDirPath() + "/translations" + << QDir::homePath() + "/.local/share/BareCode/translations" + << "/usr/share/BareCode/translations" + << "/usr/local/share/BareCode/translations"; + + // Qt-eigene Übersetzungen (Dialoge, Standard-Buttons) + QTranslator qtTranslator; + if (qtTranslator.load("qt_" + locale, + QLibraryInfo::path(QLibraryInfo::TranslationsPath))) + { + app.installTranslator(&qtTranslator); + } + + // BareCode-Übersetzung — in allen Suchpfaden versuchen + QTranslator appTranslator; + bool loaded = false; + + for (const QString &path : searchPaths) + { + if (appTranslator.load(QString("barecode_%1").arg(locale), path) || + appTranslator.load(QString("barecode_%1").arg(shortLocale), path)) + { + app.installTranslator(&appTranslator); + loaded = true; + break; + } + } + + // Fallback: Englisch + if (!loaded && shortLocale != "de") + { + for (const QString &path : searchPaths) + { + if (appTranslator.load("barecode_en", path)) + { + app.installTranslator(&appTranslator); + break; + } + } + } + + MainWindow window; + window.show(); + + // Datei(en) öffnen, die beim Start per Kommandozeile übergeben wurden + // (z. B. Doppelklick auf eine Datei im Dateibrowser → "Öffnen mit BareCode"). + // Das erste Argument ist der Programmname selbst und wird übersprungen. + QStringList cliFiles = app.arguments(); + if (!cliFiles.isEmpty()) + { + cliFiles.removeFirst(); + } + if (!cliFiles.isEmpty()) + { + window.openFilesFromArguments(cliFiles); + } + + return app.exec(); +} diff --git a/barecode/resources/BareCode.ico b/barecode/resources/BareCode.ico new file mode 100644 index 0000000..10ecf22 Binary files /dev/null and b/barecode/resources/BareCode.ico differ diff --git a/barecode/resources/icon_128.png b/barecode/resources/icon_128.png new file mode 100644 index 0000000..bb49b56 Binary files /dev/null and b/barecode/resources/icon_128.png differ diff --git a/barecode/resources/icon_16.png b/barecode/resources/icon_16.png new file mode 100644 index 0000000..7cba1e4 Binary files /dev/null and b/barecode/resources/icon_16.png differ diff --git a/barecode/resources/icon_24.png b/barecode/resources/icon_24.png new file mode 100644 index 0000000..67356ff Binary files /dev/null and b/barecode/resources/icon_24.png differ diff --git a/barecode/resources/icon_256.png b/barecode/resources/icon_256.png new file mode 100644 index 0000000..f18f69f Binary files /dev/null and b/barecode/resources/icon_256.png differ diff --git a/barecode/resources/icon_32.png b/barecode/resources/icon_32.png new file mode 100644 index 0000000..5982252 Binary files /dev/null and b/barecode/resources/icon_32.png differ diff --git a/barecode/resources/icon_48.png b/barecode/resources/icon_48.png new file mode 100644 index 0000000..b792afe Binary files /dev/null and b/barecode/resources/icon_48.png differ diff --git a/barecode/resources/icon_512.png b/barecode/resources/icon_512.png new file mode 100644 index 0000000..5b843e5 Binary files /dev/null and b/barecode/resources/icon_512.png differ diff --git a/barecode/resources/icon_64.png b/barecode/resources/icon_64.png new file mode 100644 index 0000000..1025e84 Binary files /dev/null and b/barecode/resources/icon_64.png differ diff --git a/barecode/resources/php_functions.json b/barecode/resources/php_functions.json new file mode 100644 index 0000000..1e81c97 --- /dev/null +++ b/barecode/resources/php_functions.json @@ -0,0 +1,1197 @@ +[ + { + "name": "abs", + "signature": "abs(int|float $num): int|float", + "desc": "Absolutwert" + }, + { + "name": "addslashes", + "signature": "addslashes(string $string): string", + "desc": "Sonderzeichen mit Backslash escapen" + }, + { + "name": "array_chunk", + "signature": "array_chunk(array $array, int $length, bool $preserve_keys = false): array", + "desc": "Array in Stücke aufteilen" + }, + { + "name": "array_column", + "signature": "array_column(array $array, int|string|null $column_key, int|string|null $index_key = null): array", + "desc": "Spalte aus mehrdimensionalem Array" + }, + { + "name": "array_combine", + "signature": "array_combine(array $keys, array $values): array", + "desc": "Array aus Schlüsseln und Werten erstellen" + }, + { + "name": "array_diff", + "signature": "array_diff(array $array, array ...$arrays): array", + "desc": "Differenz zweier Arrays" + }, + { + "name": "array_fill", + "signature": "array_fill(int $start_index, int $count, mixed $value): array", + "desc": "Array mit Werten füllen" + }, + { + "name": "array_fill_keys", + "signature": "array_fill_keys(array $keys, mixed $value): array", + "desc": "Array mit Schlüsseln und Wert füllen" + }, + { + "name": "array_filter", + "signature": "array_filter(array $array, callable $callback = null, int $mode = 0): array", + "desc": "Array filtern" + }, + { + "name": "array_flip", + "signature": "array_flip(array $array): array", + "desc": "Schlüssel und Werte tauschen" + }, + { + "name": "array_intersect", + "signature": "array_intersect(array $array, array ...$arrays): array", + "desc": "Schnittmenge zweier Arrays" + }, + { + "name": "array_key_exists", + "signature": "array_key_exists(string|int $key, array $array): bool", + "desc": "Prüft ob Schlüssel existiert" + }, + { + "name": "array_key_first", + "signature": "array_key_first(array $array): int|string|null", + "desc": "Ersten Schlüssel eines Arrays" + }, + { + "name": "array_key_last", + "signature": "array_key_last(array $array): int|string|null", + "desc": "Letzten Schlüssel eines Arrays" + }, + { + "name": "array_keys", + "signature": "array_keys(array $array, mixed $filter_value = null, bool $strict = false): array", + "desc": "Alle Schlüssel zurückgeben" + }, + { + "name": "array_map", + "signature": "array_map(callable|null $callback, array $array, array ...$arrays): array", + "desc": "Funktion auf alle Elemente anwenden" + }, + { + "name": "array_merge", + "signature": "array_merge(array ...$arrays): array", + "desc": "Arrays zusammenführen" + }, + { + "name": "array_merge_recursive", + "signature": "array_merge_recursive(array ...$arrays): array", + "desc": "Arrays rekursiv zusammenführen" + }, + { + "name": "array_pop", + "signature": "array_pop(array &$array): mixed", + "desc": "Letztes Element entfernen und zurückgeben" + }, + { + "name": "array_push", + "signature": "array_push(array &$array, mixed ...$values): int", + "desc": "Elemente ans Ende anhängen" + }, + { + "name": "array_reduce", + "signature": "array_reduce(array $array, callable $callback, mixed $initial = null): mixed", + "desc": "Array auf einen Wert reduzieren" + }, + { + "name": "array_reverse", + "signature": "array_reverse(array $array, bool $preserve_keys = false): array", + "desc": "Array umkehren" + }, + { + "name": "array_search", + "signature": "array_search(mixed $needle, array $haystack, bool $strict = false): int|string|false", + "desc": "Wert im Array suchen" + }, + { + "name": "array_shift", + "signature": "array_shift(array &$array): mixed", + "desc": "Erstes Element entfernen und zurückgeben" + }, + { + "name": "array_slice", + "signature": "array_slice(array $array, int $offset, int $length = null, bool $preserve_keys = false): array", + "desc": "Teilarray zurückgeben" + }, + { + "name": "array_splice", + "signature": "array_splice(array &$array, int $offset, int $length = null, mixed $replacement = []): array", + "desc": "Array-Elemente entfernen und ersetzen" + }, + { + "name": "array_unique", + "signature": "array_unique(array $array, int $flags = SORT_STRING): array", + "desc": "Doppelte Werte entfernen" + }, + { + "name": "array_unshift", + "signature": "array_unshift(array &$array, mixed ...$values): int", + "desc": "Elemente am Anfang einfügen" + }, + { + "name": "array_values", + "signature": "array_values(array $array): array", + "desc": "Alle Werte neu indiziert zurückgeben" + }, + { + "name": "array_walk", + "signature": "array_walk(array|object &$array, callable $callback, mixed $arg = null): bool", + "desc": "Funktion auf jedes Element anwenden" + }, + { + "name": "arsort", + "signature": "arsort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array absteigend sortieren, Schlüssel beibehalten" + }, + { + "name": "asort", + "signature": "asort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array aufsteigend sortieren, Schlüssel beibehalten" + }, + { + "name": "base64_decode", + "signature": "base64_decode(string $string, bool $strict = false): string|false", + "desc": "Base64 dekodieren" + }, + { + "name": "base64_encode", + "signature": "base64_encode(string $string): string", + "desc": "Base64 kodieren" + }, + { + "name": "basename", + "signature": "basename(string $path, string $suffix = ''): string", + "desc": "Dateinamen aus Pfad" + }, + { + "name": "boolval", + "signature": "boolval(mixed $value): bool", + "desc": "In Boolean umwandeln" + }, + { + "name": "call_user_func", + "signature": "call_user_func(callable $callback, mixed ...$args): mixed", + "desc": "Funktion aufrufen" + }, + { + "name": "call_user_func_array", + "signature": "call_user_func_array(callable $callback, array $args): mixed", + "desc": "Funktion mit Array-Parametern aufrufen" + }, + { + "name": "ceil", + "signature": "ceil(int|float $num): float", + "desc": "Aufrunden" + }, + { + "name": "checkdate", + "signature": "checkdate(int $month, int $day, int $year): bool", + "desc": "Datum auf Gültigkeit prüfen" + }, + { + "name": "chunk_split", + "signature": "chunk_split(string $string, int $length = 76, string $separator = \"\\r\\n\"): string", + "desc": "String in Stücke aufteilen" + }, + { + "name": "class_exists", + "signature": "class_exists(string $class, bool $autoload = true): bool", + "desc": "Prüft ob Klasse existiert" + }, + { + "name": "compact", + "signature": "compact(array|string $var_names, mixed ...$vars): array", + "desc": "Array aus Variablen erstellen" + }, + { + "name": "constant", + "signature": "constant(string $name): mixed", + "desc": "Wert einer Konstante" + }, + { + "name": "copy", + "signature": "copy(string $from, string $to, resource $context = null): bool", + "desc": "Datei kopieren" + }, + { + "name": "count", + "signature": "count(Countable|array $array, int $mode = COUNT_NORMAL): int", + "desc": "Anzahl der Elemente" + }, + { + "name": "date", + "signature": "date(string $format, int $timestamp = time()): string", + "desc": "Datum formatieren" + }, + { + "name": "date_add", + "signature": "date_add(DateTime $object, DateInterval $interval): DateTime|false", + "desc": "Interval zu Datum addieren" + }, + { + "name": "date_create", + "signature": "date_create(string $datetime = 'now', DateTimeZone $timezone = null): DateTime|false", + "desc": "DateTime-Objekt erstellen" + }, + { + "name": "date_diff", + "signature": "date_diff(DateTimeInterface $baseObject, DateTimeInterface $targetObject, bool $absolute = false): DateInterval", + "desc": "Differenz zweier Datumswerte" + }, + { + "name": "date_format", + "signature": "date_format(DateTimeInterface $object, string $format): string", + "desc": "DateTime formatieren" + }, + { + "name": "date_sub", + "signature": "date_sub(DateTime $object, DateInterval $interval): DateTime|false", + "desc": "Interval von Datum subtrahieren" + }, + { + "name": "define", + "signature": "define(string $constant_name, mixed $value, bool $case_insensitive = false): bool", + "desc": "Konstante definieren" + }, + { + "name": "defined", + "signature": "defined(string $constant_name): bool", + "desc": "Prüft ob Konstante definiert ist" + }, + { + "name": "die", + "signature": "die(int|string $status = 0): never", + "desc": "Skript beenden (Alias exit)" + }, + { + "name": "dirname", + "signature": "dirname(string $path, int $levels = 1): string", + "desc": "Verzeichnisteil eines Pfades" + }, + { + "name": "echo", + "signature": "echo(string ...$expressions): void", + "desc": "Strings ausgeben" + }, + { + "name": "empty", + "signature": "empty(mixed $var): bool", + "desc": "Prüft ob Variable leer ist" + }, + { + "name": "error_reporting", + "signature": "error_reporting(int $error_level = null): int", + "desc": "Fehler-Reporting-Level setzen" + }, + { + "name": "exit", + "signature": "exit(int|string $status = 0): never", + "desc": "Skript beenden" + }, + { + "name": "exp", + "signature": "exp(float $num): float", + "desc": "e hoch x" + }, + { + "name": "explode", + "signature": "explode(string $separator, string $string, int $limit = PHP_INT_MAX): array", + "desc": "String aufteilen" + }, + { + "name": "extract", + "signature": "extract(array &$array, int $flags = EXTR_OVERWRITE, string $prefix = ''): int", + "desc": "Variablen aus Array importieren" + }, + { + "name": "fclose", + "signature": "fclose(resource $handle): bool", + "desc": "Datei schließen" + }, + { + "name": "feof", + "signature": "feof(resource $handle): bool", + "desc": "Prüft ob Dateiende erreicht" + }, + { + "name": "fgets", + "signature": "fgets(resource $handle, int $length = null): string|false", + "desc": "Zeile aus Datei lesen" + }, + { + "name": "file", + "signature": "file(string $filename, int $flags = 0, resource $context = null): array|false", + "desc": "Datei als Array von Zeilen" + }, + { + "name": "file_exists", + "signature": "file_exists(string $filename): bool", + "desc": "Prüft ob Datei existiert" + }, + { + "name": "file_get_contents", + "signature": "file_get_contents(string $filename, bool $use_include_path = false, resource $context = null, int $offset = 0, int $length = null): string|false", + "desc": "Dateiinhalt als String" + }, + { + "name": "file_put_contents", + "signature": "file_put_contents(string $filename, mixed $data, int $flags = 0, resource $context = null): int|false", + "desc": "String in Datei schreiben" + }, + { + "name": "filectime", + "signature": "filectime(string $filename): int|false", + "desc": "Zeitpunkt der letzten Statusänderung" + }, + { + "name": "filemtime", + "signature": "filemtime(string $filename): int|false", + "desc": "Zeitpunkt der letzten Änderung" + }, + { + "name": "filesize", + "signature": "filesize(string $filename): int|false", + "desc": "Dateigröße in Bytes" + }, + { + "name": "floatval", + "signature": "floatval(mixed $value): float", + "desc": "In Float umwandeln" + }, + { + "name": "floor", + "signature": "floor(int|float $num): float", + "desc": "Abrunden" + }, + { + "name": "fmod", + "signature": "fmod(float $num1, float $num2): float", + "desc": "Modulo für Floats" + }, + { + "name": "fopen", + "signature": "fopen(string $filename, string $mode, bool $use_include_path = false, resource $context = null): resource|false", + "desc": "Datei öffnen" + }, + { + "name": "fread", + "signature": "fread(resource $handle, int $length): string|false", + "desc": "Aus Datei lesen" + }, + { + "name": "fseek", + "signature": "fseek(resource $handle, int $offset, int $whence = SEEK_SET): int", + "desc": "Dateizeiger setzen" + }, + { + "name": "ftell", + "signature": "ftell(resource $handle): int|false", + "desc": "Aktuelle Position des Dateizeigers" + }, + { + "name": "function_exists", + "signature": "function_exists(string $function): bool", + "desc": "Prüft ob Funktion existiert" + }, + { + "name": "fwrite", + "signature": "fwrite(resource $handle, string $string, int $length = null): int|false", + "desc": "In Datei schreiben" + }, + { + "name": "get_class", + "signature": "get_class(object $object = null): string|false", + "desc": "Klassenname eines Objekts" + }, + { + "name": "get_parent_class", + "signature": "get_parent_class(object|string $object_or_class): string|false", + "desc": "Elternklasse ermitteln" + }, + { + "name": "gettype", + "signature": "gettype(mixed $value): string", + "desc": "Typ einer Variable" + }, + { + "name": "glob", + "signature": "glob(string $pattern, int $flags = 0): array|false", + "desc": "Dateien per Muster suchen" + }, + { + "name": "hash", + "signature": "hash(string $algo, string $data, bool $binary = false, array $options = []): string", + "desc": "Hash mit beliebigem Algorithmus" + }, + { + "name": "header", + "signature": "header(string $header, bool $replace = true, int $response_code = 0): void", + "desc": "HTTP-Header senden" + }, + { + "name": "headers_sent", + "signature": "headers_sent(string &$filename = null, int &$line = null): bool", + "desc": "Prüft ob Header bereits gesendet" + }, + { + "name": "htmlentities", + "signature": "htmlentities(string $string, int $flags = ENT_QUOTES|ENT_SUBSTITUTE, string $encoding = 'UTF-8', bool $double_encode = true): string", + "desc": "Alle Sonderzeichen in HTML-Entities" + }, + { + "name": "htmlspecialchars", + "signature": "htmlspecialchars(string $string, int $flags = ENT_QUOTES|ENT_SUBSTITUTE, string $encoding = 'UTF-8', bool $double_encode = true): string", + "desc": "Sonderzeichen in HTML-Entities umwandeln" + }, + { + "name": "htmlspecialchars_decode", + "signature": "htmlspecialchars_decode(string $string, int $flags = ENT_QUOTES|ENT_SUBSTITUTE): string", + "desc": "HTML-Entities zurückumwandeln" + }, + { + "name": "http_build_query", + "signature": "http_build_query(array|object $data, string $numeric_prefix = '', string $arg_separator = null, int $encoding_type = PHP_QUERY_RFC1738): string", + "desc": "Query-String aufbauen" + }, + { + "name": "implode", + "signature": "implode(string $separator, array $array): string", + "desc": "Array zu String verbinden" + }, + { + "name": "in_array", + "signature": "in_array(mixed $needle, array $haystack, bool $strict = false): bool", + "desc": "Prüft ob Wert im Array vorhanden" + }, + { + "name": "include", + "signature": "include(string $filename): mixed", + "desc": "Datei einbinden" + }, + { + "name": "include_once", + "signature": "include_once(string $filename): mixed", + "desc": "Datei einmalig einbinden" + }, + { + "name": "ini_get", + "signature": "ini_get(string $option): string|false", + "desc": "PHP-Konfigurationswert holen" + }, + { + "name": "ini_set", + "signature": "ini_set(string $option, string $value): string|false", + "desc": "PHP-Konfigurationswert setzen" + }, + { + "name": "instanceof", + "signature": "instanceof", + "desc": "Prüft ob Objekt eine Instanz ist" + }, + { + "name": "intdiv", + "signature": "intdiv(int $num1, int $num2): int", + "desc": "Ganzzahlige Division" + }, + { + "name": "intval", + "signature": "intval(mixed $value, int $base = 10): int", + "desc": "In Integer umwandeln" + }, + { + "name": "is_array", + "signature": "is_array(mixed $value): bool", + "desc": "Prüft ob Array" + }, + { + "name": "is_bool", + "signature": "is_bool(mixed $value): bool", + "desc": "Prüft ob Boolean" + }, + { + "name": "is_callable", + "signature": "is_callable(mixed $value, bool $syntax_only = false, string &$callable_name = null): bool", + "desc": "Prüft ob aufrufbar" + }, + { + "name": "is_dir", + "signature": "is_dir(string $filename): bool", + "desc": "Prüft ob Pfad ein Verzeichnis ist" + }, + { + "name": "is_file", + "signature": "is_file(string $filename): bool", + "desc": "Prüft ob Pfad eine Datei ist" + }, + { + "name": "is_float", + "signature": "is_float(mixed $value): bool", + "desc": "Prüft ob Float" + }, + { + "name": "is_int", + "signature": "is_int(mixed $value): bool", + "desc": "Prüft ob Integer" + }, + { + "name": "is_null", + "signature": "is_null(mixed $value): bool", + "desc": "Prüft ob Wert null ist" + }, + { + "name": "is_numeric", + "signature": "is_numeric(mixed $value): bool", + "desc": "Prüft ob numerisch" + }, + { + "name": "is_object", + "signature": "is_object(mixed $value): bool", + "desc": "Prüft ob Objekt" + }, + { + "name": "is_readable", + "signature": "is_readable(string $filename): bool", + "desc": "Prüft ob Datei lesbar ist" + }, + { + "name": "is_string", + "signature": "is_string(mixed $value): bool", + "desc": "Prüft ob String" + }, + { + "name": "is_writable", + "signature": "is_writable(string $filename): bool", + "desc": "Prüft ob Datei schreibbar ist" + }, + { + "name": "isset", + "signature": "isset(mixed $var, mixed ...$vars): bool", + "desc": "Prüft ob Variable gesetzt und nicht null" + }, + { + "name": "join", + "signature": "join(string $separator, array $array): string", + "desc": "Array zu String verbinden (Alias implode)" + }, + { + "name": "json_decode", + "signature": "json_decode(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed", + "desc": "JSON parsen" + }, + { + "name": "json_encode", + "signature": "json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false", + "desc": "In JSON umwandeln" + }, + { + "name": "json_last_error", + "signature": "json_last_error(): int", + "desc": "Letzten JSON-Fehler abrufen" + }, + { + "name": "json_last_error_msg", + "signature": "json_last_error_msg(): string", + "desc": "Letzten JSON-Fehler als Text" + }, + { + "name": "krsort", + "signature": "krsort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array nach Schlüsseln absteigend sortieren" + }, + { + "name": "ksort", + "signature": "ksort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array nach Schlüsseln sortieren" + }, + { + "name": "lcfirst", + "signature": "lcfirst(string $string): string", + "desc": "Ersten Buchstaben klein" + }, + { + "name": "list", + "signature": "list(mixed ...$vars): array", + "desc": "Variablen wie ein Array zuweisen" + }, + { + "name": "log", + "signature": "log(float $num, float $base = M_E): float", + "desc": "Logarithmus" + }, + { + "name": "ltrim", + "signature": "ltrim(string $string, string $characters = \" \\n\\r\\t\\v\\0\"): string", + "desc": "Leerzeichen links entfernen" + }, + { + "name": "max", + "signature": "max(mixed $value, mixed ...$values): mixed", + "desc": "Größten Wert ermitteln" + }, + { + "name": "md5", + "signature": "md5(string $string, bool $binary = false): string", + "desc": "MD5-Hash berechnen" + }, + { + "name": "method_exists", + "signature": "method_exists(object|string $object_or_class, string $method): bool", + "desc": "Prüft ob Methode existiert" + }, + { + "name": "microtime", + "signature": "microtime(bool $as_float = false): string|float", + "desc": "Aktuellen Timestamp mit Mikrosekunden" + }, + { + "name": "min", + "signature": "min(mixed $value, mixed ...$values): mixed", + "desc": "Kleinsten Wert ermitteln" + }, + { + "name": "mkdir", + "signature": "mkdir(string $directory, int $permissions = 0777, bool $recursive = false, resource $context = null): bool", + "desc": "Verzeichnis erstellen" + }, + { + "name": "mktime", + "signature": "mktime(int $hour, int $minute = null, int $second = null, int $month = null, int $day = null, int $year = null): int|false", + "desc": "Unix-Timestamp erstellen" + }, + { + "name": "mt_rand", + "signature": "mt_rand(int $min = 0, int $max = MT_RAND_MAX): int", + "desc": "Bessere Zufallszahl" + }, + { + "name": "nl2br", + "signature": "nl2br(string $string, bool $use_xhtml = true): string", + "desc": "Zeilenumbrüche in
umwandeln" + }, + { + "name": "number_format", + "signature": "number_format(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string", + "desc": "Zahl formatieren" + }, + { + "name": "ob_end_clean", + "signature": "ob_end_clean(): bool", + "desc": "Buffer leeren und beenden" + }, + { + "name": "ob_get_clean", + "signature": "ob_get_clean(): string|false", + "desc": "Buffer-Inhalt holen und beenden" + }, + { + "name": "ob_get_contents", + "signature": "ob_get_contents(): string|false", + "desc": "Buffer-Inhalt holen" + }, + { + "name": "ob_start", + "signature": "ob_start(callable $callback = null, int $chunk_size = 0, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS): bool", + "desc": "Output-Buffering starten" + }, + { + "name": "parse_str", + "signature": "parse_str(string $string, array &$result): void", + "desc": "Query-String parsen" + }, + { + "name": "pathinfo", + "signature": "pathinfo(string $path, int $options = PATHINFO_ALL): array|string", + "desc": "Informationen über einen Pfad" + }, + { + "name": "PDO::__construct", + "signature": "PDO::__construct(string $dsn, string $username = null, string $password = null, array $options = null)", + "desc": "PDO-Verbindung herstellen" + }, + { + "name": "PDO::beginTransaction", + "signature": "PDO::beginTransaction(): bool", + "desc": "Transaktion starten" + }, + { + "name": "PDO::commit", + "signature": "PDO::commit(): bool", + "desc": "Transaktion bestätigen" + }, + { + "name": "PDO::exec", + "signature": "PDO::exec(string $statement): int|false", + "desc": "SQL ausführen, Anzahl betroffener Zeilen" + }, + { + "name": "PDO::lastInsertId", + "signature": "PDO::lastInsertId(string $name = null): string|false", + "desc": "Letzte eingefügte ID" + }, + { + "name": "PDO::prepare", + "signature": "PDO::prepare(string $query, array $options = []): PDOStatement|false", + "desc": "SQL-Statement vorbereiten" + }, + { + "name": "PDO::query", + "signature": "PDO::query(string $query, int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false", + "desc": "SQL direkt ausführen" + }, + { + "name": "PDO::rollBack", + "signature": "PDO::rollBack(): bool", + "desc": "Transaktion zurückrollen" + }, + { + "name": "PDOStatement::bindParam", + "signature": "PDOStatement::bindParam(string|int $param, mixed &$var, int $type = PDO::PARAM_STR, int $maxLength = 0, mixed $driverOptions = null): bool", + "desc": "Parameter binden" + }, + { + "name": "PDOStatement::bindValue", + "signature": "PDOStatement::bindValue(string|int $param, mixed $value, int $type = PDO::PARAM_STR): bool", + "desc": "Wert binden" + }, + { + "name": "PDOStatement::execute", + "signature": "PDOStatement::execute(array $params = null): bool", + "desc": "Vorbereitetes Statement ausführen" + }, + { + "name": "PDOStatement::fetch", + "signature": "PDOStatement::fetch(int $mode = PDO::FETCH_DEFAULT, int $cursorOrientation = PDO::FETCH_ORI_NEXT, int $cursorOffset = 0): mixed", + "desc": "Nächste Zeile holen" + }, + { + "name": "PDOStatement::fetchAll", + "signature": "PDOStatement::fetchAll(int $mode = PDO::FETCH_DEFAULT, mixed ...$args): array", + "desc": "Alle Zeilen holen" + }, + { + "name": "PDOStatement::fetchColumn", + "signature": "PDOStatement::fetchColumn(int $column = 0): mixed", + "desc": "Einzelne Spalte holen" + }, + { + "name": "PDOStatement::rowCount", + "signature": "PDOStatement::rowCount(): int", + "desc": "Anzahl betroffener Zeilen" + }, + { + "name": "php_uname", + "signature": "php_uname(string $mode = 'a'): string", + "desc": "Systeminformationen" + }, + { + "name": "phpinfo", + "signature": "phpinfo(int $flags = INFO_ALL): bool", + "desc": "PHP-Konfiguration ausgeben" + }, + { + "name": "phpversion", + "signature": "phpversion(string $extension = null): string|false", + "desc": "PHP-Version" + }, + { + "name": "pi", + "signature": "pi(): float", + "desc": "Wert von Pi" + }, + { + "name": "pow", + "signature": "pow(mixed $base, mixed $exp): int|float", + "desc": "Potenz berechnen" + }, + { + "name": "preg_match", + "signature": "preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int|false", + "desc": "Regulären Ausdruck prüfen" + }, + { + "name": "preg_match_all", + "signature": "preg_match_all(string $pattern, string $subject, array &$matches = null, int $flags = PREG_PATTERN_ORDER, int $offset = 0): int|false", + "desc": "Alle Treffer eines Regex finden" + }, + { + "name": "preg_quote", + "signature": "preg_quote(string $string, string $delimiter = null): string", + "desc": "Regex-Sonderzeichen escapen" + }, + { + "name": "preg_replace", + "signature": "preg_replace(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, int &$count = null): string|array|null", + "desc": "Regex-Suchen und -Ersetzen" + }, + { + "name": "preg_split", + "signature": "preg_split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array|false", + "desc": "String per Regex aufteilen" + }, + { + "name": "print", + "signature": "print(string $expression): int", + "desc": "String ausgeben" + }, + { + "name": "print_r", + "signature": "print_r(mixed $value, bool $return = false): string|bool", + "desc": "Variable lesbar ausgeben" + }, + { + "name": "printf", + "signature": "printf(string $format, mixed ...$values): int", + "desc": "Formatierten String ausgeben" + }, + { + "name": "property_exists", + "signature": "property_exists(object|string $object_or_class, string $property): bool", + "desc": "Prüft ob Eigenschaft existiert" + }, + { + "name": "rand", + "signature": "rand(int $min = 0, int $max = getrandmax()): int", + "desc": "Zufallszahl" + }, + { + "name": "random_int", + "signature": "random_int(int $min, int $max): int", + "desc": "Kryptografisch sichere Zufallszahl" + }, + { + "name": "range", + "signature": "range(string|int|float $start, string|int|float $end, int|float $step = 1): array", + "desc": "Array mit Wertebereich erstellen" + }, + { + "name": "rawurldecode", + "signature": "rawurldecode(string $string): string", + "desc": "URL-dekodieren nach RFC 3986" + }, + { + "name": "rawurlencode", + "signature": "rawurlencode(string $string): string", + "desc": "URL-kodieren nach RFC 3986" + }, + { + "name": "realpath", + "signature": "realpath(string $path): string|false", + "desc": "Absoluten Pfad auflösen" + }, + { + "name": "rename", + "signature": "rename(string $from, string $to, resource $context = null): bool", + "desc": "Datei umbenennen oder verschieben" + }, + { + "name": "require", + "signature": "require(string $filename): mixed", + "desc": "Datei einbinden (Fehler bei Misserfolg)" + }, + { + "name": "require_once", + "signature": "require_once(string $filename): mixed", + "desc": "Datei einmalig einbinden (Fehler bei Misserfolg)" + }, + { + "name": "restore_error_handler", + "signature": "restore_error_handler(): bool", + "desc": "Fehler-Handler zurücksetzen" + }, + { + "name": "rewind", + "signature": "rewind(resource $handle): bool", + "desc": "Dateizeiger zurücksetzen" + }, + { + "name": "rmdir", + "signature": "rmdir(string $directory, resource $context = null): bool", + "desc": "Verzeichnis löschen" + }, + { + "name": "round", + "signature": "round(int|float $num, int $precision = 0, int $mode = PHP_ROUND_HALF_UP): float", + "desc": "Runden" + }, + { + "name": "rsort", + "signature": "rsort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array absteigend sortieren" + }, + { + "name": "rtrim", + "signature": "rtrim(string $string, string $characters = \" \\n\\r\\t\\v\\0\"): string", + "desc": "Leerzeichen rechts entfernen" + }, + { + "name": "scandir", + "signature": "scandir(string $directory, int $sorting_order = SCANDIR_SORT_ASCENDING, resource $context = null): array|false", + "desc": "Verzeichnisinhalt auflisten" + }, + { + "name": "session_destroy", + "signature": "session_destroy(): bool", + "desc": "Session beenden" + }, + { + "name": "session_id", + "signature": "session_id(string $id = null): string|false", + "desc": "Session-ID holen oder setzen" + }, + { + "name": "session_regenerate_id", + "signature": "session_regenerate_id(bool $delete_old_session = false): bool", + "desc": "Session-ID erneuern" + }, + { + "name": "session_start", + "signature": "session_start(array $options = []): bool", + "desc": "Session starten" + }, + { + "name": "set_error_handler", + "signature": "set_error_handler(callable|null $callback, int $error_levels = E_ALL): callable|null", + "desc": "Eigenen Fehler-Handler setzen" + }, + { + "name": "set_exception_handler", + "signature": "set_exception_handler(callable|null $callback): callable|null", + "desc": "Eigenen Exception-Handler setzen" + }, + { + "name": "setcookie", + "signature": "setcookie(string $name, string $value = '', int $expires_or_options = 0, string $path = '', string $domain = '', bool $secure = false, bool $httponly = false): bool", + "desc": "Cookie setzen" + }, + { + "name": "settype", + "signature": "settype(mixed &$var, string $type): bool", + "desc": "Typ einer Variable setzen" + }, + { + "name": "sha1", + "signature": "sha1(string $string, bool $binary = false): string", + "desc": "SHA1-Hash berechnen" + }, + { + "name": "shuffle", + "signature": "shuffle(array &$array): bool", + "desc": "Array zufällig mischen" + }, + { + "name": "sleep", + "signature": "sleep(int $seconds): int|false", + "desc": "Ausführung anhalten" + }, + { + "name": "sort", + "signature": "sort(array &$array, int $flags = SORT_REGULAR): bool", + "desc": "Array aufsteigend sortieren" + }, + { + "name": "sprintf", + "signature": "sprintf(string $format, mixed ...$values): string", + "desc": "Formatierten String zurückgeben" + }, + { + "name": "sqrt", + "signature": "sqrt(float $num): float", + "desc": "Quadratwurzel" + }, + { + "name": "str_contains", + "signature": "str_contains(string $haystack, string $needle): bool", + "desc": "Prüft ob String enthalten ist" + }, + { + "name": "str_ends_with", + "signature": "str_ends_with(string $haystack, string $needle): bool", + "desc": "Prüft ob String mit needle endet" + }, + { + "name": "str_pad", + "signature": "str_pad(string $input, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT): string", + "desc": "String auf Länge auffüllen" + }, + { + "name": "str_repeat", + "signature": "str_repeat(string $string, int $times): string", + "desc": "String wiederholen" + }, + { + "name": "str_replace", + "signature": "str_replace(array|string $search, array|string $replace, string|array $subject, int &$count = null): string|array", + "desc": "Suchen und Ersetzen in einem String" + }, + { + "name": "str_split", + "signature": "str_split(string $string, int $length = 1): array", + "desc": "String in Array aufteilen" + }, + { + "name": "str_starts_with", + "signature": "str_starts_with(string $haystack, string $needle): bool", + "desc": "Prüft ob String mit needle beginnt" + }, + { + "name": "str_word_count", + "signature": "str_word_count(string $string, int $format = 0, string $characters = null): array|int", + "desc": "Wörter zählen" + }, + { + "name": "strcasecmp", + "signature": "strcasecmp(string $string1, string $string2): int", + "desc": "Strings ohne Groß-/Kleinschreibung vergleichen" + }, + { + "name": "strcmp", + "signature": "strcmp(string $string1, string $string2): int", + "desc": "Strings vergleichen" + }, + { + "name": "strip_tags", + "signature": "strip_tags(string $string, array|string $allowed_tags = null): string", + "desc": "HTML-Tags entfernen" + }, + { + "name": "stripslashes", + "signature": "stripslashes(string $string): string", + "desc": "Backslashes entfernen" + }, + { + "name": "strlen", + "signature": "strlen(string $string): int", + "desc": "Länge eines Strings" + }, + { + "name": "strpos", + "signature": "strpos(string $haystack, string $needle, int $offset = 0): int|false", + "desc": "Position des ersten Vorkommens" + }, + { + "name": "strrpos", + "signature": "strrpos(string $haystack, string $needle, int $offset = 0): int|false", + "desc": "Position des letzten Vorkommens" + }, + { + "name": "strtolower", + "signature": "strtolower(string $string): string", + "desc": "In Kleinbuchstaben umwandeln" + }, + { + "name": "strtotime", + "signature": "strtotime(string $datetime, int $baseTimestamp = time()): int|false", + "desc": "Datum-String in Timestamp umwandeln" + }, + { + "name": "strtoupper", + "signature": "strtoupper(string $string): string", + "desc": "In Großbuchstaben umwandeln" + }, + { + "name": "strval", + "signature": "strval(mixed $value): string", + "desc": "In String umwandeln" + }, + { + "name": "substr", + "signature": "substr(string $string, int $offset, int $length = null): string", + "desc": "Teilstring zurückgeben" + }, + { + "name": "sys_get_temp_dir", + "signature": "sys_get_temp_dir(): string", + "desc": "Temp-Verzeichnis des Systems" + }, + { + "name": "tempnam", + "signature": "tempnam(string $directory, string $prefix): string|false", + "desc": "Temporäre Datei erstellen" + }, + { + "name": "time", + "signature": "time(): int", + "desc": "Aktuellen Unix-Timestamp" + }, + { + "name": "trigger_error", + "signature": "trigger_error(string $message, int $error_level = E_USER_NOTICE): bool", + "desc": "Fehler auslösen" + }, + { + "name": "trim", + "signature": "trim(string $string, string $characters = \" \\n\\r\\t\\v\\0\"): string", + "desc": "Leerzeichen am Rand entfernen" + }, + { + "name": "uasort", + "signature": "uasort(array &$array, callable $callback): bool", + "desc": "Array mit eigener Funktion sortieren, Schlüssel beibehalten" + }, + { + "name": "ucfirst", + "signature": "ucfirst(string $string): string", + "desc": "Ersten Buchstaben groß" + }, + { + "name": "ucwords", + "signature": "ucwords(string $string, string $separators = \" \\t\\r\\n\\f\\v\"): string", + "desc": "Jeden Wortanfang groß" + }, + { + "name": "uksort", + "signature": "uksort(array &$array, callable $callback): bool", + "desc": "Array nach Schlüsseln mit eigener Funktion sortieren" + }, + { + "name": "unlink", + "signature": "unlink(string $filename, resource $context = null): bool", + "desc": "Datei löschen" + }, + { + "name": "urldecode", + "signature": "urldecode(string $string): string", + "desc": "URL-dekodieren" + }, + { + "name": "urlencode", + "signature": "urlencode(string $string): string", + "desc": "URL-kodieren" + }, + { + "name": "usleep", + "signature": "usleep(int $microseconds): void", + "desc": "Ausführung in Mikrosekunden anhalten" + }, + { + "name": "usort", + "signature": "usort(array &$array, callable $callback): bool", + "desc": "Array mit eigener Funktion sortieren" + }, + { + "name": "var_dump", + "signature": "var_dump(mixed $value, mixed ...$values): void", + "desc": "Variable ausgeben mit Typ-Info" + }, + { + "name": "var_export", + "signature": "var_export(mixed $value, bool $return = false): string|null", + "desc": "Variable als PHP-Code ausgeben" + }, + { + "name": "wordwrap", + "signature": "wordwrap(string $string, int $width = 75, string $break = \"\\n\", bool $cut_long_words = false): string", + "desc": "String umbrechen" + } +] \ No newline at end of file diff --git a/barecode/resources/resources.qrc b/barecode/resources/resources.qrc new file mode 100644 index 0000000..f3b588a --- /dev/null +++ b/barecode/resources/resources.qrc @@ -0,0 +1,12 @@ + + + icon_512.png + icon_256.png + icon_128.png + icon_64.png + icon_48.png + icon_32.png + icon_16.png + php_functions.json + + diff --git a/barecode/src/CMakeLists.txt b/barecode/src/CMakeLists.txt new file mode 100644 index 0000000..bb9abf8 --- /dev/null +++ b/barecode/src/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(core) +add_subdirectory(editor) +add_subdirectory(filetree) +add_subdirectory(highlighter) diff --git a/barecode/src/core/AboutDialog.cpp b/barecode/src/core/AboutDialog.cpp new file mode 100644 index 0000000..d6621d7 --- /dev/null +++ b/barecode/src/core/AboutDialog.cpp @@ -0,0 +1,105 @@ +#include "AboutDialog.h" + +#include +#include +#include +#include +#include +#include +#include + +AboutDialog::AboutDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("Über BareCode")); + setFixedSize(440, 310); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + QVBoxLayout *root = new QVBoxLayout(this); + root->setContentsMargins(0, 0, 0, 0); + root->setSpacing(0); + + // ----------------------------------------------------------------------- + // Header-Banner + // ----------------------------------------------------------------------- + QFrame *banner = new QFrame(this); + banner->setFixedHeight(88); + banner->setStyleSheet( + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0," + " stop:0 #1a1a2e, stop:1 #16213e);" + ); + + QVBoxLayout *bannerLayout = new QVBoxLayout(banner); + bannerLayout->setContentsMargins(24, 10, 24, 10); + bannerLayout->setSpacing(2); + + QLabel *appName = new QLabel("BareCode", banner); + QFont nameFont = appName->font(); + nameFont.setPointSize(22); + nameFont.setBold(true); + appName->setFont(nameFont); + appName->setStyleSheet("color: #e0e0ff; background: transparent;"); + + QLabel *tagline = new QLabel(tr("Modularer Code-Editor"), banner); + tagline->setStyleSheet("color: #8888bb; background: transparent;"); + + bannerLayout->addWidget(appName); + bannerLayout->addWidget(tagline); + root->addWidget(banner); + + // ----------------------------------------------------------------------- + // Info-Tabelle + // ----------------------------------------------------------------------- + QVBoxLayout *info = new QVBoxLayout(); + info->setContentsMargins(28, 20, 28, 8); + info->setSpacing(10); + + auto makeRow = [&](const QString &label, const QString &value) + { + QHBoxLayout *row = new QHBoxLayout(); + row->setSpacing(12); + + QLabel *lbl = new QLabel(label, this); + QFont boldFont = lbl->font(); + boldFont.setBold(true); + lbl->setFont(boldFont); + lbl->setFixedWidth(100); + lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + QLabel *val = new QLabel(value, this); + val->setTextInteractionFlags(Qt::TextSelectableByMouse); + + row->addWidget(lbl); + row->addWidget(val, 1); + info->addLayout(row); + }; + + makeRow(tr("Version"), "1.2.0"); + makeRow(tr("Entwickler"), "Dany Thinnes"); + makeRow(tr("Projekt"), "Projekt Hirnfrei"); + makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); + makeRow(tr("Sprache"), "C++17"); + + root->addLayout(info); + root->addStretch(); + + // ----------------------------------------------------------------------- + // Trennlinie + Schließen-Button + // ----------------------------------------------------------------------- + QFrame *line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + root->addWidget(line); + + QHBoxLayout *btnRow = new QHBoxLayout(); + btnRow->setContentsMargins(12, 8, 12, 12); + btnRow->addStretch(); + + QPushButton *btnClose = new QPushButton(tr("Schließen"), this); + btnClose->setDefault(true); + btnClose->setFixedWidth(110); + connect(btnClose, &QPushButton::clicked, this, &QDialog::accept); + btnRow->addWidget(btnClose); + + root->addLayout(btnRow); +} diff --git a/barecode/src/core/AboutDialog.h b/barecode/src/core/AboutDialog.h new file mode 100644 index 0000000..d90939a --- /dev/null +++ b/barecode/src/core/AboutDialog.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// --------------------------------------------------------------------------- +// AboutDialog – Zeigt Versionsinformationen und Entwicklerangaben. +// --------------------------------------------------------------------------- +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = nullptr); +}; diff --git a/barecode/src/core/CMakeLists.txt b/barecode/src/core/CMakeLists.txt new file mode 100644 index 0000000..e3b822e --- /dev/null +++ b/barecode/src/core/CMakeLists.txt @@ -0,0 +1,28 @@ +set(CORE_SOURCES + MainWindow.cpp + MainWindow.h + IPlugin.h + ProjectManager.cpp + ProjectManager.h + Settings.cpp + Settings.h + ThemeManager.cpp + ThemeManager.h + AboutDialog.cpp + AboutDialog.h +) + +add_library(BareCode_Core STATIC ${CORE_SOURCES}) + +target_link_libraries(BareCode_Core PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + BareCode_Editor + BareCode_FileTree +) + +target_include_directories(BareCode_Core PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/barecode/src/core/IPlugin.h b/barecode/src/core/IPlugin.h new file mode 100644 index 0000000..c7c3571 --- /dev/null +++ b/barecode/src/core/IPlugin.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// IPlugin – Interface that every BareCode module / plugin must implement. +// This allows components to be swapped or extended without touching the core. +// --------------------------------------------------------------------------- +class IPlugin +{ +public: + virtual ~IPlugin() = default; + + // Human-readable name of the plugin + virtual QString pluginName() const = 0; + + // Version string, e.g. "1.0.0" + virtual QString pluginVersion() const = 0; + + // Called once after all plugins are loaded so plugins can cross-reference + virtual void initialize() {} + + // Called before the application shuts down + virtual void shutdown() {} +}; diff --git a/barecode/src/core/MainWindow.cpp b/barecode/src/core/MainWindow.cpp new file mode 100644 index 0000000..38776bd --- /dev/null +++ b/barecode/src/core/MainWindow.cpp @@ -0,0 +1,412 @@ +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include + +#include "AboutDialog.h" +#include "filetree/FileTreePanel.h" +#include "editor/EditorPanel.h" + +// --------------------------------------------------------------------------- +// Konstruktor +// --------------------------------------------------------------------------- +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , m_projectManager(std::make_unique()) + , m_settings(std::make_unique()) + , m_themeManager(std::make_unique()) +{ + setWindowTitle("BareCode"); + setMinimumSize(900, 600); + + setupUi(); + setupMenuBar(); + setupStatusBar(); + connectSignals(); + restoreWindowState(); + applyInitialTheme(); + + // Letztes Projekt wieder öffnen + const QString lastPath = m_settings->lastProjectPath(); + if (!lastPath.isEmpty()) + { + m_projectManager->openProject(lastPath); + } + + // Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab) + m_editor->restoreSession( + m_settings->lastOpenFiles(), + m_settings->lastActiveFile() + ); +} + +MainWindow::~MainWindow() = default; + +// --------------------------------------------------------------------------- +// Datei(en) von der Kommandozeile öffnen +// --------------------------------------------------------------------------- +void MainWindow::openFilesFromArguments(const QStringList &filePaths) +{ + for (const QString &path : filePaths) + { + const QFileInfo info(path); + if (info.exists() && info.isFile()) + { + m_editor->openFile(info.absoluteFilePath()); + } + } +} + +// --------------------------------------------------------------------------- +// UI aufbauen +// --------------------------------------------------------------------------- +void MainWindow::setupUi() +{ + m_splitter = new QSplitter(Qt::Horizontal, this); + setCentralWidget(m_splitter); + + m_fileTree = new FileTreePanel(m_splitter); + m_editor = new EditorPanel(m_settings.get(), m_splitter); + + m_splitter->addWidget(m_fileTree); + m_splitter->addWidget(m_editor); + + const int treeWidth = m_settings->fileTreeWidth(); + m_splitter->setSizes({treeWidth, width() - treeWidth}); + m_splitter->setStretchFactor(0, 0); + m_splitter->setStretchFactor(1, 1); +} + +// --------------------------------------------------------------------------- +// Menüleiste +// --------------------------------------------------------------------------- +void MainWindow::setupMenuBar() +{ + // ---- Datei ---- + QMenu *mDatei = menuBar()->addMenu(tr("&Datei")); + + m_actNewFile = mDatei->addAction(tr("&Neue Datei…"), this, &MainWindow::onNewFile); + m_actNewFile->setShortcut(QKeySequence::New); + + mDatei->addSeparator(); + + m_actOpenFile = mDatei->addAction(tr("Datei &öffnen…"), this, &MainWindow::onOpenFile); + m_actOpenFile->setShortcut(QKeySequence::Open); + + m_actOpenProject = mDatei->addAction(tr("&Projekt öffnen…"), this, &MainWindow::onOpenProject); + m_actOpenProject->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O); + + m_actClose = mDatei->addAction(tr("Projekt &schließen"), this, &MainWindow::onCloseProject); + + mDatei->addSeparator(); + + m_actSave = mDatei->addAction(tr("&Speichern"), this, &MainWindow::onSave); + m_actSave->setShortcut(QKeySequence::Save); + + m_actSaveAs = mDatei->addAction(tr("Speichern &unter…"), this, &MainWindow::onSaveAs); + m_actSaveAs->setShortcut(QKeySequence::SaveAs); + + m_actSaveAll = mDatei->addAction(tr("&Alles speichern"), this, &MainWindow::onSaveAll); + m_actSaveAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S); + + mDatei->addSeparator(); + + m_actQuit = mDatei->addAction(tr("&Beenden"), qApp, &QApplication::quit); + m_actQuit->setShortcut(QKeySequence::Quit); + + // ---- Bearbeiten ---- + QMenu *mBearbeiten = menuBar()->addMenu(tr("&Bearbeiten")); + + m_actUndo = mBearbeiten->addAction(tr("&Rückgängig"), this, &MainWindow::onUndo); + m_actUndo->setShortcut(QKeySequence::Undo); + + m_actRedo = mBearbeiten->addAction(tr("&Wiederholen"), this, &MainWindow::onRedo); + m_actRedo->setShortcut(QKeySequence::Redo); + + mBearbeiten->addSeparator(); + + m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch); + m_actSearch->setShortcut(QKeySequence::Find); + + m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch); + m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F); + + mBearbeiten->addSeparator(); + + m_actFuncList = mBearbeiten->addAction(tr("&Projektfunktionen…"), this, &MainWindow::onShowFunctionList); + m_actFuncList->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P); + + m_actDeadCode = mBearbeiten->addAction(tr("&Tote Funktionen suchen…"), this, &MainWindow::onShowDeadCode); + m_actDeadCode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_T); + + // ---- Ansicht ---- + QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht")); + + m_actDarkMode = mAnsicht->addAction(tr("&Dark Mode")); + m_actDarkMode->setCheckable(true); + m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D); + connect(m_actDarkMode, &QAction::toggled, this, &MainWindow::onToggleDarkMode); + + mAnsicht->addSeparator(); + + // Sprachauswahl + QMenu *mSprache = mAnsicht->addMenu(tr("Sprache")); + m_langGroup = new QActionGroup(this); + m_langGroup->setExclusive(true); + + const QSettings langSettings(QSettings::IniFormat, QSettings::UserScope, + "BareCode", "BareCode"); + const QString currentLocale = langSettings.value("language/locale", + QLocale::system().name()).toString(); + + struct LangEntry { QString locale; QString label; }; + const QList languages = { + { "de", "Deutsch" }, + { "en", "English" }, + }; + + for (const LangEntry &lang : languages) + { + QAction *act = mSprache->addAction(lang.label); + act->setCheckable(true); + act->setData(lang.locale); + act->setChecked(currentLocale.startsWith(lang.locale)); + m_langGroup->addAction(act); + connect(act, &QAction::triggered, this, [this, lang]() + { + onLanguageChanged(lang.locale); + }); + } + + // ---- Hilfe ---- + QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe")); + m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout); +} + +void MainWindow::setupStatusBar() +{ + statusBar()->showMessage(tr("Bereit")); +} + +// --------------------------------------------------------------------------- +// Signale verbinden +// --------------------------------------------------------------------------- +void MainWindow::connectSignals() +{ + connect(m_projectManager.get(), &ProjectManager::projectOpened, + this, &MainWindow::onProjectOpened); + + connect(m_projectManager.get(), &ProjectManager::projectClosed, + this, &MainWindow::onProjectClosed); + + // Dateibaum → Editor + connect(m_fileTree, &FileTreePanel::fileActivated, + m_editor, &EditorPanel::openFile); + + connect(m_fileTree, &FileTreePanel::fileCreated, + m_editor, &EditorPanel::openFile); + + // Gespeichert → Statusleiste + connect(m_editor, &EditorPanel::currentFileSaved, this, [this](const QString &path) + { + statusBar()->showMessage(tr("Gespeichert: %1").arg(path), 3000); + }); + + // Splitter-Breite merken + connect(m_splitter, &QSplitter::splitterMoved, this, [this](int pos, int) + { + m_settings->setFileTreeWidth(pos); + }); +} + +// --------------------------------------------------------------------------- +// Theme beim Start +// --------------------------------------------------------------------------- +void MainWindow::applyInitialTheme() +{ + const bool dark = m_settings->darkMode(); + // Block damit toggled-Signal nicht doppelt feuert + m_actDarkMode->blockSignals(true); + m_actDarkMode->setChecked(dark); + m_actDarkMode->blockSignals(false); + + m_themeManager->applyTheme(dark ? ThemeManager::Theme::Dark + : ThemeManager::Theme::Light); +} + +// --------------------------------------------------------------------------- +// Slots – Datei +// --------------------------------------------------------------------------- +void MainWindow::onNewFile() +{ + m_fileTree->triggerNewFile(); +} + +void MainWindow::onOpenFile() +{ + const QString path = QFileDialog::getOpenFileName( + this, + tr("Datei öffnen"), + m_settings->lastProjectPath() + ); + + if (!path.isEmpty()) + { + m_editor->openFile(path); + } +} + +void MainWindow::onOpenProject() +{ + const QString path = QFileDialog::getExistingDirectory( + this, + tr("Projektverzeichnis öffnen"), + m_settings->lastProjectPath() + ); + + if (!path.isEmpty()) + { + m_projectManager->openProject(path); + m_settings->setLastProjectPath(path); + } +} + +void MainWindow::onCloseProject() +{ + m_projectManager->closeProject(); +} + +void MainWindow::onSave() +{ + m_editor->saveCurrentFile(); +} + +void MainWindow::onSaveAs() +{ + m_editor->saveCurrentFileAs(); +} + +void MainWindow::onSaveAll() +{ + m_editor->saveAllFiles(); + statusBar()->showMessage(tr("Alle Dateien gespeichert"), 3000); +} + +// --------------------------------------------------------------------------- +// Slots – Bearbeiten +// --------------------------------------------------------------------------- +void MainWindow::onUndo() +{ + m_editor->undo(); +} + +void MainWindow::onRedo() +{ + m_editor->redo(); +} + +void MainWindow::onShowSearch() +{ + m_editor->showSearchPanel(); +} + +// --------------------------------------------------------------------------- +// Slots – Ansicht +// --------------------------------------------------------------------------- +void MainWindow::onToggleDarkMode(bool checked) +{ + m_themeManager->applyTheme(checked ? ThemeManager::Theme::Dark + : ThemeManager::Theme::Light); + m_settings->setDarkMode(checked); +} + +void MainWindow::onLanguageChanged(const QString &locale) +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + s.setValue("language/locale", locale); + + QMessageBox::information(this, + tr("Sprache geändert"), + tr("Die Sprache wird beim nächsten Start von BareCode aktiv.") + ); +} + +// --------------------------------------------------------------------------- +// Slots – Hilfe +// --------------------------------------------------------------------------- +void MainWindow::onAbout() +{ + AboutDialog dlg(this); + dlg.exec(); +} + +// --------------------------------------------------------------------------- +// Slots – Projekt +// --------------------------------------------------------------------------- +void MainWindow::onShowDeadCode() +{ + m_editor->showDeadCode(); +} + +void MainWindow::onShowFunctionList() +{ + m_editor->showFunctionList(); +} + +void MainWindow::onShowFileSearch() +{ + m_editor->showFileSearchPanel(); +} + +void MainWindow::onProjectOpened(const QString &path) +{ + setWindowTitle(QString("BareCode – %1").arg(path)); + m_fileTree->setRootPath(path); + m_editor->setSearchRoot(path); + statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000); +} + +void MainWindow::onProjectClosed() +{ + setWindowTitle("BareCode"); + m_fileTree->clearRoot(); + m_editor->setSearchRoot(QString()); + statusBar()->showMessage(tr("Projekt geschlossen"), 3000); +} + +// --------------------------------------------------------------------------- +// Fenster-Zustand +// --------------------------------------------------------------------------- +void MainWindow::saveWindowState() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + s.setValue("window/geometry", saveGeometry()); + s.setValue("window/state", saveState()); + + // Session speichern + m_settings->setLastOpenFiles(m_editor->openFilePaths()); + m_settings->setLastActiveFile(m_editor->activeFilePath()); +} + +void MainWindow::restoreWindowState() +{ + QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode"); + if (s.contains("window/geometry")) + { + restoreGeometry(s.value("window/geometry").toByteArray()); + } + if (s.contains("window/state")) + { + restoreState(s.value("window/state").toByteArray()); + } +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + saveWindowState(); + event->accept(); +} diff --git a/barecode/src/core/MainWindow.h b/barecode/src/core/MainWindow.h new file mode 100644 index 0000000..beced93 --- /dev/null +++ b/barecode/src/core/MainWindow.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "ProjectManager.h" +#include "Settings.h" +#include "ThemeManager.h" + +class FileTreePanel; +class EditorPanel; + +// --------------------------------------------------------------------------- +// MainWindow – Hauptfenster. Besitzt alle zentralen Dienste und das Layout. +// --------------------------------------------------------------------------- +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + + // Datei(en) öffnen, die beim Programmstart per Kommandozeile übergeben + // wurden — z. B. durch Doppelklick auf eine Datei im Dateibrowser + // ("Öffnen mit BareCode"). + void openFilesFromArguments(const QStringList &filePaths); + +protected: + void closeEvent(QCloseEvent *event) override; + +private slots: + // Datei + void onNewFile(); + void onOpenFile(); + void onOpenProject(); + void onCloseProject(); + void onSave(); + void onSaveAs(); + void onSaveAll(); + // Bearbeiten + void onUndo(); + void onRedo(); + void onShowSearch(); + void onShowFileSearch(); + void onShowFunctionList(); + void onShowDeadCode(); + // Ansicht + void onToggleDarkMode(bool checked); + void onLanguageChanged(const QString &locale); + // Hilfe + void onAbout(); + // Intern + void onProjectOpened(const QString &path); + void onProjectClosed(); + +private: + void setupUi(); + void setupMenuBar(); + void setupStatusBar(); + void connectSignals(); + void applyInitialTheme(); + void saveWindowState(); + void restoreWindowState(); + + // Dienste + std::unique_ptr m_projectManager; + std::unique_ptr m_settings; + std::unique_ptr m_themeManager; + + // Layout + QSplitter *m_splitter = nullptr; + FileTreePanel *m_fileTree = nullptr; + EditorPanel *m_editor = nullptr; + + // Aktionen + QAction *m_actNewFile = nullptr; + QAction *m_actOpenFile = nullptr; + QAction *m_actOpenProject = nullptr; + QAction *m_actClose = nullptr; + QAction *m_actSave = nullptr; + QAction *m_actSaveAs = nullptr; + QAction *m_actSaveAll = nullptr; + QAction *m_actQuit = nullptr; + QAction *m_actUndo = nullptr; + QAction *m_actRedo = nullptr; + QAction *m_actSearch = nullptr; + QAction *m_actFileSearch = nullptr; + QAction *m_actFuncList = nullptr; + QAction *m_actDeadCode = nullptr; + QAction *m_actDarkMode = nullptr; + QActionGroup *m_langGroup = nullptr; + QAction *m_actAbout = nullptr; +}; diff --git a/barecode/src/core/ProjectManager.cpp b/barecode/src/core/ProjectManager.cpp new file mode 100644 index 0000000..c4ee879 --- /dev/null +++ b/barecode/src/core/ProjectManager.cpp @@ -0,0 +1,33 @@ +#include "ProjectManager.h" + +ProjectManager::ProjectManager(QObject *parent) + : QObject(parent) +{ +} + +QString ProjectManager::currentProjectPath() const +{ + return m_projectPath; +} + +void ProjectManager::openProject(const QString &path) +{ + if (m_projectPath == path) + { + return; + } + + m_projectPath = path; + emit projectOpened(m_projectPath); +} + +void ProjectManager::closeProject() +{ + if (m_projectPath.isEmpty()) + { + return; + } + + m_projectPath.clear(); + emit projectClosed(); +} diff --git a/barecode/src/core/ProjectManager.h b/barecode/src/core/ProjectManager.h new file mode 100644 index 0000000..9aedfcc --- /dev/null +++ b/barecode/src/core/ProjectManager.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// ProjectManager – Tracks the currently open project directory and emits +// signals when the project changes so other components can react. +// --------------------------------------------------------------------------- +class ProjectManager : public QObject +{ + Q_OBJECT + +public: + explicit ProjectManager(QObject *parent = nullptr); + + QString currentProjectPath() const; + void openProject(const QString &path); + void closeProject(); + +signals: + void projectOpened(const QString &path); + void projectClosed(); + +private: + QString m_projectPath; +}; diff --git a/barecode/src/core/Settings.cpp b/barecode/src/core/Settings.cpp new file mode 100644 index 0000000..4ebc21f --- /dev/null +++ b/barecode/src/core/Settings.cpp @@ -0,0 +1,113 @@ +#include "Settings.h" + +Settings::Settings(QObject *parent) + : QObject(parent) + , m_settings(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode") +{ +} + +// --------------------------------------------------------------------------- +// Editor font +// --------------------------------------------------------------------------- +QFont Settings::editorFont() const +{ + QFont defaultFont("Monospace", 11); + defaultFont.setStyleHint(QFont::Monospace); + return m_settings.value("editor/font", defaultFont).value(); +} + +void Settings::setEditorFont(const QFont &font) +{ + m_settings.setValue("editor/font", font); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// Tab size +// --------------------------------------------------------------------------- +int Settings::tabSize() const +{ + return m_settings.value("editor/tabSize", 4).toInt(); +} + +void Settings::setTabSize(int size) +{ + m_settings.setValue("editor/tabSize", size); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// Spaces vs. tabs +// --------------------------------------------------------------------------- +bool Settings::useSpacesForTabs() const +{ + return m_settings.value("editor/useSpacesForTabs", true).toBool(); +} + +void Settings::setUseSpacesForTabs(bool use) +{ + m_settings.setValue("editor/useSpacesForTabs", use); + emit settingsChanged(); +} + +// --------------------------------------------------------------------------- +// File tree width +// --------------------------------------------------------------------------- +int Settings::fileTreeWidth() const +{ + return m_settings.value("layout/fileTreeWidth", 240).toInt(); +} + +void Settings::setFileTreeWidth(int width) +{ + m_settings.setValue("layout/fileTreeWidth", width); +} + +// --------------------------------------------------------------------------- +// Dark mode +// --------------------------------------------------------------------------- +bool Settings::darkMode() const +{ + return m_settings.value("appearance/darkMode", false).toBool(); +} + +void Settings::setDarkMode(bool dark) +{ + m_settings.setValue("appearance/darkMode", dark); +} + +// --------------------------------------------------------------------------- +// Last project path +// --------------------------------------------------------------------------- +QString Settings::lastProjectPath() const +{ + return m_settings.value("project/lastPath", QString()).toString(); +} + +void Settings::setLastProjectPath(const QString &path) +{ + m_settings.setValue("project/lastPath", path); +} + +// --------------------------------------------------------------------------- +// Session – geöffnete Dateien +// --------------------------------------------------------------------------- +QStringList Settings::lastOpenFiles() const +{ + return m_settings.value("session/openFiles", QStringList()).toStringList(); +} + +void Settings::setLastOpenFiles(const QStringList &files) +{ + m_settings.setValue("session/openFiles", files); +} + +QString Settings::lastActiveFile() const +{ + return m_settings.value("session/activeFile", QString()).toString(); +} + +void Settings::setLastActiveFile(const QString &file) +{ + m_settings.setValue("session/activeFile", file); +} diff --git a/barecode/src/core/Settings.h b/barecode/src/core/Settings.h new file mode 100644 index 0000000..1122864 --- /dev/null +++ b/barecode/src/core/Settings.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Settings – Centralised persistent application settings. +// --------------------------------------------------------------------------- +class Settings : public QObject +{ + Q_OBJECT + +public: + explicit Settings(QObject *parent = nullptr); + + // Editor + QFont editorFont() const; + void setEditorFont(const QFont &font); + + int tabSize() const; + void setTabSize(int size); + + bool useSpacesForTabs() const; + void setUseSpacesForTabs(bool use); + + // Layout + int fileTreeWidth() const; + void setFileTreeWidth(int width); + + // Zuletzt geöffnete Dateien (Session-Wiederherstellung) + QStringList lastOpenFiles() const; + void setLastOpenFiles(const QStringList &files); + + QString lastActiveFile() const; + void setLastActiveFile(const QString &file); + + // Recent + QString lastProjectPath() const; + void setLastProjectPath(const QString &path); + + // Erscheinungsbild + bool darkMode() const; + void setDarkMode(bool dark); + +signals: + void settingsChanged(); + +private: + QSettings m_settings; +}; diff --git a/barecode/src/core/ThemeManager.cpp b/barecode/src/core/ThemeManager.cpp new file mode 100644 index 0000000..d9c9ba7 --- /dev/null +++ b/barecode/src/core/ThemeManager.cpp @@ -0,0 +1,115 @@ +#include "ThemeManager.h" + +#include +#include + +ThemeManager::ThemeManager(QObject *parent) + : QObject(parent) +{ +} + +void ThemeManager::applyTheme(Theme theme) +{ + m_currentTheme = theme; + QApplication::setStyle(QStyleFactory::create("Fusion")); + + if (theme == Theme::Dark) + { + QApplication::setPalette(buildDarkPalette()); + } + else + { + QApplication::setPalette(buildLightPalette()); + } + + emit themeChanged(theme); +} + +ThemeManager::Theme ThemeManager::currentTheme() const +{ + return m_currentTheme; +} + +QPalette ThemeManager::buildDarkPalette() +{ + QPalette p; + + const QColor bg = QColor("#1e1e1e"); + const QColor widget = QColor("#252526"); + const QColor alt = QColor("#2d2d30"); + const QColor hi = QColor("#264f78"); + const QColor hiText = QColor("#ffffff"); + const QColor text = QColor("#d4d4d4"); + const QColor disabled = QColor("#6d6d6d"); + const QColor btn = QColor("#3c3c3c"); + const QColor mid = QColor("#333333"); + const QColor dark = QColor("#1a1a1a"); + const QColor light = QColor("#454545"); + const QColor link = QColor("#569cd6"); + + p.setColor(QPalette::Window, bg); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, widget); + p.setColor(QPalette::AlternateBase, alt); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, btn); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::Highlight, hi); + p.setColor(QPalette::HighlightedText, hiText); + p.setColor(QPalette::Link, link); + p.setColor(QPalette::LinkVisited, link.darker(120)); + p.setColor(QPalette::Mid, mid); + p.setColor(QPalette::Dark, dark); + p.setColor(QPalette::Light, light); + p.setColor(QPalette::Shadow, QColor("#000000")); + p.setColor(QPalette::ToolTipBase, widget); + p.setColor(QPalette::ToolTipText, text); + p.setColor(QPalette::PlaceholderText, disabled); + + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + + return p; +} + +QPalette ThemeManager::buildLightPalette() +{ + // Fusion-Standard-Palette + QPalette p; + + const QColor bg = QColor("#f3f3f3"); + const QColor widget = QColor("#ffffff"); + const QColor alt = QColor("#e8e8e8"); + const QColor hi = QColor("#0078d4"); + const QColor hiText = QColor("#ffffff"); + const QColor text = QColor("#1e1e1e"); + const QColor disabled = QColor("#a0a0a0"); + const QColor btn = QColor("#e1e1e1"); + const QColor mid = QColor("#c8c8c8"); + const QColor dark = QColor("#a0a0a0"); + const QColor light = QColor("#ffffff"); + const QColor link = QColor("#0078d4"); + + p.setColor(QPalette::Window, bg); + p.setColor(QPalette::WindowText, text); + p.setColor(QPalette::Base, widget); + p.setColor(QPalette::AlternateBase, alt); + p.setColor(QPalette::Text, text); + p.setColor(QPalette::Button, btn); + p.setColor(QPalette::ButtonText, text); + p.setColor(QPalette::Highlight, hi); + p.setColor(QPalette::HighlightedText, hiText); + p.setColor(QPalette::Link, link); + p.setColor(QPalette::LinkVisited, link.darker(130)); + p.setColor(QPalette::Mid, mid); + p.setColor(QPalette::Dark, dark); + p.setColor(QPalette::Light, light); + p.setColor(QPalette::PlaceholderText, disabled); + + p.setColor(QPalette::Disabled, QPalette::WindowText, disabled); + p.setColor(QPalette::Disabled, QPalette::Text, disabled); + p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled); + + return p; +} diff --git a/barecode/src/core/ThemeManager.h b/barecode/src/core/ThemeManager.h new file mode 100644 index 0000000..4096918 --- /dev/null +++ b/barecode/src/core/ThemeManager.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// ThemeManager – Schaltet zwischen Hell- und Dunkelmodus um. +// --------------------------------------------------------------------------- +class ThemeManager : public QObject +{ + Q_OBJECT + +public: + enum class Theme { Light, Dark }; + + explicit ThemeManager(QObject *parent = nullptr); + + void applyTheme(Theme theme); + Theme currentTheme() const; + +signals: + void themeChanged(Theme theme); + +private: + static QPalette buildDarkPalette(); + static QPalette buildLightPalette(); + + Theme m_currentTheme = Theme::Light; +}; diff --git a/barecode/src/editor/CMakeLists.txt b/barecode/src/editor/CMakeLists.txt new file mode 100644 index 0000000..c730a1f --- /dev/null +++ b/barecode/src/editor/CMakeLists.txt @@ -0,0 +1,49 @@ +set(EDITOR_SOURCES + EditorPanel.cpp + EditorPanel.h + CodeEditor.cpp + CodeEditor.h + LineNumberArea.cpp + LineNumberArea.h + EditorTab.cpp + EditorTab.h + SearchPanel.cpp + SearchPanel.h + FileSearchPanel.cpp + FileSearchPanel.h + ColorIndicator.cpp + ColorIndicator.h + SignatureHelper.cpp + SignatureHelper.h + SignatureTooltip.cpp + SignatureTooltip.h + FunctionScanner.cpp + FunctionScanner.h + FunctionIndex.cpp + FunctionIndex.h + FunctionListPanel.cpp + FunctionListPanel.h + FunctionListDialog.cpp + FunctionListDialog.h + DeadCodeAnalyzer.cpp + DeadCodeAnalyzer.h + DeadCodeDialog.cpp + DeadCodeDialog.h + VariableCompleter.cpp + VariableCompleter.h +) + +add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) + +target_link_libraries(BareCode_Editor PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Concurrent + BareCode_Highlighter +) + +target_include_directories(BareCode_Editor PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/barecode/src/editor/CodeEditor.cpp b/barecode/src/editor/CodeEditor.cpp new file mode 100644 index 0000000..948faa8 --- /dev/null +++ b/barecode/src/editor/CodeEditor.cpp @@ -0,0 +1,762 @@ +#include "CodeEditor.h" +#include "LineNumberArea.h" +#include "ColorIndicator.h" +#include "SignatureHelper.h" +#include "FunctionIndex.h" +#include "VariableCompleter.h" + +#include "core/Settings.h" +#include "highlighter/HighlighterFactory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CodeEditor::CodeEditor(Settings *settings, QWidget *parent) + : QPlainTextEdit(parent) + , m_settings(settings) +{ + m_lineNumberArea = new LineNumberArea(this); + m_colorIndicator = new ColorIndicator(this); + m_signatureHelper = new SignatureHelper(this); + m_varCompleter = new VariableCompleter(this); + setupEditor(); + + connect(this, &CodeEditor::blockCountChanged, + this, &CodeEditor::updateLineNumberAreaWidth); + + connect(this, &CodeEditor::updateRequest, + this, &CodeEditor::updateLineNumberArea); + + connect(this, &CodeEditor::cursorPositionChanged, + this, &CodeEditor::highlightCurrentLine); + + updateLineNumberAreaWidth(0); + highlightCurrentLine(); +} + +CodeEditor::~CodeEditor() = default; + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void CodeEditor::setupEditor() +{ + applySettings(); + setLineWrapMode(QPlainTextEdit::NoWrap); +} + +void CodeEditor::applySettings() +{ + setFont(m_settings->editorFont()); + + const int tabStop = m_settings->tabSize(); + // Set tab stop width in pixels using font metrics + QFontMetrics fm(m_settings->editorFont()); + setTabStopDistance(static_cast(tabStop) * fm.horizontalAdvance(' ')); +} + +// --------------------------------------------------------------------------- +// File I/O +// --------------------------------------------------------------------------- +void CodeEditor::loadFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QMessageBox::warning(this, tr("Open File"), + tr("Cannot open file:\n%1").arg(filePath)); + return; + } + + m_filePath = filePath; + + QTextStream in(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + in.setEncoding(QStringConverter::Utf8); +#else + in.setCodec("UTF-8"); +#endif + + setPlainText(in.readAll()); + document()->setModified(false); + + installHighlighter(filePath); +} + +QString CodeEditor::filePath() const +{ + return m_filePath; +} + +bool CodeEditor::save() +{ + if (m_filePath.isEmpty()) + { + return saveAs(); + } + + return writeToFile(m_filePath); +} + +bool CodeEditor::saveAs() +{ + const QString path = QFileDialog::getSaveFileName( + this, + tr("Speichern unter"), + m_filePath + ); + + if (path.isEmpty()) + { + return false; + } + + m_filePath = path; + installHighlighter(m_filePath); + return writeToFile(m_filePath); +} + +bool CodeEditor::writeToFile(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::warning(this, tr("Speichern"), + tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath)); + return false; + } + + QTextStream out(&file); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + out.setEncoding(QStringConverter::Utf8); +#else + out.setCodec("UTF-8"); +#endif + + out << toPlainText(); + document()->setModified(false); + emit fileSaved(filePath); + return true; +} + +void CodeEditor::installHighlighter(const QString &filePath) +{ + // Remove old highlighter first + delete m_highlighter; + m_highlighter = nullptr; + + m_highlighter = HighlighterFactory::createForFile(filePath, document()); +} + +bool CodeEditor::isModified() const +{ + return document()->isModified(); +} + +// --------------------------------------------------------------------------- +// Line number area +// --------------------------------------------------------------------------- +int CodeEditor::lineNumberAreaWidth() const +{ + int digits = 1; + int max = qMax(1, blockCount()); + while (max >= 10) + { + max /= 10; + ++digits; + } + + const int padding = 8; + return fontMetrics().horizontalAdvance('9') * digits + padding * 2; +} + +void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/) +{ + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); +} + +void CodeEditor::updateLineNumberArea(const QRect &rect, int dy) +{ + if (dy != 0) + { + m_lineNumberArea->scroll(0, dy); + } + else + { + m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height()); + } + + if (rect.contains(viewport()->rect())) + { + updateLineNumberAreaWidth(0); + } +} + +void CodeEditor::paintEvent(QPaintEvent *event) +{ + // Zuerst den normalen Editor-Inhalt zeichnen + QPlainTextEdit::paintEvent(event); + + // Einrück-Führungslinien + const int tabSize = m_settings->tabSize(); + if (tabSize > 0) + { + QPainter painter(viewport()); + + QColor guideColor = palette().color(QPalette::Text); + guideColor.setAlpha(30); + painter.setPen(QPen(guideColor, 1, Qt::SolidLine)); + + const QFontMetrics fm(font()); + const int spaceWidth = fm.horizontalAdvance(' '); + const int tabPixels = tabSize * spaceWidth; + + if (tabPixels > 0) + { + int textOriginX = 0; + { + QTextBlock firstBlock = firstVisibleBlock(); + if (!firstBlock.isValid()) + { + firstBlock = document()->begin(); + } + if (firstBlock.isValid()) + { + const QRectF blockRect = blockBoundingGeometry(firstBlock) + .translated(contentOffset()); + const QTextLayout *layout = firstBlock.layout(); + if (layout && layout->lineCount() > 0) + { + textOriginX = static_cast(blockRect.left() + + layout->lineAt(0).position().x()); + } + else + { + textOriginX = static_cast(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(blockRect.top()), + xPixel, + static_cast(blockRect.bottom())); + } + } + block = block.next(); + } + } + + // Farbvorschau-Quadrate zeichnen + m_colorIndicator->paint(painter, event); + } +} + +void CodeEditor::setFunctionIndex(FunctionIndex *index) +{ + m_functionIndex = index; +} + +void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event) +{ + // Zuerst normales Verhalten — markiert das Wort unter dem Cursor + QPlainTextEdit::mouseDoubleClickEvent(event); + + if (!m_functionIndex || !m_functionIndex->isReady()) + { + return; + } + + // Markiertes Wort auslesen + const QString word = textCursor().selectedText().trimmed(); + if (word.isEmpty() || word.contains(' ')) + { + return; + } + + // Im Funktionsindex nachschlagen + const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word); + if (info.filePath.isEmpty()) + { + return; // Nicht gefunden — normales Verhalten bleibt + } + + // Nicht zur eigenen Definition springen wenn wir bereits dort sind + if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1) + { + return; + } + + emit navigateToRequested(info.filePath, info.line); +} + +void CodeEditor::mousePressEvent(QMouseEvent *event) +{ + // Zuerst prüfen ob ein Farbquadrat geklickt wurde + if (m_colorIndicator->handleMousePress(event)) + { + return; + } + QPlainTextEdit::mousePressEvent(event); +} + +void CodeEditor::resizeEvent(QResizeEvent *event) +{ + QPlainTextEdit::resizeEvent(event); + + const QRect cr = contentsRect(); + m_lineNumberArea->setGeometry( + QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()) + ); +} + +void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) +{ + QPainter painter(m_lineNumberArea); + + // Background + const QColor bgColor = palette().color(QPalette::Window).darker(110); + painter.fillRect(event->rect(), bgColor); + + const QColor lineNumColor = palette().color(QPalette::Mid); + const QColor activeColor = palette().color(QPalette::Text); + + const int currentLine = textCursor().blockNumber(); + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = static_cast(blockBoundingGeometry(block).translated(contentOffset()).top()); + int bottom = top + static_cast(blockBoundingRect(block).height()); + + while (block.isValid() && top <= event->rect().bottom()) + { + if (block.isVisible() && bottom >= event->rect().top()) + { + const QString number = QString::number(blockNumber + 1); + painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor); + painter.drawText( + 0, + top, + m_lineNumberArea->width() - 4, + fontMetrics().height(), + Qt::AlignRight, + number + ); + } + + block = block.next(); + top = bottom; + bottom = top + static_cast(blockBoundingRect(block).height()); + ++blockNumber; + } +} + +// --------------------------------------------------------------------------- +// Current line highlight + Klammerzugehörigkeit +// --------------------------------------------------------------------------- +void CodeEditor::highlightCurrentLine() +{ + QList extraSelections; + + if (!isReadOnly()) + { + // Aktuelle Zeile hervorheben + QTextEdit::ExtraSelection lineSelection; + lineSelection.format.setBackground(palette().color(QPalette::AlternateBase)); + lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true); + lineSelection.cursor = textCursor(); + lineSelection.cursor.clearSelection(); + extraSelections.append(lineSelection); + + // Klammerzugehörigkeit + matchBrackets(extraSelections); + } + + setExtraSelections(extraSelections); +} + +void CodeEditor::matchBrackets(QList &selections) +{ + static const QString openBrackets = "({["; + static const QString closeBrackets = ")}]"; + + QTextCursor cursor = textCursor(); + const QString blockText = cursor.block().text(); + const int col = cursor.columnNumber(); + + // Zeichen unter oder links vom Cursor prüfen + QChar ch; + int charPos = -1; + + // Zuerst Zeichen unter dem Cursor + if (col < blockText.length()) + { + ch = blockText[col]; + if (openBrackets.contains(ch) || closeBrackets.contains(ch)) + { + charPos = col; + } + } + + // Dann Zeichen links vom Cursor + if (charPos == -1 && col > 0) + { + ch = blockText[col - 1]; + if (openBrackets.contains(ch) || closeBrackets.contains(ch)) + { + charPos = col - 1; + } + } + + if (charPos == -1) + { + return; + } + + // Passende Klammer suchen + const bool isOpen = openBrackets.contains(ch); + const int bracketIndex = isOpen + ? openBrackets.indexOf(ch) + : closeBrackets.indexOf(ch); + const QChar matchChar = isOpen + ? closeBrackets[bracketIndex] + : openBrackets[bracketIndex]; + + // Dokumentposition der gefundenen Klammer + const int startPos = cursor.block().position() + charPos; + + // Passende Klammer suchen — vorwärts oder rückwärts + int depth = 0; + int matchPos = -1; + + if (isOpen) + { + // Vorwärts suchen + QTextCursor search(document()); + search.setPosition(startPos); + + while (!search.atEnd()) + { + search.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + const QChar c = search.selectedText()[0]; + search.clearSelection(); + + if (c == ch) { ++depth; } + else if (c == matchChar) + { + --depth; + if (depth == 0) + { + matchPos = search.position() - 1; + break; + } + } + } + } + else + { + // Rückwärts suchen + QTextCursor search(document()); + search.setPosition(startPos + 1); + + while (search.position() > 0) + { + search.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); + const QChar c = search.selectedText()[0]; + search.clearSelection(); + + if (c == ch) { ++depth; } + else if (c == matchChar) + { + --depth; + if (depth == 0) + { + matchPos = search.position(); + break; + } + } + } + } + + // Formatierung + QTextCharFormat matchFormat; + if (matchPos >= 0) + { + // Gefunden — beide Klammern grün hervorheben + matchFormat.setBackground(QColor("#1a5a1a")); + matchFormat.setForeground(QColor("#88ff88")); + matchFormat.setFontWeight(QFont::Bold); + } + else + { + // Kein Match — rot markieren + matchFormat.setBackground(QColor("#5a1a1a")); + matchFormat.setForeground(QColor("#ff8888")); + matchFormat.setFontWeight(QFont::Bold); + } + + // Öffnende / schließende Klammer markieren + QTextEdit::ExtraSelection sel1; + sel1.cursor = QTextCursor(document()); + sel1.cursor.setPosition(startPos); + sel1.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + sel1.format = matchFormat; + selections.append(sel1); + + // Passende Klammer markieren (nur wenn gefunden) + if (matchPos >= 0) + { + QTextEdit::ExtraSelection sel2; + sel2.cursor = QTextCursor(document()); + sel2.cursor.setPosition(matchPos); + sel2.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + sel2.format = matchFormat; + selections.append(sel2); + } +} + +// --------------------------------------------------------------------------- +// Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent +// --------------------------------------------------------------------------- +void CodeEditor::keyPressEvent(QKeyEvent *event) +{ + const int tabSize = m_settings->tabSize(); + + // ----------------------------------------------------------------------- + // Shift+Tab: Einrückung zurückziehen + // ----------------------------------------------------------------------- + if (event->key() == Qt::Key_Backtab || + (event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier)) + { + QTextCursor cursor = textCursor(); + + QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); + QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); + + // Wenn die Selektion genau am Anfang des letzten Blocks endet, + // diesen Block nicht mit einbeziehen — der Cursor steht dort nur + // mit Position 0, der Nutzer hat die Zeile nicht markiert + if (cursor.hasSelection() && + cursor.selectionEnd() == endBlock.position() && + endBlock != startBlock) + { + endBlock = endBlock.previous(); + } + + cursor.beginEditBlock(); + for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) + { + const QString lineText = b.text(); + int toRemove = 0; + + if (m_settings->useSpacesForTabs()) + { + for (int i = 0; i < tabSize && i < lineText.length(); ++i) + { + if (lineText[i] == ' ') { ++toRemove; } + else { break; } + } + } + else + { + if (!lineText.isEmpty() && lineText[0] == '\t') + { + toRemove = 1; + } + } + + if (toRemove > 0) + { + QTextCursor lineCursor(b); + lineCursor.movePosition(QTextCursor::StartOfBlock); + lineCursor.movePosition(QTextCursor::Right, + QTextCursor::KeepAnchor, + toRemove); + lineCursor.removeSelectedText(); + } + } + cursor.endEditBlock(); + return; + } + + // ----------------------------------------------------------------------- + // Tab: Einrücken (Leerzeichen oder echter Tab) + // ----------------------------------------------------------------------- + if (event->key() == Qt::Key_Tab) + { + QTextCursor cursor = textCursor(); + + if (cursor.hasSelection()) + { + QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); + QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); + + // Gleiche Korrektur: Cursor am Zeilenanfang → Zeile nicht einrücken + if (cursor.selectionEnd() == endBlock.position() && + endBlock != startBlock) + { + endBlock = endBlock.previous(); + } + + cursor.beginEditBlock(); + for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) + { + QTextCursor lineCursor(b); + lineCursor.movePosition(QTextCursor::StartOfBlock); + if (m_settings->useSpacesForTabs()) + { + lineCursor.insertText(QString(tabSize, ' ')); + } + else + { + lineCursor.insertText("\t"); + } + } + cursor.endEditBlock(); + } + else + { + if (m_settings->useSpacesForTabs()) + { + // Zum nächsten Tab-Stop auffüllen + const int col = cursor.columnNumber(); + const int spacesNeeded = tabSize - (col % tabSize); + cursor.insertText(QString(spacesNeeded, ' ')); + } + else + { + cursor.insertText("\t"); + } + } + return; + } + + // ----------------------------------------------------------------------- + // Smart Backspace: springt zur vorherigen Einrückungsstufe + // ----------------------------------------------------------------------- + if (event->key() == Qt::Key_Backspace + && !textCursor().hasSelection() + && m_settings->useSpacesForTabs()) + { + QTextCursor cursor = textCursor(); + const int col = cursor.columnNumber(); + + if (col > 0) + { + // Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen + const QString lineText = cursor.block().text(); + const QString leftOfCursor = lineText.left(col); + const bool onlySpaces = leftOfCursor.trimmed().isEmpty(); + + if (onlySpaces && col > 0) + { + // Zur vorherigen Tab-Stop-Position springen + const int targetCol = ((col - 1) / tabSize) * tabSize; + const int toDelete = col - targetCol; + + cursor.movePosition(QTextCursor::Left, + QTextCursor::KeepAnchor, + toDelete); + cursor.removeSelectedText(); + return; + } + } + } + + // ----------------------------------------------------------------------- + // Enter / Return: Auto-Indent + // ----------------------------------------------------------------------- + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + { + QTextCursor cursor = textCursor(); + const QString currentLine = cursor.block().text(); + + // Führende Leerzeichen der aktuellen Zeile zählen + int leadingSpaces = 0; + for (const QChar &ch : currentLine) + { + if (ch == ' ') + { + ++leadingSpaces; + } + else if (ch == '\t') + { + leadingSpaces += tabSize; + } + else + { + break; + } + } + + QPlainTextEdit::keyPressEvent(event); + + if (leadingSpaces > 0) + { + const QString indent = m_settings->useSpacesForTabs() + ? QString(leadingSpaces, ' ') + : QString(leadingSpaces / tabSize, '\t'); + textCursor().insertText(indent); + } + return; + } + + QPlainTextEdit::keyPressEvent(event); + + // Variablen-Popup nach jedem Tastendruck aktualisieren + m_varCompleter->handleKeyPress(event); +} + +// --------------------------------------------------------------------------- +// Fokus verloren — z. B. beim Wechsel zu einem anderen Tab. +// Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst +// bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen +// Dateien den Fokus blockieren. +// --------------------------------------------------------------------------- +void CodeEditor::focusOutEvent(QFocusEvent *event) +{ + QPlainTextEdit::focusOutEvent(event); + m_varCompleter->notifyFocusLost(); +} + diff --git a/barecode/src/editor/CodeEditor.h b/barecode/src/editor/CodeEditor.h new file mode 100644 index 0000000..af87818 --- /dev/null +++ b/barecode/src/editor/CodeEditor.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include + +class LineNumberArea; +class Settings; +class SyntaxHighlighter; +class ColorIndicator; +class SignatureHelper; +class FunctionIndex; +class VariableCompleter; + +// --------------------------------------------------------------------------- +// CodeEditor – Core editing widget. +// Features: +// • Line number gutter +// • Current-line highlight +// • Auto-indent on Enter +// • Tab → spaces (configurable) +// • Syntax highlighting (via pluggable SyntaxHighlighter) +// --------------------------------------------------------------------------- +class CodeEditor : public QPlainTextEdit +{ + Q_OBJECT + +public: + explicit CodeEditor(Settings *settings, QWidget *parent = nullptr); + ~CodeEditor() override; + + void loadFile(const QString &filePath); + void applySettings(); + + // Speichern + bool save(); + bool saveAs(); + + // Called by LineNumberArea + int lineNumberAreaWidth() const; + void lineNumberAreaPaintEvent(QPaintEvent *event); + + QString filePath() const; + bool isModified() const; + + // Öffentliche Hilfsmethoden für ColorIndicator + // (die Qt-Originale sind protected und von außen nicht erreichbar) + QTextBlock firstVisibleBlockPublic() const { return firstVisibleBlock(); } + QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); } + QPointF contentOffsetPublic() const { return contentOffset(); } + + void setFunctionIndex(FunctionIndex *index); + +signals: + void fileSaved(const QString &filePath); + void navigateToRequested(const QString &filePath, int line); + +protected: + void resizeEvent(QResizeEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void focusOutEvent(QFocusEvent *event) override; + +private slots: + void updateLineNumberAreaWidth(int newBlockCount); + void highlightCurrentLine(); + void updateLineNumberArea(const QRect &rect, int dy); + +private: + void setupEditor(); + void installHighlighter(const QString &filePath); + bool writeToFile(const QString &filePath); + void matchBrackets(QList &selections); + + Settings *m_settings = nullptr; + LineNumberArea *m_lineNumberArea = nullptr; + SyntaxHighlighter *m_highlighter = nullptr; + ColorIndicator *m_colorIndicator = nullptr; + SignatureHelper *m_signatureHelper = nullptr; + FunctionIndex *m_functionIndex = nullptr; + VariableCompleter *m_varCompleter = nullptr; + QString m_filePath; +}; diff --git a/barecode/src/editor/ColorIndicator.cpp b/barecode/src/editor/ColorIndicator.cpp new file mode 100644 index 0000000..60f4e04 --- /dev/null +++ b/barecode/src/editor/ColorIndicator.cpp @@ -0,0 +1,361 @@ +#include "ColorIndicator.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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 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(blockRect.left() + endCharX) + - scrollX + 3; + + if (x + squareSize > m_editor->viewport()->width()) + { + continue; + } + + const int y = static_cast(blockRect.top()) + + (static_cast(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::findColorsInBlock( + const QString &text, int blockNumber) const +{ + QList 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(match.capturedStart()); + m.length = static_cast(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; +} + + + diff --git a/barecode/src/editor/ColorIndicator.h b/barecode/src/editor/ColorIndicator.h new file mode 100644 index 0000000..2d19ede --- /dev/null +++ b/barecode/src/editor/ColorIndicator.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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 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 m_cache; + int m_cacheFirstBlock = -1; + int m_cacheLastBlock = -1; + + // Kombinierter Regex für alle Farbformate + static const QRegularExpression s_colorRegex; +}; diff --git a/barecode/src/editor/DeadCodeAnalyzer.cpp b/barecode/src/editor/DeadCodeAnalyzer.cpp new file mode 100644 index 0000000..3a930d6 --- /dev/null +++ b/barecode/src/editor/DeadCodeAnalyzer.cpp @@ -0,0 +1,181 @@ +#include "DeadCodeAnalyzer.h" + +#include +#include +#include +#include +#include +#include +#include + +DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent) + : QObject(parent) +{ + m_scanner = new FunctionScanner(this); +} + +QList 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 definitions; + + for (const QString &path : allFiles) + { + const QList 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 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 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 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; +} diff --git a/barecode/src/editor/DeadCodeAnalyzer.h b/barecode/src/editor/DeadCodeAnalyzer.h new file mode 100644 index 0000000..0431fde --- /dev/null +++ b/barecode/src/editor/DeadCodeAnalyzer.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#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 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 ¤tFile) const; + +private: + FunctionScanner *m_scanner = nullptr; +}; diff --git a/barecode/src/editor/DeadCodeDialog.cpp b/barecode/src/editor/DeadCodeDialog.cpp new file mode 100644 index 0000000..9669c85 --- /dev/null +++ b/barecode/src/editor/DeadCodeDialog.cpp @@ -0,0 +1,461 @@ +#include "DeadCodeDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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>(this); + + connect(m_watcher, &QFutureWatcher>::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> 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 ¤tFile) +{ + 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 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(); +} diff --git a/barecode/src/editor/DeadCodeDialog.h b/barecode/src/editor/DeadCodeDialog.h new file mode 100644 index 0000000..f7a4028 --- /dev/null +++ b/barecode/src/editor/DeadCodeDialog.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ¤tFile); + 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> *m_watcher = nullptr; +}; diff --git a/barecode/src/editor/EditorPanel.cpp b/barecode/src/editor/EditorPanel.cpp new file mode 100644 index 0000000..92766b3 --- /dev/null +++ b/barecode/src/editor/EditorPanel.cpp @@ -0,0 +1,301 @@ +#include "EditorPanel.h" +#include "EditorTab.h" +#include "CodeEditor.h" +#include "SearchPanel.h" +#include "FileSearchPanel.h" +#include "FunctionListDialog.h" +#include "DeadCodeDialog.h" +#include "FunctionIndex.h" + +#include +#include +#include +#include + +EditorPanel::EditorPanel(Settings *settings, QWidget *parent) + : QWidget(parent) + , m_settings(settings) +{ + setupUi(); +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void EditorPanel::setupUi() +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + m_tabWidget = new QTabWidget(this); + m_tabWidget->setTabsClosable(true); + m_tabWidget->setMovable(true); + m_tabWidget->setDocumentMode(true); + + m_searchPanel = new SearchPanel(this); + m_fileSearch = new FileSearchPanel(this); + m_funcDialog = new FunctionListDialog(window()); + m_deadCode = new DeadCodeDialog(window()); + m_funcIndex = new FunctionIndex(this); + + m_layout->addWidget(m_tabWidget, 1); + m_layout->addWidget(m_searchPanel, 0); + m_layout->addWidget(m_fileSearch, 0); + + connect(m_tabWidget, &QTabWidget::tabCloseRequested, + this, &EditorPanel::onTabCloseRequested); + + connect(m_tabWidget, &QTabWidget::currentChanged, + this, &EditorPanel::onCurrentTabChanged); + + connect(m_fileSearch, &FileSearchPanel::fileLineRequested, + this, &EditorPanel::goToLine); + + connect(m_funcDialog, &FunctionListDialog::fileLineRequested, + this, &EditorPanel::goToLine); + + connect(m_deadCode, &DeadCodeDialog::fileLineRequested, + this, &EditorPanel::goToLine); +} + +// --------------------------------------------------------------------------- +// Hilfsmethoden +// --------------------------------------------------------------------------- +EditorTab *EditorPanel::currentTab() const +{ + return qobject_cast(m_tabWidget->currentWidget()); +} + +int EditorPanel::findTabForFile(const QString &filePath) const +{ + EditorTab *tab = m_openTabs.value(filePath, nullptr); + return tab ? m_tabWidget->indexOf(tab) : -1; +} + +// --------------------------------------------------------------------------- +// Öffentliche Slots +// --------------------------------------------------------------------------- +void EditorPanel::openFile(const QString &filePath) +{ + const int existing = findTabForFile(filePath); + if (existing != -1) + { + m_tabWidget->setCurrentIndex(existing); + return; + } + + EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget); + const int index = m_tabWidget->addTab(tab, tab->fileName()); + m_tabWidget->setCurrentIndex(index); + m_tabWidget->setTabToolTip(index, filePath); + m_openTabs.insert(filePath, tab); + + // FunctionIndex dem Editor mitgeben für Doppelklick-Navigation + tab->editor()->setFunctionIndex(m_funcIndex); + + // Doppelklick auf Funktion → zur Definition springen + connect(tab->editor(), &CodeEditor::navigateToRequested, + this, &EditorPanel::goToLine); + + // Änderungsindikator im Tab-Titel (● = ungespeichert) + connect(tab->editor()->document(), &QTextDocument::modificationChanged, + this, [this, tab](bool modified) + { + const int idx = m_tabWidget->indexOf(tab); + if (idx == -1) + { + return; + } + const QString name = QFileInfo(tab->filePath()).fileName(); + m_tabWidget->setTabText(idx, modified ? "● " + name : name); + }); + + // Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt) + connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath) + { + const int idx = m_tabWidget->indexOf(tab); + if (idx != -1) + { + m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName()); + m_tabWidget->setTabToolTip(idx, savedPath); + } + emit currentFileSaved(savedPath); + + // Index und Funktionsliste aktualisieren + m_funcIndex->refresh(); + if (m_funcDialog->isVisible()) + { + m_funcDialog->refresh(); + } + }); +} + +void EditorPanel::saveCurrentFile() +{ + if (EditorTab *tab = currentTab()) + { + tab->save(); + } +} + +void EditorPanel::saveCurrentFileAs() +{ + if (EditorTab *tab = currentTab()) + { + tab->saveAs(); + } +} + +void EditorPanel::saveAllFiles() +{ + for (int i = 0; i < m_tabWidget->count(); ++i) + { + EditorTab *tab = qobject_cast(m_tabWidget->widget(i)); + if (tab && tab->isModified()) + { + tab->save(); + } + } +} + +void EditorPanel::showSearchPanel() +{ + m_fileSearch->hide(); + m_searchPanel->activate(); +} + +void EditorPanel::showFileSearchPanel() +{ + m_searchPanel->hide(); + m_fileSearch->activate(); +} + +void EditorPanel::showFunctionList() +{ + m_funcDialog->show(); + m_funcDialog->raise(); + m_funcDialog->activateWindow(); +} + +void EditorPanel::showDeadCode() +{ + m_deadCode->show(); + m_deadCode->raise(); + m_deadCode->activateWindow(); +} + +void EditorPanel::setSearchRoot(const QString &path) +{ + m_fileSearch->setSearchRoot(path); + m_funcDialog->setProjectRoot(path); + m_deadCode->setProjectRoot(path); + m_funcIndex->setProjectRoot(path); +} + +void EditorPanel::goToLine(const QString &filePath, int line) +{ + // Datei öffnen falls noch nicht geöffnet + openFile(filePath); + + EditorTab *tab = m_openTabs.value(filePath, nullptr); + if (!tab) + { + return; + } + + m_tabWidget->setCurrentWidget(tab); + + // Zur gewünschten Zeile springen + CodeEditor *editor = tab->editor(); + QTextBlock block = editor->document()->findBlockByLineNumber(line - 1); + if (block.isValid()) + { + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::StartOfBlock); + editor->setTextCursor(cursor); + editor->centerCursor(); + editor->setFocus(); + } +} + +QStringList EditorPanel::openFilePaths() const +{ + QStringList paths; + for (int i = 0; i < m_tabWidget->count(); ++i) + { + EditorTab *tab = qobject_cast(m_tabWidget->widget(i)); + if (tab) + { + paths.append(tab->filePath()); + } + } + return paths; +} + +QString EditorPanel::activeFilePath() const +{ + EditorTab *tab = currentTab(); + return tab ? tab->filePath() : QString(); +} + +void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile) +{ + for (const QString &path : files) + { + if (QFile::exists(path)) + { + openFile(path); + } + } + + // Aktiven Tab wiederherstellen + if (!activeFile.isEmpty()) + { + const int idx = findTabForFile(activeFile); + if (idx != -1) + { + m_tabWidget->setCurrentIndex(idx); + } + } +} + +void EditorPanel::undo() +{ + if (EditorTab *tab = currentTab()) + { + tab->editor()->undo(); + } +} + +void EditorPanel::redo() +{ + if (EditorTab *tab = currentTab()) + { + tab->editor()->redo(); + } +} + +// --------------------------------------------------------------------------- +// Private Slots +// --------------------------------------------------------------------------- +void EditorPanel::onTabCloseRequested(int index) +{ + EditorTab *tab = qobject_cast(m_tabWidget->widget(index)); + if (!tab) + { + return; + } + + m_openTabs.remove(tab->filePath()); + m_tabWidget->removeTab(index); + tab->deleteLater(); + + m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr); +} + +void EditorPanel::onCurrentTabChanged(int /*index*/) +{ + EditorTab *tab = currentTab(); + m_searchPanel->setEditor(tab ? tab->editor() : nullptr); +} diff --git a/barecode/src/editor/EditorPanel.h b/barecode/src/editor/EditorPanel.h new file mode 100644 index 0000000..e90c518 --- /dev/null +++ b/barecode/src/editor/EditorPanel.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +class EditorTab; +class Settings; +class SearchPanel; +class FileSearchPanel; +class FunctionListDialog; +class DeadCodeDialog; +class FunctionIndex; + +// --------------------------------------------------------------------------- +// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel. +// --------------------------------------------------------------------------- +class EditorPanel : public QWidget +{ + Q_OBJECT + +public: + explicit EditorPanel(Settings *settings, QWidget *parent = nullptr); + +public slots: + void openFile(const QString &filePath); + void saveCurrentFile(); + void saveCurrentFileAs(); + void saveAllFiles(); + void setSearchRoot(const QString &path); + void showSearchPanel(); + void showFileSearchPanel(); + void showFunctionList(); + void showDeadCode(); + void goToLine(const QString &filePath, int line); + + // Session + QStringList openFilePaths() const; + QString activeFilePath() const; + void restoreSession(const QStringList &files, const QString &activeFile); + void undo(); + void redo(); + +signals: + void currentFileSaved(const QString &filePath); + +private slots: + void onTabCloseRequested(int index); + void onCurrentTabChanged(int index); + +private: + void setupUi(); + int findTabForFile(const QString &filePath) const; + EditorTab *currentTab() const; + + Settings *m_settings = nullptr; + QVBoxLayout *m_layout = nullptr; + QTabWidget *m_tabWidget = nullptr; + SearchPanel *m_searchPanel = nullptr; + FileSearchPanel *m_fileSearch = nullptr; + FunctionListDialog *m_funcDialog = nullptr; + DeadCodeDialog *m_deadCode = nullptr; + FunctionIndex *m_funcIndex = nullptr; + + QHash m_openTabs; +}; diff --git a/barecode/src/editor/EditorTab.cpp b/barecode/src/editor/EditorTab.cpp new file mode 100644 index 0000000..0b0b8fa --- /dev/null +++ b/barecode/src/editor/EditorTab.cpp @@ -0,0 +1,53 @@ +#include "EditorTab.h" +#include "CodeEditor.h" + +#include + +EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent) + : QWidget(parent) + , m_filePath(filePath) +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + m_editor = new CodeEditor(settings, this); + m_editor->loadFile(filePath); + + m_layout->addWidget(m_editor); +} + +QString EditorTab::filePath() const +{ + return m_filePath; +} + +QString EditorTab::fileName() const +{ + return QFileInfo(m_filePath).fileName(); +} + +CodeEditor *EditorTab::editor() const +{ + return m_editor; +} + +bool EditorTab::isModified() const +{ + return m_editor->isModified(); +} + +bool EditorTab::save() +{ + const bool ok = m_editor->save(); + // Path may have changed if this was an untitled buffer saved for the first time + m_filePath = m_editor->filePath(); + return ok; +} + +bool EditorTab::saveAs() +{ + const bool ok = m_editor->saveAs(); + m_filePath = m_editor->filePath(); + return ok; +} diff --git a/barecode/src/editor/EditorTab.h b/barecode/src/editor/EditorTab.h new file mode 100644 index 0000000..3bf0d8f --- /dev/null +++ b/barecode/src/editor/EditorTab.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +class CodeEditor; +class Settings; + +// --------------------------------------------------------------------------- +// EditorTab – Widget placed inside each tab of the tab bar. +// Owns a CodeEditor for a single file. +// --------------------------------------------------------------------------- +class EditorTab : public QWidget +{ + Q_OBJECT + +public: + explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr); + + QString filePath() const; + QString fileName() const; + CodeEditor *editor() const; + bool isModified() const; + + bool save(); + bool saveAs(); + +private: + QString m_filePath; + QVBoxLayout *m_layout = nullptr; + CodeEditor *m_editor = nullptr; +}; diff --git a/barecode/src/editor/FileSearchPanel.cpp b/barecode/src/editor/FileSearchPanel.cpp new file mode 100644 index 0000000..f7d0030 --- /dev/null +++ b/barecode/src/editor/FileSearchPanel.cpp @@ -0,0 +1,330 @@ +#include "FileSearchPanel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Konstruktor +// --------------------------------------------------------------------------- +FileSearchPanel::FileSearchPanel(QWidget *parent) + : QWidget(parent) +{ + setupUi(); + hide(); + + m_watcher = new QFutureWatcher>(this); + connect(m_watcher, &QFutureWatcher>::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> 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 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::searchInFiles( + const QString &root, + const QString &needle, + bool caseSensitive, + bool wholeWord, + bool useRegex, + const QStringList &extensions) const +{ + QList 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; +} diff --git a/barecode/src/editor/FileSearchPanel.h b/barecode/src/editor/FileSearchPanel.h new file mode 100644 index 0000000..97b9766 --- /dev/null +++ b/barecode/src/editor/FileSearchPanel.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// FileSearchPanel – Suche in allen Dateien eines Verzeichnisses. +// +// Ergebnisse werden als aufklappbare Liste angezeigt: +// Dateiname (N Treffer) +// └ Zeile 12: +// └ Zeile 34: +// +// 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 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> *m_watcher = nullptr; +}; diff --git a/barecode/src/editor/FunctionIndex.cpp b/barecode/src/editor/FunctionIndex.cpp new file mode 100644 index 0000000..a2ec7ce --- /dev/null +++ b/barecode/src/editor/FunctionIndex.cpp @@ -0,0 +1,83 @@ +#include "FunctionIndex.h" + +#include + +FunctionIndex::FunctionIndex(QObject *parent) + : QObject(parent) +{ + m_scanner = new FunctionScanner(this); + m_watcher = new QFutureWatcher>(this); + + connect(m_watcher, &QFutureWatcher>::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 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; +} diff --git a/barecode/src/editor/FunctionIndex.h b/barecode/src/editor/FunctionIndex.h new file mode 100644 index 0000000..2446d14 --- /dev/null +++ b/barecode/src/editor/FunctionIndex.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#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> *m_watcher = nullptr; + + // name.toLower() → FunctionInfo + QHash m_index; + bool m_ready = false; +}; diff --git a/barecode/src/editor/FunctionListDialog.cpp b/barecode/src/editor/FunctionListDialog.cpp new file mode 100644 index 0000000..ab7c56b --- /dev/null +++ b/barecode/src/editor/FunctionListDialog.cpp @@ -0,0 +1,52 @@ +#include "FunctionListDialog.h" +#include "FunctionListPanel.h" + +#include +#include +#include + +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(); +} diff --git a/barecode/src/editor/FunctionListDialog.h b/barecode/src/editor/FunctionListDialog.h new file mode 100644 index 0000000..9310f60 --- /dev/null +++ b/barecode/src/editor/FunctionListDialog.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +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; +}; diff --git a/barecode/src/editor/FunctionListPanel.cpp b/barecode/src/editor/FunctionListPanel.cpp new file mode 100644 index 0000000..c6441b3 --- /dev/null +++ b/barecode/src/editor/FunctionListPanel.cpp @@ -0,0 +1,332 @@ +#include "FunctionListPanel.h" + +#include +#include +#include +#include + +FunctionListPanel::FunctionListPanel(QWidget *parent) + : QWidget(parent) +{ + m_scanner = new FunctionScanner(this); + m_watcher = new QFutureWatcher>(this); + + connect(m_watcher, &QFutureWatcher>::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::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> 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 &functions) +{ + m_tree->clear(); + + const QString groupBy = m_groupCombo->currentData().toString(); + const QString filter = m_filterEdit->text().trimmed().toLower(); + + // Funktionen filtern + QList 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> 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); + } +} diff --git a/barecode/src/editor/FunctionListPanel.h b/barecode/src/editor/FunctionListPanel.h new file mode 100644 index 0000000..fd283be --- /dev/null +++ b/barecode/src/editor/FunctionListPanel.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 &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> *m_watcher = nullptr; + + // Letztes Scan-Ergebnis für Filter ohne Neuscan + QList m_lastResult; +}; diff --git a/barecode/src/editor/FunctionScanner.cpp b/barecode/src/editor/FunctionScanner.cpp new file mode 100644 index 0000000..10a8a02 --- /dev/null +++ b/barecode/src/editor/FunctionScanner.cpp @@ -0,0 +1,158 @@ +#include "FunctionScanner.h" + +#include +#include +#include +#include +#include + +FunctionScanner::FunctionScanner(QObject *parent) + : QObject(parent) +{ +} + +// --------------------------------------------------------------------------- +// Verzeichnis rekursiv scannen +// --------------------------------------------------------------------------- +QList FunctionScanner::scanDirectory( + const QString &rootPath, + const QStringList &extensions) const +{ + QList 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::scanFile(const QString &filePath) const +{ + QList 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(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 +} diff --git a/barecode/src/editor/FunctionScanner.h b/barecode/src/editor/FunctionScanner.h new file mode 100644 index 0000000..cd08243 --- /dev/null +++ b/barecode/src/editor/FunctionScanner.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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 scanDirectory(const QString &rootPath, + const QStringList &extensions = {"*.php"}) const; + + // Scannt eine einzelne Datei + QList scanFile(const QString &filePath) const; + +private: + static QString extractParameters(const QString &line, int parenPos); + static QString detectClassContext(const QStringList &lines, int functionLineIndex); +}; diff --git a/barecode/src/editor/LineNumberArea.cpp b/barecode/src/editor/LineNumberArea.cpp new file mode 100644 index 0000000..44ef836 --- /dev/null +++ b/barecode/src/editor/LineNumberArea.cpp @@ -0,0 +1,18 @@ +#include "LineNumberArea.h" +#include "CodeEditor.h" + +LineNumberArea::LineNumberArea(CodeEditor *editor) + : QWidget(editor) + , m_codeEditor(editor) +{ +} + +QSize LineNumberArea::sizeHint() const +{ + return QSize(m_codeEditor->lineNumberAreaWidth(), 0); +} + +void LineNumberArea::paintEvent(QPaintEvent *event) +{ + m_codeEditor->lineNumberAreaPaintEvent(event); +} diff --git a/barecode/src/editor/LineNumberArea.h b/barecode/src/editor/LineNumberArea.h new file mode 100644 index 0000000..1a6963a --- /dev/null +++ b/barecode/src/editor/LineNumberArea.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// LineNumberArea – Thin widget painted on the left side of the CodeEditor. +// Painted by CodeEditor::lineNumberAreaPaintEvent(). +// --------------------------------------------------------------------------- +class LineNumberArea : public QWidget +{ + Q_OBJECT + +public: + explicit LineNumberArea(CodeEditor *editor); + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + CodeEditor *m_codeEditor; +}; diff --git a/barecode/src/editor/SearchPanel.cpp b/barecode/src/editor/SearchPanel.cpp new file mode 100644 index 0000000..39704f6 --- /dev/null +++ b/barecode/src/editor/SearchPanel.cpp @@ -0,0 +1,492 @@ +#include "SearchPanel.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include + +SearchPanel::SearchPanel(QWidget *parent) + : QWidget(parent) +{ + setupUi(); + hide(); +} + +// --------------------------------------------------------------------------- +// UI +// --------------------------------------------------------------------------- +void SearchPanel::setupUi() +{ + m_grid = new QGridLayout(this); + m_grid->setContentsMargins(6, 4, 6, 4); + m_grid->setSpacing(4); + + // ---- Row 0: Suchen ---- + m_searchEdit = new QLineEdit(this); + m_searchEdit->setPlaceholderText(tr("Suchen…")); + m_searchEdit->setClearButtonEnabled(true); + + m_btnPrev = new QPushButton(tr("▲"), this); + m_btnNext = new QPushButton(tr("▼"), this); + m_btnPrev->setFixedWidth(28); + m_btnNext->setFixedWidth(28); + m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)")); + m_btnNext->setToolTip(tr("Nächster Treffer (F3)")); + + m_matchLabel = new QLabel(this); + m_matchLabel->setMinimumWidth(80); + + m_btnClose = new QPushButton(tr("✕"), this); + m_btnClose->setFixedWidth(24); + m_btnClose->setToolTip(tr("Schließen (Esc)")); + m_btnClose->setFlat(true); + + QHBoxLayout *searchRow = new QHBoxLayout(); + searchRow->addWidget(new QLabel(tr("Suchen:"), this)); + searchRow->addWidget(m_searchEdit, 1); + searchRow->addWidget(m_btnPrev); + searchRow->addWidget(m_btnNext); + searchRow->addWidget(m_matchLabel); + searchRow->addWidget(m_btnClose); + m_grid->addLayout(searchRow, 0, 0); + + // ---- Row 1: Ersetzen ---- + m_replaceEdit = new QLineEdit(this); + m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…")); + m_replaceEdit->setClearButtonEnabled(true); + + m_btnReplace = new QPushButton(tr("Ersetzen"), this); + m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this); + m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this); + + QHBoxLayout *replaceRow = new QHBoxLayout(); + replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this)); + replaceRow->addWidget(m_replaceEdit, 1); + replaceRow->addWidget(m_btnReplace); + replaceRow->addWidget(m_btnReplaceAll); + replaceRow->addWidget(m_btnReplaceSelection); + m_grid->addLayout(replaceRow, 1, 0); + + // ---- Row 2: Optionen ---- + m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this); + m_chkWord = new QCheckBox(tr("Ganzes Wort"), this); + m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this); + + QHBoxLayout *optRow = new QHBoxLayout(); + optRow->addWidget(m_chkCase); + optRow->addWidget(m_chkWord); + optRow->addWidget(m_chkRegex); + optRow->addStretch(); + m_grid->addLayout(optRow, 2, 0); + + // ---- Connections ---- + connect(m_searchEdit, &QLineEdit::textChanged, + this, &SearchPanel::onSearchTextChanged); + + connect(m_searchEdit, &QLineEdit::returnPressed, + this, &SearchPanel::findNext); + + connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext); + connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious); + + connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent); + connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll); + connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection); + + connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked); + + connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); + connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); + connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- +void SearchPanel::setEditor(CodeEditor *editor) +{ + clearHighlights(); + m_editor = editor; +} + +void SearchPanel::activate() +{ + show(); + m_searchEdit->setFocus(); + m_searchEdit->selectAll(); + + // Pre-fill with selected text if short enough + if (m_editor) + { + const QString sel = m_editor->textCursor().selectedText(); + if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200) + { + m_searchEdit->setText(sel); + } + } + + updateMatchLabel(); +} + +// --------------------------------------------------------------------------- +// Find helpers +// --------------------------------------------------------------------------- +QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const +{ + QTextDocument::FindFlags flags; + if (backwards) { flags |= QTextDocument::FindBackward; } + if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; } + if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; } + return flags; +} + +bool SearchPanel::performFind(bool backwards) +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return false; + } + + const QTextDocument::FindFlags flags = buildFindFlags(backwards); + bool found = false; + + if (m_chkRegex->isChecked()) + { + QRegularExpression re(m_searchEdit->text()); + if (m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::NoPatternOption); + } + else + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + found = m_editor->find(re, flags); + + // Wrap around + if (!found) + { + QTextCursor c = m_editor->textCursor(); + c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start); + m_editor->setTextCursor(c); + found = m_editor->find(re, flags); + } + } + else + { + found = m_editor->find(m_searchEdit->text(), flags); + + // Wrap around + if (!found) + { + QTextCursor c = m_editor->textCursor(); + c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start); + m_editor->setTextCursor(c); + found = m_editor->find(m_searchEdit->text(), flags); + } + } + + return found; +} + +void SearchPanel::highlightAllMatches() +{ + if (!m_editor) + { + return; + } + + QList extras; + + const QString needle = m_searchEdit->text(); + if (needle.isEmpty()) + { + m_editor->setExtraSelections(extras); + return; + } + + QTextCharFormat fmt; + fmt.setBackground(QColor("#3a3a00")); + fmt.setForeground(QColor("#ffff80")); + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + + const QTextDocument::FindFlags flags = buildFindFlags(false); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + QTextEdit::ExtraSelection sel; + sel.cursor = cursor; + sel.format = fmt; + extras.append(sel); + } + + m_editor->setExtraSelections(extras); +} + +void SearchPanel::clearHighlights() +{ + if (m_editor) + { + m_editor->setExtraSelections({}); + } +} + +void SearchPanel::updateMatchLabel() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + m_matchLabel->setText(QString()); + return; + } + + // Count total matches + int count = 0; + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + ++count; + } + + if (count == 0) + { + m_matchLabel->setText(tr("Kein Treffer")); + m_matchLabel->setStyleSheet("color: #cc4444;"); + } + else + { + m_matchLabel->setText(tr("%1 Treffer").arg(count)); + m_matchLabel->setStyleSheet(QString()); + } +} + +// --------------------------------------------------------------------------- +// Public slots +// --------------------------------------------------------------------------- +void SearchPanel::findNext() +{ + performFind(false); +} + +void SearchPanel::findPrevious() +{ + performFind(true); +} + +void SearchPanel::replaceCurrent() +{ + if (!m_editor) + { + return; + } + + QTextCursor cursor = m_editor->textCursor(); + + // If current selection matches the search term, replace it + // Otherwise just find the next occurrence first + const bool hasMatch = !cursor.selectedText().isEmpty(); + if (!hasMatch) + { + performFind(false); + return; + } + + cursor.insertText(m_replaceEdit->text()); + + // Move to next match + performFind(false); + updateMatchLabel(); + highlightAllMatches(); +} + +void SearchPanel::replaceAll() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return; + } + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + cursor.beginEditBlock(); + + int count = 0; + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + const QString replacement = m_replaceEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + cursor.insertText(replacement); + ++count; + } + + cursor.endEditBlock(); + + updateMatchLabel(); + clearHighlights(); + + QMessageBox::information(this, tr("Alle ersetzen"), + tr("%1 Ersetzung(en) durchgeführt.").arg(count)); +} + +void SearchPanel::replaceInSelection() +{ + if (!m_editor || m_searchEdit->text().isEmpty()) + { + return; + } + + QTextCursor selCursor = m_editor->textCursor(); + if (!selCursor.hasSelection()) + { + QMessageBox::information(this, tr("In Auswahl ersetzen"), + tr("Es ist kein Text ausgewählt.")); + return; + } + + // Work only within the selected region + const int selStart = selCursor.selectionStart(); + const int selEnd = selCursor.selectionEnd(); + + QTextDocument *doc = m_editor->document(); + QTextCursor cursor(doc); + cursor.setPosition(selStart); + cursor.beginEditBlock(); + + int count = 0; + int offset = 0; // Replacement may be longer/shorter than search term + const QTextDocument::FindFlags flags = buildFindFlags(false); + const QString needle = m_searchEdit->text(); + const QString replacement = m_replaceEdit->text(); + + while (true) + { + if (m_chkRegex->isChecked()) + { + QRegularExpression re(needle); + if (!m_chkCase->isChecked()) + { + re.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + cursor = doc->find(re, cursor, flags); + } + else + { + cursor = doc->find(needle, cursor, flags); + } + + if (cursor.isNull()) + { + break; + } + + // Stop if we've left the original selection + if (cursor.selectionEnd() > selEnd + offset) + { + break; + } + + offset += replacement.length() - cursor.selectedText().length(); + cursor.insertText(replacement); + ++count; + } + + cursor.endEditBlock(); + + updateMatchLabel(); + clearHighlights(); + + QMessageBox::information(this, tr("In Auswahl ersetzen"), + tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count)); +} + +// --------------------------------------------------------------------------- +// Private slots +// --------------------------------------------------------------------------- +void SearchPanel::onSearchTextChanged(const QString &/*text*/) +{ + highlightAllMatches(); + updateMatchLabel(); +} + +void SearchPanel::onOptionChanged() +{ + highlightAllMatches(); + updateMatchLabel(); +} + +void SearchPanel::onCloseClicked() +{ + clearHighlights(); + m_matchLabel->setText(QString()); + hide(); + if (m_editor) + { + m_editor->setFocus(); + } +} diff --git a/barecode/src/editor/SearchPanel.h b/barecode/src/editor/SearchPanel.h new file mode 100644 index 0000000..b3e1158 --- /dev/null +++ b/barecode/src/editor/SearchPanel.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// SearchPanel – Collapsible find/replace bar that operates on a CodeEditor. +// +// Capabilities: +// • Nächsten / Vorherigen Treffer suchen +// • Einzeln ersetzen +// • Alle ersetzen +// • Nur in Auswahl ersetzen +// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke +// --------------------------------------------------------------------------- +class SearchPanel : public QWidget +{ + Q_OBJECT + +public: + explicit SearchPanel(QWidget *parent = nullptr); + + // Must be called whenever the active editor changes + void setEditor(CodeEditor *editor); + + // Toggle visibility and focus the search field + void activate(); + +public slots: + void findNext(); + void findPrevious(); + void replaceCurrent(); + void replaceAll(); + void replaceInSelection(); + +private slots: + void onSearchTextChanged(const QString &text); + void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert) + void onCloseClicked(); + +private: + void setupUi(); + + QTextDocument::FindFlags buildFindFlags(bool backwards = false) const; + bool performFind(bool backwards = false); + void highlightAllMatches(); + void clearHighlights(); + void updateMatchLabel(); + + CodeEditor *m_editor = nullptr; + + // Search row + QLineEdit *m_searchEdit = nullptr; + QPushButton *m_btnPrev = nullptr; + QPushButton *m_btnNext = nullptr; + QLabel *m_matchLabel = nullptr; + QPushButton *m_btnClose = nullptr; + + // Replace row + QLineEdit *m_replaceEdit = nullptr; + QPushButton *m_btnReplace = nullptr; + QPushButton *m_btnReplaceAll = nullptr; + QPushButton *m_btnReplaceSelection = nullptr; + + // Options row + QCheckBox *m_chkCase = nullptr; + QCheckBox *m_chkWord = nullptr; + QCheckBox *m_chkRegex = nullptr; + + QGridLayout *m_grid = nullptr; +}; diff --git a/barecode/src/editor/SignatureHelper.cpp b/barecode/src/editor/SignatureHelper.cpp new file mode 100644 index 0000000..c84f723 --- /dev/null +++ b/barecode/src/editor/SignatureHelper.cpp @@ -0,0 +1,222 @@ +#include "SignatureHelper.h" +#include "SignatureTooltip.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/barecode/src/editor/SignatureHelper.h b/barecode/src/editor/SignatureHelper.h new file mode 100644 index 0000000..9b1ec89 --- /dev/null +++ b/barecode/src/editor/SignatureHelper.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +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 m_functions; // name → info +}; diff --git a/barecode/src/editor/SignatureTooltip.cpp b/barecode/src/editor/SignatureTooltip.cpp new file mode 100644 index 0000000..3ac4099 --- /dev/null +++ b/barecode/src/editor/SignatureTooltip.cpp @@ -0,0 +1,91 @@ +#include "SignatureTooltip.h" + +#include +#include + +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(); +} diff --git a/barecode/src/editor/SignatureTooltip.h b/barecode/src/editor/SignatureTooltip.h new file mode 100644 index 0000000..110062f --- /dev/null +++ b/barecode/src/editor/SignatureTooltip.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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; +}; diff --git a/barecode/src/editor/VariableCompleter.cpp b/barecode/src/editor/VariableCompleter.cpp new file mode 100644 index 0000000..64d57af --- /dev/null +++ b/barecode/src/editor/VariableCompleter.cpp @@ -0,0 +1,256 @@ +#include "VariableCompleter.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +VariableCompleter::VariableCompleter(CodeEditor *editor) + : QObject(editor) + , m_editor(editor) +{ + // Popup als reines Anzeige-Fenster — dieselbe Fensterart wie + // SignatureTooltip, die nachweislich nie den Fokus an sich reißt. + // Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst + // behandelt der Fenstermanager das Popup als eigenständiges Fenster. + // Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste + // Popups entstehen konnten, die den Fokus übernehmen und die + // Texteingabe blockieren. + m_popup = new QListWidget(editor->window()); + m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint); + m_popup->setAttribute(Qt::WA_ShowWithoutActivating); + m_popup->setFocusPolicy(Qt::NoFocus); + m_popup->setMaximumHeight(200); + m_popup->setMinimumWidth(180); + m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_popup->setStyleSheet( + "QListWidget {" + " border: 1px solid palette(mid);" + " background: palette(toolTipBase);" + " color: palette(toolTipText);" + " font-family: Monospace;" + "}" + "QListWidget::item:selected {" + " background: palette(highlight);" + " color: palette(highlightedText);" + "}" + ); + + connect(m_popup, &QListWidget::itemActivated, + this, &VariableCompleter::onItemActivated); +} + +// --------------------------------------------------------------------------- +// Nach jedem Tastendruck aufrufen +// --------------------------------------------------------------------------- +void VariableCompleter::handleKeyPress(QKeyEvent *event) +{ + // Popup-Navigation + if (m_popup->isVisible()) + { + if (event->key() == Qt::Key_Escape) + { + hidePopup(); + return; + } + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + { + if (m_popup->currentItem()) + { + onItemActivated(m_popup->currentItem()); + } + hidePopup(); + return; + } + if (event->key() == Qt::Key_Down) + { + const int next = qMin(m_popup->currentRow() + 1, + m_popup->count() - 1); + m_popup->setCurrentRow(next); + return; + } + if (event->key() == Qt::Key_Up) + { + const int prev = qMax(m_popup->currentRow() - 1, 0); + m_popup->setCurrentRow(prev); + return; + } + } + + // Nach dem normalen Tastendruck Popup aktualisieren + // (der Event wurde bereits an QPlainTextEdit weitergegeben) + updatePopup(); +} + +// --------------------------------------------------------------------------- +// Popup aktualisieren +// --------------------------------------------------------------------------- +void VariableCompleter::updatePopup() +{ + const QString prefix = currentPrefix(); + + // Nur bei $ und mindestens einem weiteren Zeichen anzeigen + if (prefix.length() < 2 || !prefix.startsWith('$')) + { + hidePopup(); + return; + } + + const QStringList allVars = collectVariables(); + const QString filter = prefix.toLower(); + + QStringList matches; + for (const QString &var : allVars) + { + if (var.toLower().startsWith(filter) && var != prefix) + { + matches.append(var); + } + } + + if (matches.isEmpty()) + { + hidePopup(); + return; + } + + // Popup befüllen + m_popup->clear(); + for (const QString &var : matches) + { + m_popup->addItem(var); + } + m_popup->setCurrentRow(0); + + // Größe anpassen + const int itemHeight = m_popup->sizeHintForRow(0) + 2; + const int height = qMin(matches.size() * itemHeight + 4, 200); + m_popup->setFixedHeight(height); + + // Position unter dem Cursor + const QRect cursorRect = m_editor->cursorRect(); + QPoint pos = m_editor->viewport()->mapToGlobal( + QPoint(cursorRect.left(), cursorRect.bottom() + 2) + ); + + // Nicht aus dem Bildschirm herausragen + const QRect screen = QApplication::primaryScreen()->availableGeometry(); + if (pos.x() + m_popup->width() > screen.right()) + { + pos.setX(screen.right() - m_popup->width()); + } + if (pos.y() + height > screen.bottom()) + { + pos.setY(cursorRect.top() - height - 2); + } + + m_popup->move(pos); + m_popup->show(); + m_popup->raise(); +} + +void VariableCompleter::hidePopup() +{ + m_popup->hide(); + m_popup->clear(); +} + +// --------------------------------------------------------------------------- +// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen +// --------------------------------------------------------------------------- +void VariableCompleter::notifyFocusLost() +{ + hidePopup(); +} + +// --------------------------------------------------------------------------- +// Variablen im aktuellen Dokument sammeln +// --------------------------------------------------------------------------- +QStringList VariableCompleter::collectVariables() const +{ + static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)"); + + QSet 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(); +} diff --git a/barecode/src/editor/VariableCompleter.h b/barecode/src/editor/VariableCompleter.h new file mode 100644 index 0000000..08743e9 --- /dev/null +++ b/barecode/src/editor/VariableCompleter.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// VariableCompleter – Zeigt ein Popup mit passenden Variablennamen +// wenn der Nutzer $ tippt und weiter eingibt. +// +// Kein Autocomplete — nur Anzeige. Klick oder Enter übernimmt. +// Escape oder Weiterschreiben ohne Treffer schließt das Popup. +// --------------------------------------------------------------------------- +class VariableCompleter : public QObject +{ + Q_OBJECT + +public: + explicit VariableCompleter(CodeEditor *editor); + + // Muss nach jedem Tastendruck aufgerufen werden + void handleKeyPress(QKeyEvent *event); + + // Muss aufgerufen werden, wenn der Editor den Fokus verliert + // (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup, + // damit es nicht als "Geisterfenster" stehen bleibt. + void notifyFocusLost(); + +private slots: + void onItemActivated(QListWidgetItem *item); + +private: + void updatePopup(); + void hidePopup(); + QStringList collectVariables() const; + QString currentPrefix() const; // "$me" etc. links vom Cursor + + CodeEditor *m_editor = nullptr; + QListWidget *m_popup = nullptr; +}; diff --git a/barecode/src/filetree/CMakeLists.txt b/barecode/src/filetree/CMakeLists.txt new file mode 100644 index 0000000..f269f5e --- /dev/null +++ b/barecode/src/filetree/CMakeLists.txt @@ -0,0 +1,17 @@ +set(FILETREE_SOURCES + FileTreePanel.cpp + FileTreePanel.h +) + +add_library(BareCode_FileTree STATIC ${FILETREE_SOURCES}) + +target_link_libraries(BareCode_FileTree PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode_FileTree PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/barecode/src/filetree/FileTreePanel.cpp b/barecode/src/filetree/FileTreePanel.cpp new file mode 100644 index 0000000..1ed0221 --- /dev/null +++ b/barecode/src/filetree/FileTreePanel.cpp @@ -0,0 +1,259 @@ +#include "FileTreePanel.h" + +#include +#include +#include +#include +#include +#include + +FileTreePanel::FileTreePanel(QWidget *parent) + : QWidget(parent) +{ + setupUi(); +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- +void FileTreePanel::setupUi() +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + m_layout->setSpacing(0); + + // Small header label showing the project name + m_label = new QLabel(tr("Kein Projekt geöffnet"), this); + m_label->setContentsMargins(6, 4, 6, 4); + m_label->setStyleSheet("font-weight: bold; background: palette(mid);"); + m_label->setWordWrap(true); + m_layout->addWidget(m_label); + + // File system model – show only the project subtree + m_model = new QFileSystemModel(this); + m_model->setReadOnly(false); + m_model->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); + + // Tree view + m_tree = new QTreeView(this); + m_tree->setModel(m_model); + m_tree->setAnimated(true); + m_tree->setIndentation(16); + m_tree->setSortingEnabled(true); + m_tree->sortByColumn(0, Qt::AscendingOrder); + m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_tree->setHeaderHidden(true); + m_tree->setContextMenuPolicy(Qt::CustomContextMenu); + + // Hide all columns except the file name + for (int col = 1; col < m_model->columnCount(); ++col) + { + m_tree->hideColumn(col); + } + + m_layout->addWidget(m_tree); + + connect(m_tree, &QTreeView::activated, + this, &FileTreePanel::onItemActivated); + + connect(m_tree, &QTreeView::customContextMenuRequested, + this, &FileTreePanel::onContextMenuRequested); +} + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- +void FileTreePanel::setRootPath(const QString &path) +{ + const QModelIndex root = m_model->setRootPath(path); + m_tree->setRootIndex(root); + + const QString projectName = QDir(path).dirName(); + m_label->setText(projectName.isEmpty() ? path : projectName); +} + +void FileTreePanel::clearRoot() +{ + m_model->setRootPath(QString()); + m_tree->setRootIndex(QModelIndex()); + m_label->setText(tr("Kein Projekt geöffnet")); +} + +void FileTreePanel::triggerNewFile() +{ + onNewFile(); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +QString FileTreePanel::selectedDirectory() const +{ + const QModelIndex index = m_tree->currentIndex(); + if (!index.isValid()) + { + return m_model->rootPath(); + } + + const QString path = m_model->filePath(index); + const QFileInfo info(path); + return info.isDir() ? path : info.absolutePath(); +} + +// --------------------------------------------------------------------------- +// Slots +// --------------------------------------------------------------------------- +void FileTreePanel::onItemActivated(const QModelIndex &index) +{ + const QString path = m_model->filePath(index); + const QFileInfo info(path); + + if (info.isFile()) + { + emit fileActivated(path); + } +} + +void FileTreePanel::onContextMenuRequested(const QPoint &pos) +{ + QMenu menu(this); + + QAction *actNewFile = menu.addAction(tr("Neue Datei…")); + QAction *actNewFolder = menu.addAction(tr("Neuer Ordner…")); + menu.addSeparator(); + QAction *actDelete = menu.addAction(tr("Löschen")); + + // Disable delete if nothing is selected + const QModelIndex index = m_tree->indexAt(pos); + actDelete->setEnabled(index.isValid()); + + QAction *chosen = menu.exec(m_tree->viewport()->mapToGlobal(pos)); + + if (chosen == actNewFile) + { + onNewFile(); + } + else if (chosen == actNewFolder) + { + onNewFolder(); + } + else if (chosen == actDelete) + { + onDeleteEntry(); + } +} + +void FileTreePanel::onNewFile() +{ + const QString dir = selectedDirectory(); + if (dir.isEmpty()) + { + return; + } + + bool ok = false; + const QString name = QInputDialog::getText( + this, + tr("Neue Datei"), + tr("Dateiname:"), + QLineEdit::Normal, + QString(), + &ok + ); + + if (!ok || name.trimmed().isEmpty()) + { + return; + } + + const QString filePath = QDir(dir).filePath(name.trimmed()); + + if (QFile::exists(filePath)) + { + QMessageBox::warning(this, tr("Neue Datei"), + tr("Eine Datei mit diesem Namen existiert bereits:\n%1").arg(filePath)); + return; + } + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) + { + QMessageBox::critical(this, tr("Neue Datei"), + tr("Datei konnte nicht angelegt werden:\n%1").arg(filePath)); + return; + } + file.close(); + + emit fileCreated(filePath); + + // Select the new file in the tree + const QModelIndex newIndex = m_model->index(filePath); + m_tree->setCurrentIndex(newIndex); + m_tree->scrollTo(newIndex); +} + +void FileTreePanel::onNewFolder() +{ + const QString dir = selectedDirectory(); + if (dir.isEmpty()) + { + return; + } + + bool ok = false; + const QString name = QInputDialog::getText( + this, + tr("Neuer Ordner"), + tr("Ordnername:"), + QLineEdit::Normal, + QString(), + &ok + ); + + if (!ok || name.trimmed().isEmpty()) + { + return; + } + + if (!QDir(dir).mkdir(name.trimmed())) + { + QMessageBox::critical(this, tr("Neuer Ordner"), + tr("Ordner konnte nicht angelegt werden:\n%1") + .arg(QDir(dir).filePath(name.trimmed()))); + } +} + +void FileTreePanel::onDeleteEntry() +{ + const QModelIndex index = m_tree->currentIndex(); + if (!index.isValid()) + { + return; + } + + const QString path = m_model->filePath(index); + const QFileInfo info(path); + const QString what = info.isDir() ? tr("Ordner") : tr("Datei"); + + const auto answer = QMessageBox::question( + this, + tr("%1 löschen").arg(what), + tr("%1 wirklich löschen?\n%2").arg(what, path), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + ); + + if (answer != QMessageBox::Yes) + { + return; + } + + if (info.isDir()) + { + QDir(path).removeRecursively(); + } + else + { + QFile::remove(path); + } +} diff --git a/barecode/src/filetree/FileTreePanel.h b/barecode/src/filetree/FileTreePanel.h new file mode 100644 index 0000000..32c5c9e --- /dev/null +++ b/barecode/src/filetree/FileTreePanel.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// FileTreePanel – Left panel showing the project directory tree. +// Emits fileActivated(path) when the user double-clicks a file. +// Supports creating new files/folders via context menu. +// --------------------------------------------------------------------------- +class FileTreePanel : public QWidget +{ + Q_OBJECT + +public: + explicit FileTreePanel(QWidget *parent = nullptr); + + void setRootPath(const QString &path); + void clearRoot(); + void triggerNewFile(); // Called from MainWindow menu action + +signals: + void fileActivated(const QString &filePath); + void fileCreated(const QString &filePath); + +private slots: + void onItemActivated(const QModelIndex &index); + void onContextMenuRequested(const QPoint &pos); + void onNewFile(); + void onNewFolder(); + void onDeleteEntry(); + +private: + void setupUi(); + + // Returns the directory of the currently selected item + QString selectedDirectory() const; + + QVBoxLayout *m_layout = nullptr; + QLabel *m_label = nullptr; + QTreeView *m_tree = nullptr; + QFileSystemModel *m_model = nullptr; +}; diff --git a/barecode/src/highlighter/CMakeLists.txt b/barecode/src/highlighter/CMakeLists.txt new file mode 100644 index 0000000..3cbc2eb --- /dev/null +++ b/barecode/src/highlighter/CMakeLists.txt @@ -0,0 +1,19 @@ +set(HIGHLIGHTER_SOURCES + SyntaxHighlighter.cpp + SyntaxHighlighter.h + HighlighterFactory.cpp + HighlighterFactory.h +) + +add_library(BareCode_Highlighter STATIC ${HIGHLIGHTER_SOURCES}) + +target_link_libraries(BareCode_Highlighter PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +target_include_directories(BareCode_Highlighter PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) diff --git a/barecode/src/highlighter/HighlighterFactory.cpp b/barecode/src/highlighter/HighlighterFactory.cpp new file mode 100644 index 0000000..250e3a2 --- /dev/null +++ b/barecode/src/highlighter/HighlighterFactory.cpp @@ -0,0 +1,45 @@ +#include "HighlighterFactory.h" + +#include +#include +#include + +SyntaxHighlighter *HighlighterFactory::createForFile(const QString &filePath, + QTextDocument *document) +{ + // Erweiterung → Highlighter-Fabrik + // Neue Sprache hinzufügen: einfach einen Eintrag ergänzen. + static const QHash> registry = + { + // C / C++ + { "c", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cc", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cpp", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "cxx", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "h", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "hpp", [](QTextDocument *d) { return new CppHighlighter(d); } }, + { "hxx", [](QTextDocument *d) { return new CppHighlighter(d); } }, + + // CSS + { "css", [](QTextDocument *d) { return new CssHighlighter(d); } }, + + // HTML / Templates + { "html", [](QTextDocument *d) { return new HtmlHighlighter(d); } }, + { "htm", [](QTextDocument *d) { return new HtmlHighlighter(d); } }, + { "xhtml",[](QTextDocument *d) { return new HtmlHighlighter(d); } }, + + // PHP (HTML + eingebettetes PHP) + { "php", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "phtml",[](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php3", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php4", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php5", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php7", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + { "php8", [](QTextDocument *d) { return new PhpHighlighter(d); } }, + }; + + const QString ext = QFileInfo(filePath).suffix().toLower(); + const auto it = registry.find(ext); + + return (it != registry.end()) ? it.value()(document) : nullptr; +} diff --git a/barecode/src/highlighter/HighlighterFactory.h b/barecode/src/highlighter/HighlighterFactory.h new file mode 100644 index 0000000..8392758 --- /dev/null +++ b/barecode/src/highlighter/HighlighterFactory.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include "SyntaxHighlighter.h" + +// --------------------------------------------------------------------------- +// HighlighterFactory – Maps file extensions to the correct highlighter. +// To add a new language, register it in HighlighterFactory.cpp. +// --------------------------------------------------------------------------- +class HighlighterFactory +{ +public: + // Creates and returns a highlighter for the given file path. + // Returns nullptr if no highlighter is registered for this file type. + static SyntaxHighlighter *createForFile(const QString &filePath, + QTextDocument *document); +}; diff --git a/barecode/src/highlighter/SyntaxHighlighter.cpp b/barecode/src/highlighter/SyntaxHighlighter.cpp new file mode 100644 index 0000000..4658293 --- /dev/null +++ b/barecode/src/highlighter/SyntaxHighlighter.cpp @@ -0,0 +1,589 @@ +#include "SyntaxHighlighter.h" +#include + +// =========================================================================== +// SyntaxHighlighter – Basis +// =========================================================================== +SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent) + : QSyntaxHighlighter(parent) +{ +} + +void SyntaxHighlighter::highlightBlock(const QString &text) +{ + // Einzel-Zeilen-Regeln anwenden + for (const HighlightRule &rule : m_rules) + { + QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text); + while (it.hasNext()) + { + QRegularExpressionMatch match = it.next(); + setFormat( + static_cast(match.capturedStart()), + static_cast(match.capturedLength()), + rule.format + ); + } + } + + if (!m_hasMultiLineComment) + { + setCurrentBlockState(0); + return; + } + + // Mehrzeilige Kommentare + // Zustand 0 = normal, 1 = mitten in /* ... */ + setCurrentBlockState(0); + + // Leere Zeile mitten im Kommentar — Zustand weitertragen + if (text.isEmpty()) + { + if (previousBlockState() == 1) + { + setCurrentBlockState(1); + } + return; + } + + int startIndex = 0; + + if (previousBlockState() == 1) + { + // Vorherige Zeile war mitten im Kommentar — ab Zeilenanfang nach Ende suchen + QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, 0); + + if (endMatch.hasMatch()) + { + // Ende des Kommentars gefunden + const int commentLength = static_cast(endMatch.capturedStart()) + + static_cast(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(nextStart.capturedStart()) + : -1; + } + else + { + // Noch kein Ende — gesamte Zeile ist Kommentar + setFormat(0, text.length(), m_multiLineCommentFormat); + setCurrentBlockState(1); + return; + } + } + else + { + // Neuen Kommentaranfang suchen + QRegularExpressionMatch m = m_commentStartExpression.match(text); + startIndex = m.hasMatch() ? static_cast(m.capturedStart()) : -1; + } + + while (startIndex >= 0) + { + QRegularExpressionMatch endMatch = + m_commentEndExpression.match(text, startIndex); + + int commentLength = 0; + + if (endMatch.hasMatch()) + { + commentLength = static_cast(endMatch.capturedStart()) + - startIndex + + static_cast(endMatch.capturedLength()); + } + else + { + // Kommentar geht über Zeilenende + setCurrentBlockState(1); + commentLength = text.length() - startIndex; + } + + setFormat(startIndex, commentLength, m_multiLineCommentFormat); + + if (!endMatch.hasMatch()) + { + break; + } + + // Nach weiteren Kommentaren in dieser Zeile suchen + QRegularExpressionMatch nextStart = + m_commentStartExpression.match(text, startIndex + commentLength); + startIndex = nextStart.hasMatch() + ? static_cast(nextStart.capturedStart()) + : -1; + } +} + +// =========================================================================== +// CppHighlighter +// =========================================================================== +CppHighlighter::CppHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + m_hasMultiLineComment = true; + + QTextCharFormat keywordFormat; + keywordFormat.setForeground(QColor("#569CD6")); + keywordFormat.setFontWeight(QFont::Bold); + + const QStringList keywords = { + "alignas","alignof","and","and_eq","asm","auto","bitand","bitor", + "bool","break","case","catch","char","char8_t","char16_t","char32_t", + "class","compl","concept","const","consteval","constexpr","constinit", + "const_cast","continue","co_await","co_return","co_yield","decltype", + "default","delete","do","double","dynamic_cast","else","enum", + "explicit","export","extern","false","float","for","friend","goto", + "if","inline","int","long","mutable","namespace","new","noexcept", + "not","not_eq","nullptr","operator","or","or_eq","private","protected", + "public","register","reinterpret_cast","requires","return","short", + "signed","sizeof","static","static_assert","static_cast","struct", + "switch","template","this","thread_local","throw","true","try", + "typedef","typeid","typename","union","unsigned","using","virtual", + "void","volatile","wchar_t","while","xor","xor_eq","override","final" + }; + + for (const QString &kw : keywords) + { + HighlightRule rule; + rule.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw)); + rule.format = keywordFormat; + m_rules.append(rule); + } + + QTextCharFormat preprocFormat; + preprocFormat.setForeground(QColor("#C586C0")); + { HighlightRule r; r.pattern = QRegularExpression("^\\s*#\\s*\\w+"); r.format = preprocFormat; m_rules.append(r); } + + QTextCharFormat stringFormat; + stringFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*")"); r.format = stringFormat; m_rules.append(r); } + { HighlightRule r; r.pattern = QRegularExpression(R"('(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); } + + QTextCharFormat numberFormat; + numberFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b(0[xX][0-9A-Fa-f]+[uUlL]*|[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?[fFlLuU]*)\b)"); r.format = numberFormat; m_rules.append(r); } + + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + { HighlightRule r; r.pattern = QRegularExpression("//[^\n]*"); r.format = commentFormat; m_rules.append(r); } + + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(R"(/\*)"); + m_commentEndExpression = QRegularExpression(R"(\*/)"); +} + +void CppHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// CssHighlighter +// =========================================================================== +CssHighlighter::CssHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + m_hasMultiLineComment = true; + + // Selektoren: .klasse #id element ::pseudo :pseudo + QTextCharFormat selectorFormat; + selectorFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"([.#]?[\w-]+\s*(?=\s*[,{]))"); r.format = selectorFormat; m_rules.append(r); } + { HighlightRule r; r.pattern = QRegularExpression(R"(:{1,2}[\w-]+)"); r.format = selectorFormat; m_rules.append(r); } + + // Eigenschaften (property:) + QTextCharFormat propFormat; + propFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"([\w-]+\s*(?=:))"); r.format = propFormat; m_rules.append(r); } + + // Werte – Farben #hex + QTextCharFormat colorFormat; + colorFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"(#[0-9A-Fa-f]{3,8}\b)"); r.format = colorFormat; m_rules.append(r); } + + // Zahlen + Einheiten + QTextCharFormat numberFormat; + numberFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*(px|em|rem|%|vh|vw|pt|cm|mm|s|ms)?\b)"); r.format = numberFormat; m_rules.append(r); } + + // Strings + QTextCharFormat stringFormat; + stringFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); } + + // !important + QTextCharFormat importantFormat; + importantFormat.setForeground(QColor("#F44747")); + importantFormat.setFontWeight(QFont::Bold); + { HighlightRule r; r.pattern = QRegularExpression(R"(!important)"); r.format = importantFormat; m_rules.append(r); } + + // @-Regeln + QTextCharFormat atFormat; + atFormat.setForeground(QColor("#C586C0")); + { HighlightRule r; r.pattern = QRegularExpression(R"(@[\w-]+)"); r.format = atFormat; m_rules.append(r); } + + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(R"(/\*)"); + m_commentEndExpression = QRegularExpression(R"(\*/)"); +} + +void CssHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// HtmlHighlighter +// =========================================================================== +HtmlHighlighter::HtmlHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + // Tag-Namen
+ QTextCharFormat tagFormat; + tagFormat.setForeground(QColor("#569CD6")); + { HighlightRule r; r.pattern = QRegularExpression(R"()"); r.format = tagFormat; m_rules.append(r); } + + // Attribute name= + QTextCharFormat attrFormat; + attrFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); } + + // Attributwerte "wert" 'wert' + QTextCharFormat valueFormat; + valueFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); } + + // DOCTYPE + QTextCharFormat doctypeFormat; + doctypeFormat.setForeground(QColor("#808080")); + { HighlightRule r; r.pattern = QRegularExpression(R"(]*>)", QRegularExpression::CaseInsensitiveOption); r.format = doctypeFormat; m_rules.append(r); } + + // Entities & { + QTextCharFormat entityFormat; + entityFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); } + + // Kommentare (mehrzeilig) + QTextCharFormat commentFormat; + commentFormat.setForeground(QColor("#6A9955")); + commentFormat.setFontItalic(true); + m_multiLineCommentFormat = commentFormat; + m_commentStartExpression = QRegularExpression(""); + m_hasMultiLineComment = true; +} + +void HtmlHighlighter::highlightBlock(const QString &text) +{ + SyntaxHighlighter::highlightBlock(text); +} + +// =========================================================================== +// PhpHighlighter +// =========================================================================== +PhpHighlighter::PhpHighlighter(QTextDocument *parent) + : SyntaxHighlighter(parent) +{ + // ---- HTML-Regeln (Basis, für den Teil außerhalb von ) ---- + + QTextCharFormat tagFormat; + tagFormat.setForeground(QColor("#569CD6")); + { HighlightRule r; r.pattern = QRegularExpression(R"()"); r.format = tagFormat; m_rules.append(r); } + + QTextCharFormat attrFormat; + attrFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); } + + QTextCharFormat valueFormat; + valueFormat.setForeground(QColor("#CE9178")); + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); } + + QTextCharFormat entityFormat; + entityFormat.setForeground(QColor("#D7BA7D")); + { HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); } + + // HTML-Kommentare + QTextCharFormat htmlCommentFormat; + htmlCommentFormat.setForeground(QColor("#6A9955")); + htmlCommentFormat.setFontItalic(true); + m_multiLineCommentFormat = htmlCommentFormat; + m_commentStartExpression = QRegularExpression(""); + m_hasMultiLineComment = true; + + // ---- PHP-Tags hervorheben ---- + m_phpTagFormat.setForeground(QColor("#C586C0")); + m_phpTagFormat.setFontWeight(QFont::Bold); + + // ---- PHP-spezifische Regeln ---- + m_phpStringFormat.setForeground(QColor("#CE9178")); + m_phpCommentFormat.setForeground(QColor("#6A9955")); + m_phpCommentFormat.setFontItalic(true); + + // Keywords + QTextCharFormat kwFormat; + kwFormat.setForeground(QColor("#569CD6")); + kwFormat.setFontWeight(QFont::Bold); + + const QStringList phpKeywords = { + "abstract","and","array","as","break","callable","case","catch", + "class","clone","const","continue","declare","default","die","do", + "echo","else","elseif","empty","enddeclare","endfor","endforeach", + "endif","endswitch","endwhile","enum","extends","final","finally", + "fn","for","foreach","function","global","goto","if","implements", + "include","include_once","instanceof","insteadof","interface", + "isset","list","match","namespace","new","or","print","private", + "protected","public","readonly","require","require_once","return", + "static","switch","throw","trait","try","unset","use","var", + "while","xor","yield","null","true","false","NULL","TRUE","FALSE" + }; + + for (const QString &kw : phpKeywords) + { + HighlightRule r; + r.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw)); + r.format = kwFormat; + m_phpRules.append(r); + } + + // Variablen $var + QTextCharFormat varFormat; + varFormat.setForeground(QColor("#9CDCFE")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\$[\w]+)"); r.format = varFormat; m_phpRules.append(r); } + + // Strings + { HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = m_phpStringFormat; m_phpRules.append(r); } + + // Zahlen + QTextCharFormat numFormat; + numFormat.setForeground(QColor("#B5CEA8")); + { HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*\b)"); r.format = numFormat; m_phpRules.append(r); } + + // Einzeilige Kommentare + { HighlightRule r; r.pattern = QRegularExpression(R"((//|#)[^\n]*)"); r.format = m_phpCommentFormat; m_phpRules.append(r); } + + // Eingebaute Funktionen (Auswahl der häufigsten) + QTextCharFormat builtinFormat; + builtinFormat.setForeground(QColor("#DCDCAA")); + const QStringList builtins = { + "array_map","array_filter","array_keys","array_values","array_merge", + "array_push","array_pop","array_shift","array_slice","array_splice", + "count","strlen","substr","strpos","strtolower","strtoupper","trim", + "ltrim","rtrim","explode","implode","str_replace","preg_match", + "preg_replace","sprintf","printf","print_r","var_dump","isset", + "empty","unset","intval","floatval","strval","is_array","is_string", + "is_int","is_float","is_null","is_bool","is_numeric","date","time", + "mktime","json_encode","json_decode","header","session_start", + "htmlspecialchars","htmlentities","strip_tags","nl2br","round", + "floor","ceil","abs","min","max","rand","in_array","array_key_exists", + "sort","rsort","usort","ksort","krsort","ob_start","ob_get_clean" + }; + + for (const QString &fn : builtins) + { + HighlightRule r; + r.pattern = QRegularExpression(QString("\\b%1\\b").arg(fn)); + r.format = builtinFormat; + m_phpRules.append(r); + } +} + +void PhpHighlighter::highlightPhpRange(const QString &text, int start, int length) +{ + if (length <= 0) + { + return; + } + + const QString phpText = text.mid(start, length); + + for (const HighlightRule &rule : m_phpRules) + { + QRegularExpressionMatchIterator it = rule.pattern.globalMatch(phpText); + while (it.hasNext()) + { + QRegularExpressionMatch match = it.next(); + setFormat( + start + static_cast(match.capturedStart()), + static_cast(match.capturedLength()), + rule.format + ); + } + } + + 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(closeMatch.capturedStart()) + + static_cast(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(openMatch.capturedStart()); + QRegularExpressionMatch closeMatch = blockClose.match(phpText, openPos + 2); + + if (closeMatch.hasMatch()) + { + const int closeEnd = static_cast(closeMatch.capturedStart()) + + static_cast(closeMatch.capturedLength()); + setFormat(start + openPos, closeEnd - openPos, m_phpCommentFormat); + searchFrom = closeEnd; + } + else + { + setFormat(start + openPos, length - openPos, m_phpCommentFormat); + setCurrentBlockState(3); + return; + } + } +} + +void PhpHighlighter::highlightBlock(const QString &text) +{ + // Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden + // (setzt auch den State für HTML Kommentare) + SyntaxHighlighter::highlightBlock(text); + + // Block-Zustände: + // 0 = HTML-Modus + // 1 = HTML Kommentar (von Basisklasse verwaltet) + // 2 = innerhalb PHP-Block (kein /* */ Kommentar) + // 3 = innerhalb PHP /* */ Block-Kommentar + + // State 1 (HTML-Kommentar) wurde von der Basisklasse gesetzt — nicht überschreiben + if (currentBlockState() == 1) + { + return; + } + + // Wenn der vorherige Block ein HTML-Kommentar war und dieser noch nicht + // abgeschlossen wurde, hat die Basisklasse das bereits korrekt behandelt. + // Wir setzen nur dann auf 0 zurück wenn wir sicher nicht in HTML-Kommentar sind. + if (previousBlockState() != 1) + { + setCurrentBlockState(0); + } + + // Leere Zeile — Zustand weitertragen + if (text.isEmpty()) + { + const int prev = previousBlockState(); + if (prev == 2 || prev == 3) + { + setCurrentBlockState(prev); + } + return; + } + + static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)", + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression phpClose(R"(\?>)"); + + int pos = 0; + + if (previousBlockState() == 2 || previousBlockState() == 3) + { + // Wir befinden uns bereits in einem PHP-Block (ggf. in einem Kommentar) + QRegularExpressionMatch closeMatch = phpClose.match(text, 0); + if (closeMatch.hasMatch()) + { + const int end = static_cast(closeMatch.capturedStart()) + + static_cast(closeMatch.capturedLength()); + highlightPhpRange(text, 0, end); + setFormat(static_cast(closeMatch.capturedStart()), + static_cast(closeMatch.capturedLength()), + m_phpTagFormat); + pos = end; + setCurrentBlockState(0); + } + else + { + highlightPhpRange(text, 0, text.length()); + if (currentBlockState() != 3) + { + setCurrentBlockState(2); + } + return; + } + } + + while (pos < text.length()) + { + QRegularExpressionMatch openMatch = phpOpen.match(text, pos); + if (!openMatch.hasMatch()) + { + break; + } + + const int openStart = static_cast(openMatch.capturedStart()); + const int openEnd = openStart + static_cast(openMatch.capturedLength()); + + setFormat(openStart, static_cast(openMatch.capturedLength()), m_phpTagFormat); + + QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd); + if (closeMatch.hasMatch()) + { + const int closeStart = static_cast(closeMatch.capturedStart()); + const int closeEnd = closeStart + static_cast(closeMatch.capturedLength()); + + highlightPhpRange(text, openEnd, closeStart - openEnd); + setFormat(closeStart, static_cast(closeMatch.capturedLength()), m_phpTagFormat); + + pos = closeEnd; + setCurrentBlockState(0); + } + else + { + highlightPhpRange(text, openEnd, text.length() - openEnd); + if (currentBlockState() != 3) + { + setCurrentBlockState(2); + } + return; + } + } +} diff --git a/barecode/src/highlighter/SyntaxHighlighter.h b/barecode/src/highlighter/SyntaxHighlighter.h new file mode 100644 index 0000000..9c330ae --- /dev/null +++ b/barecode/src/highlighter/SyntaxHighlighter.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// SyntaxHighlighter – Regelbasierte Basis-Klasse. +// Unterklassen befüllen m_rules und können highlightBlock() überschreiben +// um mehrzeilige Konstrukte (Block-Kommentare, heredocs, …) zu behandeln. +// --------------------------------------------------------------------------- +class SyntaxHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + explicit SyntaxHighlighter(QTextDocument *parent = nullptr); + +protected: + struct HighlightRule + { + QRegularExpression pattern; + QTextCharFormat format; + }; + + void highlightBlock(const QString &text) override; + + // Unterklassen befüllen dies im Konstruktor + QVector m_rules; + + // Mehrzeilige Block-Kommentare (/* ... */) + QRegularExpression m_commentStartExpression; + QRegularExpression m_commentEndExpression; + QTextCharFormat m_multiLineCommentFormat; + bool m_hasMultiLineComment = false; +}; + +// --------------------------------------------------------------------------- +// CppHighlighter – C und C++ +// --------------------------------------------------------------------------- +class CppHighlighter : public SyntaxHighlighter +{ + Q_OBJECT +public: + explicit CppHighlighter(QTextDocument *parent = nullptr); +protected: + void highlightBlock(const QString &text) override; +}; + +// --------------------------------------------------------------------------- +// CssHighlighter – CSS +// --------------------------------------------------------------------------- +class CssHighlighter : public SyntaxHighlighter +{ + Q_OBJECT +public: + explicit CssHighlighter(QTextDocument *parent = nullptr); +protected: + void highlightBlock(const QString &text) override; +}; + +// --------------------------------------------------------------------------- +// HtmlHighlighter – HTML (mit eingebettetem CSS in