Dateien, die als Parameter übergeben werden, werden nach dem Start direkt geladen
3
barecode/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{src/
|
||||
build/
|
||||
BareCodeAUR
|
||||
13
barecode/BareCode.desktop
Normal file
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=BareCode
|
||||
GenericName=Code Editor
|
||||
Comment=Modularer Code-Editor für HTML, PHP, CSS und mehr
|
||||
Exec=BareCode %F
|
||||
Icon=barecode
|
||||
Terminal=false
|
||||
Categories=Development;TextEditor;
|
||||
MimeType=text/plain;text/html;text/css;text/x-php;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr;
|
||||
Keywords=editor;code;html;php;css;c++;
|
||||
StartupWMClass=BareCode
|
||||
1
barecode/BareCode.rc
Normal file
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON "resources/BareCode.ico"
|
||||
159
barecode/CMakeLists.txt
Normal file
@@ -0,0 +1,159 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(BareCode
|
||||
VERSION 1.2.0
|
||||
DESCRIPTION "A modular code editor built with Qt6"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plattform-Erkennung
|
||||
# ---------------------------------------------------------------------------
|
||||
if(WIN32)
|
||||
set(PLATFORM_WINDOWS TRUE)
|
||||
add_compile_definitions(PLATFORM_WINDOWS)
|
||||
set(CMAKE_WIN32_EXECUTABLE ON)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Haiku")
|
||||
set(PLATFORM_HAIKU TRUE)
|
||||
add_compile_definitions(PLATFORM_HAIKU)
|
||||
elseif(APPLE)
|
||||
set(PLATFORM_MACOS TRUE)
|
||||
add_compile_definitions(PLATFORM_MACOS)
|
||||
elseif(UNIX)
|
||||
set(PLATFORM_LINUX TRUE)
|
||||
add_compile_definitions(PLATFORM_LINUX)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Qt6
|
||||
# ---------------------------------------------------------------------------
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Concurrent)
|
||||
qt_standard_project_setup()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
add_subdirectory(src)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Übersetzungen — lrelease → .qm → ins Build-Verzeichnis
|
||||
# ---------------------------------------------------------------------------
|
||||
find_program(LRELEASE_EXECUTABLE
|
||||
NAMES lrelease lrelease-qt6
|
||||
HINTS "${Qt6_DIR}/../../../bin"
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
set(TS_FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_de.ts
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/translations/barecode_en.ts
|
||||
)
|
||||
|
||||
set(QM_FILES)
|
||||
foreach(TS_FILE ${TS_FILES})
|
||||
get_filename_component(TS_NAME ${TS_FILE} NAME_WE)
|
||||
set(QM_FILE "${CMAKE_CURRENT_BINARY_DIR}/translations/${TS_NAME}.qm")
|
||||
add_custom_command(
|
||||
OUTPUT "${QM_FILE}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/translations"
|
||||
COMMAND ${LRELEASE_EXECUTABLE} "${TS_FILE}" -qm "${QM_FILE}"
|
||||
DEPENDS "${TS_FILE}"
|
||||
COMMENT "lrelease: ${TS_NAME}.qm"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND QM_FILES "${QM_FILE}")
|
||||
endforeach()
|
||||
|
||||
add_custom_target(BareCode_translations ALL DEPENDS ${QM_FILES})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Resources
|
||||
# ---------------------------------------------------------------------------
|
||||
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Executable
|
||||
# ---------------------------------------------------------------------------
|
||||
if(PLATFORM_WINDOWS)
|
||||
qt_add_executable(BareCode main.cpp BareCode.rc ${BARECODE_RESOURCES})
|
||||
else()
|
||||
qt_add_executable(BareCode main.cpp ${BARECODE_RESOURCES})
|
||||
endif()
|
||||
|
||||
add_dependencies(BareCode BareCode_translations)
|
||||
|
||||
target_link_libraries(BareCode PRIVATE
|
||||
BareCode_Core
|
||||
BareCode_Editor
|
||||
BareCode_FileTree
|
||||
BareCode_Highlighter
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
target_include_directories(BareCode PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installation
|
||||
# ---------------------------------------------------------------------------
|
||||
include(GNUInstallDirs)
|
||||
|
||||
install(TARGETS BareCode
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(FILES LICENSE
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode
|
||||
)
|
||||
|
||||
# .qm-Dateien installieren
|
||||
install(FILES ${QM_FILES}
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode/translations
|
||||
)
|
||||
|
||||
if(PLATFORM_LINUX)
|
||||
foreach(SIZE 16 32 48 64 128 256 512)
|
||||
install(FILES resources/icon_${SIZE}.png
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/${SIZE}x${SIZE}/apps
|
||||
RENAME barecode.png
|
||||
)
|
||||
endforeach()
|
||||
install(FILES BareCode.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
|
||||
)
|
||||
endif()
|
||||
|
||||
if(PLATFORM_HAIKU)
|
||||
install(FILES resources/icon_256.png
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode
|
||||
RENAME BareCode.png
|
||||
)
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND mimeset -f \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/BareCode\"
|
||||
RESULT_VARIABLE _mimeset_result
|
||||
)
|
||||
if(NOT _mimeset_result EQUAL 0)
|
||||
message(WARNING \"mimeset konnte nicht ausgeführt werden.\")
|
||||
endif()
|
||||
")
|
||||
endif()
|
||||
|
||||
if(PLATFORM_MACOS)
|
||||
set_target_properties(BareCode PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "BareCode"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}
|
||||
)
|
||||
endif()
|
||||
21
barecode/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
36
barecode/PKGBUILD
Normal file
@@ -0,0 +1,36 @@
|
||||
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
||||
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
|
||||
|
||||
pkgname=barecode
|
||||
pkgver=1.2.0
|
||||
pkgrel=1
|
||||
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://www.projekt-hirnfrei.de"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode-git')
|
||||
source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz")
|
||||
sha256sums=('SKIP') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz
|
||||
|
||||
build() {
|
||||
cd "BareCode"
|
||||
cmake -B build \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_SKIP_RPATH=ON
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
check() {
|
||||
:
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "BareCode"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
43
barecode/PKGBUILD-git
Normal file
@@ -0,0 +1,43 @@
|
||||
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
||||
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
|
||||
|
||||
pkgname=barecode-git
|
||||
pkgver=1.1.0.r0.g0000000
|
||||
pkgrel=1
|
||||
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++ (Git-Version)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://www.projekt-hirnfrei.de"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja' 'git')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode')
|
||||
source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$pkgname"
|
||||
git describe --long --tags 2>/dev/null \
|
||||
| sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' \
|
||||
|| printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$pkgname"
|
||||
cmake -B build \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_SKIP_RPATH=ON
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
check() {
|
||||
:
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
200
barecode/README.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# BareCode
|
||||
|
||||
**Schlanker, modularer Code-Editor für Web-Entwickler**
|
||||
Version 1.2.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de)
|
||||
|
||||
Entwickelt mit **C++17**, **Qt6** und **CMake**.
|
||||
Kompiliert auf **Linux**, **Windows** und **Haiku**.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Arch Linux / Manjaro (AUR)
|
||||
|
||||
```bash
|
||||
# Mit yay
|
||||
yay -S barecode
|
||||
|
||||
# Mit paru
|
||||
paru -S barecode
|
||||
|
||||
# Immer aktuellster Git-Stand
|
||||
yay -S barecode-git
|
||||
```
|
||||
|
||||
### Aus dem Quellcode bauen
|
||||
|
||||
Siehe Abschnitt **Bauen** weiter unten.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Editor
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare über Leerzeilen |
|
||||
| Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
|
||||
| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
|
||||
| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
|
||||
| Einrück-Führungslinien | Vertikale Linien wie in VS Code |
|
||||
| Klammerzugehörigkeit | Zusammengehörige Klammern werden grün hervorgehoben, fehlende rot |
|
||||
| Auto-Indent | Einrückungstiefe wird bei Enter übernommen |
|
||||
| Tab / Shift+Tab | Ein- und Ausrücken für einzelne Zeilen und Selektionen |
|
||||
| Smart Backspace | Springt zur vorherigen Tab-Stop-Position |
|
||||
| Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt |
|
||||
| Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog |
|
||||
| Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen |
|
||||
| Variablen-Popup | Vorschläge für $variablen beim Tippen (nur Anzeige, kein Autocomplete) |
|
||||
|
||||
### Navigation
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Projektbaum | Linkes Panel — zeigt nur das gewählte Projektverzeichnis |
|
||||
| Neue Datei / Ordner | Rechtsklick im Dateibaum |
|
||||
| Doppelklick-Navigation | Doppelklick auf einen Funktionsnamen springt zur Definition |
|
||||
| Suchen & Ersetzen | Einzeln, Alle, In Auswahl, Regex, Live-Hervorhebung (Strg+F) |
|
||||
| In Dateien suchen | Rekursive Projektsuche mit Ergebnisliste (Strg+Shift+F) |
|
||||
|
||||
### Projektanalyse
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Projektfunktionen | Alle definierten Funktionen als durchsuchbare Liste (Strg+Shift+P) |
|
||||
| Tote Funktionen | Analyse ungenutzter Funktionen mit Exclude-Verzeichnissen (Strg+Shift+T) |
|
||||
| Export | Analyseergebnisse als TXT exportieren |
|
||||
|
||||
### Allgemein
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Speichern | Speichern, Speichern unter, Alles speichern |
|
||||
| Dark / Light Mode | Umschaltbar (Strg+Shift+D), wird gespeichert |
|
||||
| Session | Geöffnete Dateien und aktiver Tab werden wiederhergestellt |
|
||||
|
||||
---
|
||||
|
||||
## Tastaturkürzel
|
||||
|
||||
| Aktion | Kürzel |
|
||||
|---|---|
|
||||
| Datei öffnen | Strg+O |
|
||||
| Neue Datei | Strg+N |
|
||||
| Projekt öffnen | Strg+Shift+O |
|
||||
| Speichern | Strg+S |
|
||||
| Speichern unter | Strg+Shift+A |
|
||||
| Alles speichern | Strg+Shift+S |
|
||||
| Suchen / Ersetzen | Strg+F |
|
||||
| In Dateien suchen | Strg+Shift+F |
|
||||
| Projektfunktionen | Strg+Shift+P |
|
||||
| Tote Funktionen | Strg+Shift+T |
|
||||
| Dark Mode | Strg+Shift+D |
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
BareCode/
|
||||
├── CMakeLists.txt
|
||||
├── main.cpp
|
||||
├── LICENSE
|
||||
├── PKGBUILD # Stabiles AUR-Paket
|
||||
├── PKGBUILD-git # AUR-Paket (Git-Stand)
|
||||
├── BareCode.desktop # Linux-Anwendungsmenü
|
||||
├── resources/
|
||||
│ ├── resources.qrc
|
||||
│ ├── icon_*.png
|
||||
│ └── php_functions.json
|
||||
└── src/
|
||||
├── core/ # MainWindow, ProjectManager, Settings,
|
||||
│ # ThemeManager, AboutDialog, IPlugin
|
||||
├── editor/ # EditorPanel, EditorTab, CodeEditor,
|
||||
│ # SearchPanel, FileSearchPanel,
|
||||
│ # ColorIndicator, SignatureHelper,
|
||||
│ # VariableCompleter, FunctionScanner,
|
||||
│ # FunctionIndex, FunctionListPanel,
|
||||
│ # FunctionListDialog, DeadCodeAnalyzer,
|
||||
│ # DeadCodeDialog
|
||||
├── filetree/ # FileTreePanel
|
||||
└── highlighter/ # SyntaxHighlighter, HighlighterFactory
|
||||
```
|
||||
|
||||
Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek.
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten & Bauen
|
||||
|
||||
### Linux
|
||||
|
||||
**Ubuntu / Debian:**
|
||||
```bash
|
||||
sudo apt install cmake ninja-build qt6-base-dev qt6-base-dev-tools
|
||||
```
|
||||
|
||||
**Arch / Manjaro:**
|
||||
```bash
|
||||
sudo pacman -S cmake ninja qt6-base
|
||||
```
|
||||
|
||||
**Bauen:**
|
||||
```bash
|
||||
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
**Installieren:**
|
||||
```bash
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Windows
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- [Qt6](https://www.qt.io/download) — Komponenten: Qt6 Base, Qt6 Concurrent
|
||||
- CMake ≥ 3.16
|
||||
- Visual Studio 2019 oder neuer
|
||||
|
||||
```bat
|
||||
cmake -B build -G "Visual Studio 17 2022" -A x64
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Haiku
|
||||
|
||||
```bash
|
||||
pkgman install qt6_base qt6_base_devel
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
> Getestet auf Haiku R1/beta5.
|
||||
|
||||
---
|
||||
|
||||
## Neue Sprache hinzufügen
|
||||
|
||||
1. Subklasse von `SyntaxHighlighter` in `src/highlighter/` erstellen
|
||||
2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage)
|
||||
3. Dateiendung in `HighlighterFactory.cpp` registrieren
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License — Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [www.projekt-hirnfrei.de](https://www.projekt-hirnfrei.de)
|
||||
- Discord: [discord.projekt-hirnfrei.de](https://discord.projekt-hirnfrei.de)
|
||||
- Quellcode: [git.projekt-hirnfrei.de/diabolus/BareCode](https://git.projekt-hirnfrei.de/diabolus/BareCode)
|
||||
- AUR: [aur.archlinux.org/packages/barecode](https://aur.archlinux.org/packages/barecode)
|
||||
13
barecode/barecode.desktop
Normal file
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=BareCode
|
||||
GenericName=Code-Editor
|
||||
Comment=Modularer Code-Editor von Projekt Hirnfrei
|
||||
Exec=BareCode %F
|
||||
Icon=barecode
|
||||
Terminal=false
|
||||
Categories=Development;TextEditor;IDE;
|
||||
MimeType=text/plain;text/x-csrc;text/x-chdr;text/x-c++src;text/x-c++hdr;
|
||||
Keywords=editor;code;programmierung;entwicklung;
|
||||
StartupNotify=true
|
||||
StartupWMClass=BareCode
|
||||
133
barecode/bauen.sh
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# BareCode – Build-Skript
|
||||
# Verwendung: ./bauen.sh [release|debug|clean|install]
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
BUILD_TYPE="${1:-release}"
|
||||
BUILD_DIR="build"
|
||||
|
||||
# Farben
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}==>${NC} $1"; }
|
||||
success() { echo -e "${GREEN}==>${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}==>${NC} $1"; }
|
||||
error() { echo -e "${RED}==>${NC} $1"; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# clean
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "$BUILD_TYPE" = "clean" ]; then
|
||||
info "Build-Verzeichnis wird gelöscht…"
|
||||
rm -rf "$BUILD_DIR"
|
||||
success "Fertig."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Abhängigkeiten prüfen
|
||||
# ---------------------------------------------------------------------------
|
||||
info "Prüfe Abhängigkeiten…"
|
||||
|
||||
command -v cmake &>/dev/null || error "cmake nicht gefunden. Installation: sudo pacman -S cmake"
|
||||
command -v ninja &>/dev/null || error "ninja nicht gefunden. Installation: sudo pacman -S ninja"
|
||||
command -v lrelease &>/dev/null || \
|
||||
command -v lrelease-qt6 &>/dev/null || \
|
||||
error "lrelease nicht gefunden. Installation: sudo pacman -S qt6-tools"
|
||||
|
||||
# Qt6 prüfen
|
||||
if ! pkg-config --exists Qt6Core 2>/dev/null; then
|
||||
if ! cmake --find-package -DNAME=Qt6 -DCOMPILER_ID=GNU \
|
||||
-DLANGUAGE=CXX -DMODE=EXIST &>/dev/null 2>&1; then
|
||||
warn "Qt6 konnte nicht automatisch geprüft werden — cmake wird es versuchen."
|
||||
fi
|
||||
fi
|
||||
|
||||
success "Abhängigkeiten OK."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CMake-Build-Typ
|
||||
# ---------------------------------------------------------------------------
|
||||
case "$BUILD_TYPE" in
|
||||
release|Release)
|
||||
CMAKE_BUILD_TYPE="Release"
|
||||
;;
|
||||
debug|Debug)
|
||||
CMAKE_BUILD_TYPE="Debug"
|
||||
;;
|
||||
install)
|
||||
CMAKE_BUILD_TYPE="Release"
|
||||
;;
|
||||
*)
|
||||
error "Unbekannter Build-Typ: '$BUILD_TYPE'. Erlaubt: release, debug, clean, install"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konfigurieren
|
||||
# ---------------------------------------------------------------------------
|
||||
info "Konfiguriere mit CMake (${CMAKE_BUILD_TYPE})…"
|
||||
|
||||
cmake -B "$BUILD_DIR" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
2>&1 || error "CMake-Konfiguration fehlgeschlagen."
|
||||
|
||||
success "Konfiguration OK."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bauen
|
||||
# ---------------------------------------------------------------------------
|
||||
JOBS=$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)
|
||||
info "Baue mit $JOBS parallelen Jobs…"
|
||||
|
||||
cmake --build "$BUILD_DIR" -j"$JOBS" 2>&1 || error "Build fehlgeschlagen."
|
||||
|
||||
success "Build erfolgreich!"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installieren (optional)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "$BUILD_TYPE" = "install" ]; then
|
||||
info "Installiere nach /usr (sudo erforderlich)…"
|
||||
sudo cmake --install "$BUILD_DIR" || error "Installation fehlgeschlagen."
|
||||
success "BareCode wurde installiert."
|
||||
echo ""
|
||||
echo " Starten: BareCode"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fertig
|
||||
# ---------------------------------------------------------------------------
|
||||
BINARY="$BUILD_DIR/BareCode"
|
||||
if [ -f "$BINARY" ]; then
|
||||
# .qm-Dateien neben die Binary kopieren damit sie gefunden werden
|
||||
mkdir -p "$BUILD_DIR/translations"
|
||||
if compgen -G "$BUILD_DIR/translations/barecode_*.qm" > /dev/null 2>&1; then
|
||||
success "Übersetzungen bereits vorhanden."
|
||||
else
|
||||
# Aus dem cmake-Zwischenverzeichnis holen
|
||||
find "$BUILD_DIR" -name "barecode_*.qm" -not -path "$BUILD_DIR/translations/*" \
|
||||
-exec cp {} "$BUILD_DIR/translations/" \; 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
success "BareCode ist bereit:"
|
||||
echo " Starten: ./$BINARY"
|
||||
echo " Installieren: ./bauen.sh install"
|
||||
echo " Debug-Build: ./bauen.sh debug"
|
||||
echo " Aufräumen: ./bauen.sh clean"
|
||||
fi
|
||||
106
barecode/main.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include <QApplication>
|
||||
#include <QIcon>
|
||||
#include <QTranslator>
|
||||
#include <QLocale>
|
||||
#include <QLibraryInfo>
|
||||
#include <QSettings>
|
||||
#include <QDir>
|
||||
#include <QCoreApplication>
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
app.setApplicationName("BareCode");
|
||||
app.setApplicationVersion("1.2.0");
|
||||
app.setOrganizationName("BareCode");
|
||||
|
||||
// Icon
|
||||
QIcon appIcon;
|
||||
appIcon.addFile(":/icon_16.png", QSize(16, 16));
|
||||
appIcon.addFile(":/icon_32.png", QSize(32, 32));
|
||||
appIcon.addFile(":/icon_48.png", QSize(48, 48));
|
||||
appIcon.addFile(":/icon_64.png", QSize(64, 64));
|
||||
appIcon.addFile(":/icon_128.png", QSize(128, 128));
|
||||
appIcon.addFile(":/icon_256.png", QSize(256, 256));
|
||||
appIcon.addFile(":/icon_512.png", QSize(512, 512));
|
||||
app.setWindowIcon(appIcon);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Übersetzung laden
|
||||
// .qm-Dateien liegen im Unterverzeichnis "translations" neben der Binary
|
||||
// oder unter /usr/share/BareCode/translations nach Installation
|
||||
// ---------------------------------------------------------------------------
|
||||
QSettings settings(QSettings::IniFormat, QSettings::UserScope,
|
||||
"BareCode", "BareCode");
|
||||
QString locale = settings.value("language/locale", QString()).toString();
|
||||
|
||||
if (locale.isEmpty())
|
||||
{
|
||||
locale = QLocale::system().name(); // z.B. "de_DE", "en_US"
|
||||
}
|
||||
|
||||
const QString shortLocale = locale.left(2); // "de", "en"
|
||||
|
||||
// Suchpfade für .qm-Dateien
|
||||
QStringList searchPaths;
|
||||
searchPaths << QCoreApplication::applicationDirPath() + "/translations"
|
||||
<< QDir::homePath() + "/.local/share/BareCode/translations"
|
||||
<< "/usr/share/BareCode/translations"
|
||||
<< "/usr/local/share/BareCode/translations";
|
||||
|
||||
// Qt-eigene Übersetzungen (Dialoge, Standard-Buttons)
|
||||
QTranslator qtTranslator;
|
||||
if (qtTranslator.load("qt_" + locale,
|
||||
QLibraryInfo::path(QLibraryInfo::TranslationsPath)))
|
||||
{
|
||||
app.installTranslator(&qtTranslator);
|
||||
}
|
||||
|
||||
// BareCode-Übersetzung — in allen Suchpfaden versuchen
|
||||
QTranslator appTranslator;
|
||||
bool loaded = false;
|
||||
|
||||
for (const QString &path : searchPaths)
|
||||
{
|
||||
if (appTranslator.load(QString("barecode_%1").arg(locale), path) ||
|
||||
appTranslator.load(QString("barecode_%1").arg(shortLocale), path))
|
||||
{
|
||||
app.installTranslator(&appTranslator);
|
||||
loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Englisch
|
||||
if (!loaded && shortLocale != "de")
|
||||
{
|
||||
for (const QString &path : searchPaths)
|
||||
{
|
||||
if (appTranslator.load("barecode_en", path))
|
||||
{
|
||||
app.installTranslator(&appTranslator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
// Datei(en) öffnen, die beim Start per Kommandozeile übergeben wurden
|
||||
// (z. B. Doppelklick auf eine Datei im Dateibrowser → "Öffnen mit BareCode").
|
||||
// Das erste Argument ist der Programmname selbst und wird übersprungen.
|
||||
QStringList cliFiles = app.arguments();
|
||||
if (!cliFiles.isEmpty())
|
||||
{
|
||||
cliFiles.removeFirst();
|
||||
}
|
||||
if (!cliFiles.isEmpty())
|
||||
{
|
||||
window.openFilesFromArguments(cliFiles);
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
BIN
barecode/resources/BareCode.ico
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
barecode/resources/icon_128.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
barecode/resources/icon_16.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
barecode/resources/icon_24.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
barecode/resources/icon_256.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
barecode/resources/icon_32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
barecode/resources/icon_48.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
barecode/resources/icon_512.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
barecode/resources/icon_64.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1197
barecode/resources/php_functions.json
Normal file
12
barecode/resources/resources.qrc
Normal file
@@ -0,0 +1,12 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>icon_512.png</file>
|
||||
<file>icon_256.png</file>
|
||||
<file>icon_128.png</file>
|
||||
<file>icon_64.png</file>
|
||||
<file>icon_48.png</file>
|
||||
<file>icon_32.png</file>
|
||||
<file>icon_16.png</file>
|
||||
<file>php_functions.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
4
barecode/src/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(editor)
|
||||
add_subdirectory(filetree)
|
||||
add_subdirectory(highlighter)
|
||||
105
barecode/src/core/AboutDialog.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "AboutDialog.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QFrame>
|
||||
#include <QFont>
|
||||
#include <QApplication>
|
||||
|
||||
AboutDialog::AboutDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Über BareCode"));
|
||||
setFixedSize(440, 310);
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Header-Banner
|
||||
// -----------------------------------------------------------------------
|
||||
QFrame *banner = new QFrame(this);
|
||||
banner->setFixedHeight(88);
|
||||
banner->setStyleSheet(
|
||||
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0,"
|
||||
" stop:0 #1a1a2e, stop:1 #16213e);"
|
||||
);
|
||||
|
||||
QVBoxLayout *bannerLayout = new QVBoxLayout(banner);
|
||||
bannerLayout->setContentsMargins(24, 10, 24, 10);
|
||||
bannerLayout->setSpacing(2);
|
||||
|
||||
QLabel *appName = new QLabel("BareCode", banner);
|
||||
QFont nameFont = appName->font();
|
||||
nameFont.setPointSize(22);
|
||||
nameFont.setBold(true);
|
||||
appName->setFont(nameFont);
|
||||
appName->setStyleSheet("color: #e0e0ff; background: transparent;");
|
||||
|
||||
QLabel *tagline = new QLabel(tr("Modularer Code-Editor"), banner);
|
||||
tagline->setStyleSheet("color: #8888bb; background: transparent;");
|
||||
|
||||
bannerLayout->addWidget(appName);
|
||||
bannerLayout->addWidget(tagline);
|
||||
root->addWidget(banner);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Info-Tabelle
|
||||
// -----------------------------------------------------------------------
|
||||
QVBoxLayout *info = new QVBoxLayout();
|
||||
info->setContentsMargins(28, 20, 28, 8);
|
||||
info->setSpacing(10);
|
||||
|
||||
auto makeRow = [&](const QString &label, const QString &value)
|
||||
{
|
||||
QHBoxLayout *row = new QHBoxLayout();
|
||||
row->setSpacing(12);
|
||||
|
||||
QLabel *lbl = new QLabel(label, this);
|
||||
QFont boldFont = lbl->font();
|
||||
boldFont.setBold(true);
|
||||
lbl->setFont(boldFont);
|
||||
lbl->setFixedWidth(100);
|
||||
lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
|
||||
QLabel *val = new QLabel(value, this);
|
||||
val->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
row->addWidget(lbl);
|
||||
row->addWidget(val, 1);
|
||||
info->addLayout(row);
|
||||
};
|
||||
|
||||
makeRow(tr("Version"), "1.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);
|
||||
}
|
||||
14
barecode/src/core/AboutDialog.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AboutDialog – Zeigt Versionsinformationen und Entwicklerangaben.
|
||||
// ---------------------------------------------------------------------------
|
||||
class AboutDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AboutDialog(QWidget *parent = nullptr);
|
||||
};
|
||||
28
barecode/src/core/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
set(CORE_SOURCES
|
||||
MainWindow.cpp
|
||||
MainWindow.h
|
||||
IPlugin.h
|
||||
ProjectManager.cpp
|
||||
ProjectManager.h
|
||||
Settings.cpp
|
||||
Settings.h
|
||||
ThemeManager.cpp
|
||||
ThemeManager.h
|
||||
AboutDialog.cpp
|
||||
AboutDialog.h
|
||||
)
|
||||
|
||||
add_library(BareCode_Core STATIC ${CORE_SOURCES})
|
||||
|
||||
target_link_libraries(BareCode_Core PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
BareCode_Editor
|
||||
BareCode_FileTree
|
||||
)
|
||||
|
||||
target_include_directories(BareCode_Core PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
)
|
||||
26
barecode/src/core/IPlugin.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPlugin – Interface that every BareCode module / plugin must implement.
|
||||
// This allows components to be swapped or extended without touching the core.
|
||||
// ---------------------------------------------------------------------------
|
||||
class IPlugin
|
||||
{
|
||||
public:
|
||||
virtual ~IPlugin() = default;
|
||||
|
||||
// Human-readable name of the plugin
|
||||
virtual QString pluginName() const = 0;
|
||||
|
||||
// Version string, e.g. "1.0.0"
|
||||
virtual QString pluginVersion() const = 0;
|
||||
|
||||
// Called once after all plugins are loaded so plugins can cross-reference
|
||||
virtual void initialize() {}
|
||||
|
||||
// Called before the application shuts down
|
||||
virtual void shutdown() {}
|
||||
};
|
||||
412
barecode/src/core/MainWindow.cpp
Normal file
@@ -0,0 +1,412 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QSettings>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "AboutDialog.h"
|
||||
#include "filetree/FileTreePanel.h"
|
||||
#include "editor/EditorPanel.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_projectManager(std::make_unique<ProjectManager>())
|
||||
, m_settings(std::make_unique<Settings>())
|
||||
, m_themeManager(std::make_unique<ThemeManager>())
|
||||
{
|
||||
setWindowTitle("BareCode");
|
||||
setMinimumSize(900, 600);
|
||||
|
||||
setupUi();
|
||||
setupMenuBar();
|
||||
setupStatusBar();
|
||||
connectSignals();
|
||||
restoreWindowState();
|
||||
applyInitialTheme();
|
||||
|
||||
// Letztes Projekt wieder öffnen
|
||||
const QString lastPath = m_settings->lastProjectPath();
|
||||
if (!lastPath.isEmpty())
|
||||
{
|
||||
m_projectManager->openProject(lastPath);
|
||||
}
|
||||
|
||||
// Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab)
|
||||
m_editor->restoreSession(
|
||||
m_settings->lastOpenFiles(),
|
||||
m_settings->lastActiveFile()
|
||||
);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Datei(en) von der Kommandozeile öffnen
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::openFilesFromArguments(const QStringList &filePaths)
|
||||
{
|
||||
for (const QString &path : filePaths)
|
||||
{
|
||||
const QFileInfo info(path);
|
||||
if (info.exists() && info.isFile())
|
||||
{
|
||||
m_editor->openFile(info.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI aufbauen
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::setupUi()
|
||||
{
|
||||
m_splitter = new QSplitter(Qt::Horizontal, this);
|
||||
setCentralWidget(m_splitter);
|
||||
|
||||
m_fileTree = new FileTreePanel(m_splitter);
|
||||
m_editor = new EditorPanel(m_settings.get(), m_splitter);
|
||||
|
||||
m_splitter->addWidget(m_fileTree);
|
||||
m_splitter->addWidget(m_editor);
|
||||
|
||||
const int treeWidth = m_settings->fileTreeWidth();
|
||||
m_splitter->setSizes({treeWidth, width() - treeWidth});
|
||||
m_splitter->setStretchFactor(0, 0);
|
||||
m_splitter->setStretchFactor(1, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Menüleiste
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::setupMenuBar()
|
||||
{
|
||||
// ---- Datei ----
|
||||
QMenu *mDatei = menuBar()->addMenu(tr("&Datei"));
|
||||
|
||||
m_actNewFile = mDatei->addAction(tr("&Neue Datei…"), this, &MainWindow::onNewFile);
|
||||
m_actNewFile->setShortcut(QKeySequence::New);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actOpenFile = mDatei->addAction(tr("Datei &öffnen…"), this, &MainWindow::onOpenFile);
|
||||
m_actOpenFile->setShortcut(QKeySequence::Open);
|
||||
|
||||
m_actOpenProject = mDatei->addAction(tr("&Projekt öffnen…"), this, &MainWindow::onOpenProject);
|
||||
m_actOpenProject->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O);
|
||||
|
||||
m_actClose = mDatei->addAction(tr("Projekt &schließen"), this, &MainWindow::onCloseProject);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actSave = mDatei->addAction(tr("&Speichern"), this, &MainWindow::onSave);
|
||||
m_actSave->setShortcut(QKeySequence::Save);
|
||||
|
||||
m_actSaveAs = mDatei->addAction(tr("Speichern &unter…"), this, &MainWindow::onSaveAs);
|
||||
m_actSaveAs->setShortcut(QKeySequence::SaveAs);
|
||||
|
||||
m_actSaveAll = mDatei->addAction(tr("&Alles speichern"), this, &MainWindow::onSaveAll);
|
||||
m_actSaveAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actQuit = mDatei->addAction(tr("&Beenden"), qApp, &QApplication::quit);
|
||||
m_actQuit->setShortcut(QKeySequence::Quit);
|
||||
|
||||
// ---- Bearbeiten ----
|
||||
QMenu *mBearbeiten = menuBar()->addMenu(tr("&Bearbeiten"));
|
||||
|
||||
m_actUndo = mBearbeiten->addAction(tr("&Rückgängig"), this, &MainWindow::onUndo);
|
||||
m_actUndo->setShortcut(QKeySequence::Undo);
|
||||
|
||||
m_actRedo = mBearbeiten->addAction(tr("&Wiederholen"), this, &MainWindow::onRedo);
|
||||
m_actRedo->setShortcut(QKeySequence::Redo);
|
||||
|
||||
mBearbeiten->addSeparator();
|
||||
|
||||
m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch);
|
||||
m_actSearch->setShortcut(QKeySequence::Find);
|
||||
|
||||
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
|
||||
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
|
||||
|
||||
mBearbeiten->addSeparator();
|
||||
|
||||
m_actFuncList = mBearbeiten->addAction(tr("&Projektfunktionen…"), this, &MainWindow::onShowFunctionList);
|
||||
m_actFuncList->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
|
||||
|
||||
m_actDeadCode = mBearbeiten->addAction(tr("&Tote Funktionen suchen…"), this, &MainWindow::onShowDeadCode);
|
||||
m_actDeadCode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_T);
|
||||
|
||||
// ---- Ansicht ----
|
||||
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
||||
|
||||
m_actDarkMode = mAnsicht->addAction(tr("&Dark Mode"));
|
||||
m_actDarkMode->setCheckable(true);
|
||||
m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D);
|
||||
connect(m_actDarkMode, &QAction::toggled, this, &MainWindow::onToggleDarkMode);
|
||||
|
||||
mAnsicht->addSeparator();
|
||||
|
||||
// Sprachauswahl
|
||||
QMenu *mSprache = mAnsicht->addMenu(tr("Sprache"));
|
||||
m_langGroup = new QActionGroup(this);
|
||||
m_langGroup->setExclusive(true);
|
||||
|
||||
const QSettings langSettings(QSettings::IniFormat, QSettings::UserScope,
|
||||
"BareCode", "BareCode");
|
||||
const QString currentLocale = langSettings.value("language/locale",
|
||||
QLocale::system().name()).toString();
|
||||
|
||||
struct LangEntry { QString locale; QString label; };
|
||||
const QList<LangEntry> languages = {
|
||||
{ "de", "Deutsch" },
|
||||
{ "en", "English" },
|
||||
};
|
||||
|
||||
for (const LangEntry &lang : languages)
|
||||
{
|
||||
QAction *act = mSprache->addAction(lang.label);
|
||||
act->setCheckable(true);
|
||||
act->setData(lang.locale);
|
||||
act->setChecked(currentLocale.startsWith(lang.locale));
|
||||
m_langGroup->addAction(act);
|
||||
connect(act, &QAction::triggered, this, [this, lang]()
|
||||
{
|
||||
onLanguageChanged(lang.locale);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Hilfe ----
|
||||
QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe"));
|
||||
m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout);
|
||||
}
|
||||
|
||||
void MainWindow::setupStatusBar()
|
||||
{
|
||||
statusBar()->showMessage(tr("Bereit"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Signale verbinden
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::connectSignals()
|
||||
{
|
||||
connect(m_projectManager.get(), &ProjectManager::projectOpened,
|
||||
this, &MainWindow::onProjectOpened);
|
||||
|
||||
connect(m_projectManager.get(), &ProjectManager::projectClosed,
|
||||
this, &MainWindow::onProjectClosed);
|
||||
|
||||
// Dateibaum → Editor
|
||||
connect(m_fileTree, &FileTreePanel::fileActivated,
|
||||
m_editor, &EditorPanel::openFile);
|
||||
|
||||
connect(m_fileTree, &FileTreePanel::fileCreated,
|
||||
m_editor, &EditorPanel::openFile);
|
||||
|
||||
// Gespeichert → Statusleiste
|
||||
connect(m_editor, &EditorPanel::currentFileSaved, this, [this](const QString &path)
|
||||
{
|
||||
statusBar()->showMessage(tr("Gespeichert: %1").arg(path), 3000);
|
||||
});
|
||||
|
||||
// Splitter-Breite merken
|
||||
connect(m_splitter, &QSplitter::splitterMoved, this, [this](int pos, int)
|
||||
{
|
||||
m_settings->setFileTreeWidth(pos);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme beim Start
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::applyInitialTheme()
|
||||
{
|
||||
const bool dark = m_settings->darkMode();
|
||||
// Block damit toggled-Signal nicht doppelt feuert
|
||||
m_actDarkMode->blockSignals(true);
|
||||
m_actDarkMode->setChecked(dark);
|
||||
m_actDarkMode->blockSignals(false);
|
||||
|
||||
m_themeManager->applyTheme(dark ? ThemeManager::Theme::Dark
|
||||
: ThemeManager::Theme::Light);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Datei
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onNewFile()
|
||||
{
|
||||
m_fileTree->triggerNewFile();
|
||||
}
|
||||
|
||||
void MainWindow::onOpenFile()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Datei öffnen"),
|
||||
m_settings->lastProjectPath()
|
||||
);
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
m_editor->openFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onOpenProject()
|
||||
{
|
||||
const QString path = QFileDialog::getExistingDirectory(
|
||||
this,
|
||||
tr("Projektverzeichnis öffnen"),
|
||||
m_settings->lastProjectPath()
|
||||
);
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
m_projectManager->openProject(path);
|
||||
m_settings->setLastProjectPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onCloseProject()
|
||||
{
|
||||
m_projectManager->closeProject();
|
||||
}
|
||||
|
||||
void MainWindow::onSave()
|
||||
{
|
||||
m_editor->saveCurrentFile();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveAs()
|
||||
{
|
||||
m_editor->saveCurrentFileAs();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveAll()
|
||||
{
|
||||
m_editor->saveAllFiles();
|
||||
statusBar()->showMessage(tr("Alle Dateien gespeichert"), 3000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Bearbeiten
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onUndo()
|
||||
{
|
||||
m_editor->undo();
|
||||
}
|
||||
|
||||
void MainWindow::onRedo()
|
||||
{
|
||||
m_editor->redo();
|
||||
}
|
||||
|
||||
void MainWindow::onShowSearch()
|
||||
{
|
||||
m_editor->showSearchPanel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Ansicht
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onToggleDarkMode(bool checked)
|
||||
{
|
||||
m_themeManager->applyTheme(checked ? ThemeManager::Theme::Dark
|
||||
: ThemeManager::Theme::Light);
|
||||
m_settings->setDarkMode(checked);
|
||||
}
|
||||
|
||||
void MainWindow::onLanguageChanged(const QString &locale)
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("language/locale", locale);
|
||||
|
||||
QMessageBox::information(this,
|
||||
tr("Sprache geändert"),
|
||||
tr("Die Sprache wird beim nächsten Start von BareCode aktiv.")
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Hilfe
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onAbout()
|
||||
{
|
||||
AboutDialog dlg(this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Projekt
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onShowDeadCode()
|
||||
{
|
||||
m_editor->showDeadCode();
|
||||
}
|
||||
|
||||
void MainWindow::onShowFunctionList()
|
||||
{
|
||||
m_editor->showFunctionList();
|
||||
}
|
||||
|
||||
void MainWindow::onShowFileSearch()
|
||||
{
|
||||
m_editor->showFileSearchPanel();
|
||||
}
|
||||
|
||||
void MainWindow::onProjectOpened(const QString &path)
|
||||
{
|
||||
setWindowTitle(QString("BareCode – %1").arg(path));
|
||||
m_fileTree->setRootPath(path);
|
||||
m_editor->setSearchRoot(path);
|
||||
statusBar()->showMessage(tr("Projekt geöffnet: %1").arg(path), 4000);
|
||||
}
|
||||
|
||||
void MainWindow::onProjectClosed()
|
||||
{
|
||||
setWindowTitle("BareCode");
|
||||
m_fileTree->clearRoot();
|
||||
m_editor->setSearchRoot(QString());
|
||||
statusBar()->showMessage(tr("Projekt geschlossen"), 3000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fenster-Zustand
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::saveWindowState()
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("window/geometry", saveGeometry());
|
||||
s.setValue("window/state", saveState());
|
||||
|
||||
// Session speichern
|
||||
m_settings->setLastOpenFiles(m_editor->openFilePaths());
|
||||
m_settings->setLastActiveFile(m_editor->activeFilePath());
|
||||
}
|
||||
|
||||
void MainWindow::restoreWindowState()
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
if (s.contains("window/geometry"))
|
||||
{
|
||||
restoreGeometry(s.value("window/geometry").toByteArray());
|
||||
}
|
||||
if (s.contains("window/state"))
|
||||
{
|
||||
restoreState(s.value("window/state").toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
saveWindowState();
|
||||
event->accept();
|
||||
}
|
||||
99
barecode/src/core/MainWindow.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QSplitter>
|
||||
#include <QMenuBar>
|
||||
#include <QStatusBar>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <memory>
|
||||
|
||||
#include "ProjectManager.h"
|
||||
#include "Settings.h"
|
||||
#include "ThemeManager.h"
|
||||
|
||||
class FileTreePanel;
|
||||
class EditorPanel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainWindow – Hauptfenster. Besitzt alle zentralen Dienste und das Layout.
|
||||
// ---------------------------------------------------------------------------
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
// Datei(en) öffnen, die beim Programmstart per Kommandozeile übergeben
|
||||
// wurden — z. B. durch Doppelklick auf eine Datei im Dateibrowser
|
||||
// ("Öffnen mit BareCode").
|
||||
void openFilesFromArguments(const QStringList &filePaths);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
// Datei
|
||||
void onNewFile();
|
||||
void onOpenFile();
|
||||
void onOpenProject();
|
||||
void onCloseProject();
|
||||
void onSave();
|
||||
void onSaveAs();
|
||||
void onSaveAll();
|
||||
// Bearbeiten
|
||||
void onUndo();
|
||||
void onRedo();
|
||||
void onShowSearch();
|
||||
void onShowFileSearch();
|
||||
void onShowFunctionList();
|
||||
void onShowDeadCode();
|
||||
// Ansicht
|
||||
void onToggleDarkMode(bool checked);
|
||||
void onLanguageChanged(const QString &locale);
|
||||
// Hilfe
|
||||
void onAbout();
|
||||
// Intern
|
||||
void onProjectOpened(const QString &path);
|
||||
void onProjectClosed();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void setupMenuBar();
|
||||
void setupStatusBar();
|
||||
void connectSignals();
|
||||
void applyInitialTheme();
|
||||
void saveWindowState();
|
||||
void restoreWindowState();
|
||||
|
||||
// Dienste
|
||||
std::unique_ptr<ProjectManager> m_projectManager;
|
||||
std::unique_ptr<Settings> m_settings;
|
||||
std::unique_ptr<ThemeManager> m_themeManager;
|
||||
|
||||
// Layout
|
||||
QSplitter *m_splitter = nullptr;
|
||||
FileTreePanel *m_fileTree = nullptr;
|
||||
EditorPanel *m_editor = nullptr;
|
||||
|
||||
// Aktionen
|
||||
QAction *m_actNewFile = nullptr;
|
||||
QAction *m_actOpenFile = nullptr;
|
||||
QAction *m_actOpenProject = nullptr;
|
||||
QAction *m_actClose = nullptr;
|
||||
QAction *m_actSave = nullptr;
|
||||
QAction *m_actSaveAs = nullptr;
|
||||
QAction *m_actSaveAll = nullptr;
|
||||
QAction *m_actQuit = nullptr;
|
||||
QAction *m_actUndo = nullptr;
|
||||
QAction *m_actRedo = nullptr;
|
||||
QAction *m_actSearch = nullptr;
|
||||
QAction *m_actFileSearch = nullptr;
|
||||
QAction *m_actFuncList = nullptr;
|
||||
QAction *m_actDeadCode = nullptr;
|
||||
QAction *m_actDarkMode = nullptr;
|
||||
QActionGroup *m_langGroup = nullptr;
|
||||
QAction *m_actAbout = nullptr;
|
||||
};
|
||||
33
barecode/src/core/ProjectManager.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "ProjectManager.h"
|
||||
|
||||
ProjectManager::ProjectManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString ProjectManager::currentProjectPath() const
|
||||
{
|
||||
return m_projectPath;
|
||||
}
|
||||
|
||||
void ProjectManager::openProject(const QString &path)
|
||||
{
|
||||
if (m_projectPath == path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_projectPath = path;
|
||||
emit projectOpened(m_projectPath);
|
||||
}
|
||||
|
||||
void ProjectManager::closeProject()
|
||||
{
|
||||
if (m_projectPath.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_projectPath.clear();
|
||||
emit projectClosed();
|
||||
}
|
||||
27
barecode/src/core/ProjectManager.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ProjectManager – Tracks the currently open project directory and emits
|
||||
// signals when the project changes so other components can react.
|
||||
// ---------------------------------------------------------------------------
|
||||
class ProjectManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectManager(QObject *parent = nullptr);
|
||||
|
||||
QString currentProjectPath() const;
|
||||
void openProject(const QString &path);
|
||||
void closeProject();
|
||||
|
||||
signals:
|
||||
void projectOpened(const QString &path);
|
||||
void projectClosed();
|
||||
|
||||
private:
|
||||
QString m_projectPath;
|
||||
};
|
||||
113
barecode/src/core/Settings.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "Settings.h"
|
||||
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode")
|
||||
{
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor font
|
||||
// ---------------------------------------------------------------------------
|
||||
QFont Settings::editorFont() const
|
||||
{
|
||||
QFont defaultFont("Monospace", 11);
|
||||
defaultFont.setStyleHint(QFont::Monospace);
|
||||
return m_settings.value("editor/font", defaultFont).value<QFont>();
|
||||
}
|
||||
|
||||
void Settings::setEditorFont(const QFont &font)
|
||||
{
|
||||
m_settings.setValue("editor/font", font);
|
||||
emit settingsChanged();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab size
|
||||
// ---------------------------------------------------------------------------
|
||||
int Settings::tabSize() const
|
||||
{
|
||||
return m_settings.value("editor/tabSize", 4).toInt();
|
||||
}
|
||||
|
||||
void Settings::setTabSize(int size)
|
||||
{
|
||||
m_settings.setValue("editor/tabSize", size);
|
||||
emit settingsChanged();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spaces vs. tabs
|
||||
// ---------------------------------------------------------------------------
|
||||
bool Settings::useSpacesForTabs() const
|
||||
{
|
||||
return m_settings.value("editor/useSpacesForTabs", true).toBool();
|
||||
}
|
||||
|
||||
void Settings::setUseSpacesForTabs(bool use)
|
||||
{
|
||||
m_settings.setValue("editor/useSpacesForTabs", use);
|
||||
emit settingsChanged();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File tree width
|
||||
// ---------------------------------------------------------------------------
|
||||
int Settings::fileTreeWidth() const
|
||||
{
|
||||
return m_settings.value("layout/fileTreeWidth", 240).toInt();
|
||||
}
|
||||
|
||||
void Settings::setFileTreeWidth(int width)
|
||||
{
|
||||
m_settings.setValue("layout/fileTreeWidth", width);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dark mode
|
||||
// ---------------------------------------------------------------------------
|
||||
bool Settings::darkMode() const
|
||||
{
|
||||
return m_settings.value("appearance/darkMode", false).toBool();
|
||||
}
|
||||
|
||||
void Settings::setDarkMode(bool dark)
|
||||
{
|
||||
m_settings.setValue("appearance/darkMode", dark);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Last project path
|
||||
// ---------------------------------------------------------------------------
|
||||
QString Settings::lastProjectPath() const
|
||||
{
|
||||
return m_settings.value("project/lastPath", QString()).toString();
|
||||
}
|
||||
|
||||
void Settings::setLastProjectPath(const QString &path)
|
||||
{
|
||||
m_settings.setValue("project/lastPath", path);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session – geöffnete Dateien
|
||||
// ---------------------------------------------------------------------------
|
||||
QStringList Settings::lastOpenFiles() const
|
||||
{
|
||||
return m_settings.value("session/openFiles", QStringList()).toStringList();
|
||||
}
|
||||
|
||||
void Settings::setLastOpenFiles(const QStringList &files)
|
||||
{
|
||||
m_settings.setValue("session/openFiles", files);
|
||||
}
|
||||
|
||||
QString Settings::lastActiveFile() const
|
||||
{
|
||||
return m_settings.value("session/activeFile", QString()).toString();
|
||||
}
|
||||
|
||||
void Settings::setLastActiveFile(const QString &file)
|
||||
{
|
||||
m_settings.setValue("session/activeFile", file);
|
||||
}
|
||||
52
barecode/src/core/Settings.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QFont>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings – Centralised persistent application settings.
|
||||
// ---------------------------------------------------------------------------
|
||||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = nullptr);
|
||||
|
||||
// Editor
|
||||
QFont editorFont() const;
|
||||
void setEditorFont(const QFont &font);
|
||||
|
||||
int tabSize() const;
|
||||
void setTabSize(int size);
|
||||
|
||||
bool useSpacesForTabs() const;
|
||||
void setUseSpacesForTabs(bool use);
|
||||
|
||||
// Layout
|
||||
int fileTreeWidth() const;
|
||||
void setFileTreeWidth(int width);
|
||||
|
||||
// Zuletzt geöffnete Dateien (Session-Wiederherstellung)
|
||||
QStringList lastOpenFiles() const;
|
||||
void setLastOpenFiles(const QStringList &files);
|
||||
|
||||
QString lastActiveFile() const;
|
||||
void setLastActiveFile(const QString &file);
|
||||
|
||||
// Recent
|
||||
QString lastProjectPath() const;
|
||||
void setLastProjectPath(const QString &path);
|
||||
|
||||
// Erscheinungsbild
|
||||
bool darkMode() const;
|
||||
void setDarkMode(bool dark);
|
||||
|
||||
signals:
|
||||
void settingsChanged();
|
||||
|
||||
private:
|
||||
QSettings m_settings;
|
||||
};
|
||||
115
barecode/src/core/ThemeManager.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include "ThemeManager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QStyleFactory>
|
||||
|
||||
ThemeManager::ThemeManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ThemeManager::applyTheme(Theme theme)
|
||||
{
|
||||
m_currentTheme = theme;
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
if (theme == Theme::Dark)
|
||||
{
|
||||
QApplication::setPalette(buildDarkPalette());
|
||||
}
|
||||
else
|
||||
{
|
||||
QApplication::setPalette(buildLightPalette());
|
||||
}
|
||||
|
||||
emit themeChanged(theme);
|
||||
}
|
||||
|
||||
ThemeManager::Theme ThemeManager::currentTheme() const
|
||||
{
|
||||
return m_currentTheme;
|
||||
}
|
||||
|
||||
QPalette ThemeManager::buildDarkPalette()
|
||||
{
|
||||
QPalette p;
|
||||
|
||||
const QColor bg = QColor("#1e1e1e");
|
||||
const QColor widget = QColor("#252526");
|
||||
const QColor alt = QColor("#2d2d30");
|
||||
const QColor hi = QColor("#264f78");
|
||||
const QColor hiText = QColor("#ffffff");
|
||||
const QColor text = QColor("#d4d4d4");
|
||||
const QColor disabled = QColor("#6d6d6d");
|
||||
const QColor btn = QColor("#3c3c3c");
|
||||
const QColor mid = QColor("#333333");
|
||||
const QColor dark = QColor("#1a1a1a");
|
||||
const QColor light = QColor("#454545");
|
||||
const QColor link = QColor("#569cd6");
|
||||
|
||||
p.setColor(QPalette::Window, bg);
|
||||
p.setColor(QPalette::WindowText, text);
|
||||
p.setColor(QPalette::Base, widget);
|
||||
p.setColor(QPalette::AlternateBase, alt);
|
||||
p.setColor(QPalette::Text, text);
|
||||
p.setColor(QPalette::Button, btn);
|
||||
p.setColor(QPalette::ButtonText, text);
|
||||
p.setColor(QPalette::Highlight, hi);
|
||||
p.setColor(QPalette::HighlightedText, hiText);
|
||||
p.setColor(QPalette::Link, link);
|
||||
p.setColor(QPalette::LinkVisited, link.darker(120));
|
||||
p.setColor(QPalette::Mid, mid);
|
||||
p.setColor(QPalette::Dark, dark);
|
||||
p.setColor(QPalette::Light, light);
|
||||
p.setColor(QPalette::Shadow, QColor("#000000"));
|
||||
p.setColor(QPalette::ToolTipBase, widget);
|
||||
p.setColor(QPalette::ToolTipText, text);
|
||||
p.setColor(QPalette::PlaceholderText, disabled);
|
||||
|
||||
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
|
||||
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
|
||||
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
QPalette ThemeManager::buildLightPalette()
|
||||
{
|
||||
// Fusion-Standard-Palette
|
||||
QPalette p;
|
||||
|
||||
const QColor bg = QColor("#f3f3f3");
|
||||
const QColor widget = QColor("#ffffff");
|
||||
const QColor alt = QColor("#e8e8e8");
|
||||
const QColor hi = QColor("#0078d4");
|
||||
const QColor hiText = QColor("#ffffff");
|
||||
const QColor text = QColor("#1e1e1e");
|
||||
const QColor disabled = QColor("#a0a0a0");
|
||||
const QColor btn = QColor("#e1e1e1");
|
||||
const QColor mid = QColor("#c8c8c8");
|
||||
const QColor dark = QColor("#a0a0a0");
|
||||
const QColor light = QColor("#ffffff");
|
||||
const QColor link = QColor("#0078d4");
|
||||
|
||||
p.setColor(QPalette::Window, bg);
|
||||
p.setColor(QPalette::WindowText, text);
|
||||
p.setColor(QPalette::Base, widget);
|
||||
p.setColor(QPalette::AlternateBase, alt);
|
||||
p.setColor(QPalette::Text, text);
|
||||
p.setColor(QPalette::Button, btn);
|
||||
p.setColor(QPalette::ButtonText, text);
|
||||
p.setColor(QPalette::Highlight, hi);
|
||||
p.setColor(QPalette::HighlightedText, hiText);
|
||||
p.setColor(QPalette::Link, link);
|
||||
p.setColor(QPalette::LinkVisited, link.darker(130));
|
||||
p.setColor(QPalette::Mid, mid);
|
||||
p.setColor(QPalette::Dark, dark);
|
||||
p.setColor(QPalette::Light, light);
|
||||
p.setColor(QPalette::PlaceholderText, disabled);
|
||||
|
||||
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
|
||||
p.setColor(QPalette::Disabled, QPalette::Text, disabled);
|
||||
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
|
||||
|
||||
return p;
|
||||
}
|
||||
29
barecode/src/core/ThemeManager.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPalette>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ThemeManager – Schaltet zwischen Hell- und Dunkelmodus um.
|
||||
// ---------------------------------------------------------------------------
|
||||
class ThemeManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Theme { Light, Dark };
|
||||
|
||||
explicit ThemeManager(QObject *parent = nullptr);
|
||||
|
||||
void applyTheme(Theme theme);
|
||||
Theme currentTheme() const;
|
||||
|
||||
signals:
|
||||
void themeChanged(Theme theme);
|
||||
|
||||
private:
|
||||
static QPalette buildDarkPalette();
|
||||
static QPalette buildLightPalette();
|
||||
|
||||
Theme m_currentTheme = Theme::Light;
|
||||
};
|
||||
49
barecode/src/editor/CMakeLists.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
set(EDITOR_SOURCES
|
||||
EditorPanel.cpp
|
||||
EditorPanel.h
|
||||
CodeEditor.cpp
|
||||
CodeEditor.h
|
||||
LineNumberArea.cpp
|
||||
LineNumberArea.h
|
||||
EditorTab.cpp
|
||||
EditorTab.h
|
||||
SearchPanel.cpp
|
||||
SearchPanel.h
|
||||
FileSearchPanel.cpp
|
||||
FileSearchPanel.h
|
||||
ColorIndicator.cpp
|
||||
ColorIndicator.h
|
||||
SignatureHelper.cpp
|
||||
SignatureHelper.h
|
||||
SignatureTooltip.cpp
|
||||
SignatureTooltip.h
|
||||
FunctionScanner.cpp
|
||||
FunctionScanner.h
|
||||
FunctionIndex.cpp
|
||||
FunctionIndex.h
|
||||
FunctionListPanel.cpp
|
||||
FunctionListPanel.h
|
||||
FunctionListDialog.cpp
|
||||
FunctionListDialog.h
|
||||
DeadCodeAnalyzer.cpp
|
||||
DeadCodeAnalyzer.h
|
||||
DeadCodeDialog.cpp
|
||||
DeadCodeDialog.h
|
||||
VariableCompleter.cpp
|
||||
VariableCompleter.h
|
||||
)
|
||||
|
||||
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
||||
|
||||
target_link_libraries(BareCode_Editor PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Concurrent
|
||||
BareCode_Highlighter
|
||||
)
|
||||
|
||||
target_include_directories(BareCode_Editor PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
)
|
||||
762
barecode/src/editor/CodeEditor.cpp
Normal file
@@ -0,0 +1,762 @@
|
||||
#include "CodeEditor.h"
|
||||
#include "LineNumberArea.h"
|
||||
#include "ColorIndicator.h"
|
||||
#include "SignatureHelper.h"
|
||||
#include "FunctionIndex.h"
|
||||
#include "VariableCompleter.h"
|
||||
|
||||
#include "core/Settings.h"
|
||||
#include "highlighter/HighlighterFactory.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTextBlock>
|
||||
#include <QPaintEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QFocusEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QFileInfo>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
CodeEditor::CodeEditor(Settings *settings, QWidget *parent)
|
||||
: QPlainTextEdit(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
m_lineNumberArea = new LineNumberArea(this);
|
||||
m_colorIndicator = new ColorIndicator(this);
|
||||
m_signatureHelper = new SignatureHelper(this);
|
||||
m_varCompleter = new VariableCompleter(this);
|
||||
setupEditor();
|
||||
|
||||
connect(this, &CodeEditor::blockCountChanged,
|
||||
this, &CodeEditor::updateLineNumberAreaWidth);
|
||||
|
||||
connect(this, &CodeEditor::updateRequest,
|
||||
this, &CodeEditor::updateLineNumberArea);
|
||||
|
||||
connect(this, &CodeEditor::cursorPositionChanged,
|
||||
this, &CodeEditor::highlightCurrentLine);
|
||||
|
||||
updateLineNumberAreaWidth(0);
|
||||
highlightCurrentLine();
|
||||
}
|
||||
|
||||
CodeEditor::~CodeEditor() = default;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::setupEditor()
|
||||
{
|
||||
applySettings();
|
||||
setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
}
|
||||
|
||||
void CodeEditor::applySettings()
|
||||
{
|
||||
setFont(m_settings->editorFont());
|
||||
|
||||
const int tabStop = m_settings->tabSize();
|
||||
// Set tab stop width in pixels using font metrics
|
||||
QFontMetrics fm(m_settings->editorFont());
|
||||
setTabStopDistance(static_cast<qreal>(tabStop) * fm.horizontalAdvance(' '));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File I/O
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::loadFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Open File"),
|
||||
tr("Cannot open file:\n%1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
m_filePath = filePath;
|
||||
|
||||
QTextStream in(&file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
in.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
in.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
setPlainText(in.readAll());
|
||||
document()->setModified(false);
|
||||
|
||||
installHighlighter(filePath);
|
||||
}
|
||||
|
||||
QString CodeEditor::filePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
bool CodeEditor::save()
|
||||
{
|
||||
if (m_filePath.isEmpty())
|
||||
{
|
||||
return saveAs();
|
||||
}
|
||||
|
||||
return writeToFile(m_filePath);
|
||||
}
|
||||
|
||||
bool CodeEditor::saveAs()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Speichern unter"),
|
||||
m_filePath
|
||||
);
|
||||
|
||||
if (path.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_filePath = path;
|
||||
installHighlighter(m_filePath);
|
||||
return writeToFile(m_filePath);
|
||||
}
|
||||
|
||||
bool CodeEditor::writeToFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Speichern"),
|
||||
tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath));
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
out.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
out.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
out << toPlainText();
|
||||
document()->setModified(false);
|
||||
emit fileSaved(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CodeEditor::installHighlighter(const QString &filePath)
|
||||
{
|
||||
// Remove old highlighter first
|
||||
delete m_highlighter;
|
||||
m_highlighter = nullptr;
|
||||
|
||||
m_highlighter = HighlighterFactory::createForFile(filePath, document());
|
||||
}
|
||||
|
||||
bool CodeEditor::isModified() const
|
||||
{
|
||||
return document()->isModified();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Line number area
|
||||
// ---------------------------------------------------------------------------
|
||||
int CodeEditor::lineNumberAreaWidth() const
|
||||
{
|
||||
int digits = 1;
|
||||
int max = qMax(1, blockCount());
|
||||
while (max >= 10)
|
||||
{
|
||||
max /= 10;
|
||||
++digits;
|
||||
}
|
||||
|
||||
const int padding = 8;
|
||||
return fontMetrics().horizontalAdvance('9') * digits + padding * 2;
|
||||
}
|
||||
|
||||
void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/)
|
||||
{
|
||||
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
||||
}
|
||||
|
||||
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||||
{
|
||||
if (dy != 0)
|
||||
{
|
||||
m_lineNumberArea->scroll(0, dy);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
|
||||
}
|
||||
|
||||
if (rect.contains(viewport()->rect()))
|
||||
{
|
||||
updateLineNumberAreaWidth(0);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeEditor::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
// Zuerst den normalen Editor-Inhalt zeichnen
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
|
||||
// Einrück-Führungslinien
|
||||
const int tabSize = m_settings->tabSize();
|
||||
if (tabSize > 0)
|
||||
{
|
||||
QPainter painter(viewport());
|
||||
|
||||
QColor guideColor = palette().color(QPalette::Text);
|
||||
guideColor.setAlpha(30);
|
||||
painter.setPen(QPen(guideColor, 1, Qt::SolidLine));
|
||||
|
||||
const QFontMetrics fm(font());
|
||||
const int spaceWidth = fm.horizontalAdvance(' ');
|
||||
const int tabPixels = tabSize * spaceWidth;
|
||||
|
||||
if (tabPixels > 0)
|
||||
{
|
||||
int textOriginX = 0;
|
||||
{
|
||||
QTextBlock firstBlock = firstVisibleBlock();
|
||||
if (!firstBlock.isValid())
|
||||
{
|
||||
firstBlock = document()->begin();
|
||||
}
|
||||
if (firstBlock.isValid())
|
||||
{
|
||||
const QRectF blockRect = blockBoundingGeometry(firstBlock)
|
||||
.translated(contentOffset());
|
||||
const QTextLayout *layout = firstBlock.layout();
|
||||
if (layout && layout->lineCount() > 0)
|
||||
{
|
||||
textOriginX = static_cast<int>(blockRect.left()
|
||||
+ layout->lineAt(0).position().x());
|
||||
}
|
||||
else
|
||||
{
|
||||
textOriginX = static_cast<int>(blockRect.left());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int scrollX = horizontalScrollBar()->value();
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
const int bottom = event->rect().bottom();
|
||||
|
||||
while (block.isValid())
|
||||
{
|
||||
const QRectF blockRect = blockBoundingGeometry(block)
|
||||
.translated(contentOffset());
|
||||
if (blockRect.top() > bottom) { break; }
|
||||
|
||||
if (block.isVisible())
|
||||
{
|
||||
const QString text = block.text();
|
||||
int indentSpaces = 0;
|
||||
for (const QChar &ch : text)
|
||||
{
|
||||
if (ch == ' ') { ++indentSpaces; }
|
||||
else if (ch == '\t') { indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize; }
|
||||
else { break; }
|
||||
}
|
||||
|
||||
const int indentStops = indentSpaces / tabSize;
|
||||
for (int stop = 1; stop <= indentStops; ++stop)
|
||||
{
|
||||
const int xPixel = textOriginX + stop * tabPixels - scrollX;
|
||||
if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
painter.drawLine(xPixel,
|
||||
static_cast<int>(blockRect.top()),
|
||||
xPixel,
|
||||
static_cast<int>(blockRect.bottom()));
|
||||
}
|
||||
}
|
||||
block = block.next();
|
||||
}
|
||||
}
|
||||
|
||||
// Farbvorschau-Quadrate zeichnen
|
||||
m_colorIndicator->paint(painter, event);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeEditor::setFunctionIndex(FunctionIndex *index)
|
||||
{
|
||||
m_functionIndex = index;
|
||||
}
|
||||
|
||||
void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
// Zuerst normales Verhalten — markiert das Wort unter dem Cursor
|
||||
QPlainTextEdit::mouseDoubleClickEvent(event);
|
||||
|
||||
if (!m_functionIndex || !m_functionIndex->isReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Markiertes Wort auslesen
|
||||
const QString word = textCursor().selectedText().trimmed();
|
||||
if (word.isEmpty() || word.contains(' '))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Im Funktionsindex nachschlagen
|
||||
const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word);
|
||||
if (info.filePath.isEmpty())
|
||||
{
|
||||
return; // Nicht gefunden — normales Verhalten bleibt
|
||||
}
|
||||
|
||||
// Nicht zur eigenen Definition springen wenn wir bereits dort sind
|
||||
if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
emit navigateToRequested(info.filePath, info.line);
|
||||
}
|
||||
|
||||
void CodeEditor::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
// Zuerst prüfen ob ein Farbquadrat geklickt wurde
|
||||
if (m_colorIndicator->handleMousePress(event))
|
||||
{
|
||||
return;
|
||||
}
|
||||
QPlainTextEdit::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void CodeEditor::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QPlainTextEdit::resizeEvent(event);
|
||||
|
||||
const QRect cr = contentsRect();
|
||||
m_lineNumberArea->setGeometry(
|
||||
QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())
|
||||
);
|
||||
}
|
||||
|
||||
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(m_lineNumberArea);
|
||||
|
||||
// Background
|
||||
const QColor bgColor = palette().color(QPalette::Window).darker(110);
|
||||
painter.fillRect(event->rect(), bgColor);
|
||||
|
||||
const QColor lineNumColor = palette().color(QPalette::Mid);
|
||||
const QColor activeColor = palette().color(QPalette::Text);
|
||||
|
||||
const int currentLine = textCursor().blockNumber();
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
int blockNumber = block.blockNumber();
|
||||
int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
|
||||
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||||
|
||||
while (block.isValid() && top <= event->rect().bottom())
|
||||
{
|
||||
if (block.isVisible() && bottom >= event->rect().top())
|
||||
{
|
||||
const QString number = QString::number(blockNumber + 1);
|
||||
painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor);
|
||||
painter.drawText(
|
||||
0,
|
||||
top,
|
||||
m_lineNumberArea->width() - 4,
|
||||
fontMetrics().height(),
|
||||
Qt::AlignRight,
|
||||
number
|
||||
);
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
top = bottom;
|
||||
bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||||
++blockNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Current line highlight + Klammerzugehörigkeit
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::highlightCurrentLine()
|
||||
{
|
||||
QList<QTextEdit::ExtraSelection> extraSelections;
|
||||
|
||||
if (!isReadOnly())
|
||||
{
|
||||
// Aktuelle Zeile hervorheben
|
||||
QTextEdit::ExtraSelection lineSelection;
|
||||
lineSelection.format.setBackground(palette().color(QPalette::AlternateBase));
|
||||
lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
lineSelection.cursor = textCursor();
|
||||
lineSelection.cursor.clearSelection();
|
||||
extraSelections.append(lineSelection);
|
||||
|
||||
// Klammerzugehörigkeit
|
||||
matchBrackets(extraSelections);
|
||||
}
|
||||
|
||||
setExtraSelections(extraSelections);
|
||||
}
|
||||
|
||||
void CodeEditor::matchBrackets(QList<QTextEdit::ExtraSelection> &selections)
|
||||
{
|
||||
static const QString openBrackets = "({[";
|
||||
static const QString closeBrackets = ")}]";
|
||||
|
||||
QTextCursor cursor = textCursor();
|
||||
const QString blockText = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
// Zeichen unter oder links vom Cursor prüfen
|
||||
QChar ch;
|
||||
int charPos = -1;
|
||||
|
||||
// Zuerst Zeichen unter dem Cursor
|
||||
if (col < blockText.length())
|
||||
{
|
||||
ch = blockText[col];
|
||||
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
|
||||
{
|
||||
charPos = col;
|
||||
}
|
||||
}
|
||||
|
||||
// Dann Zeichen links vom Cursor
|
||||
if (charPos == -1 && col > 0)
|
||||
{
|
||||
ch = blockText[col - 1];
|
||||
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
|
||||
{
|
||||
charPos = col - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (charPos == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Passende Klammer suchen
|
||||
const bool isOpen = openBrackets.contains(ch);
|
||||
const int bracketIndex = isOpen
|
||||
? openBrackets.indexOf(ch)
|
||||
: closeBrackets.indexOf(ch);
|
||||
const QChar matchChar = isOpen
|
||||
? closeBrackets[bracketIndex]
|
||||
: openBrackets[bracketIndex];
|
||||
|
||||
// Dokumentposition der gefundenen Klammer
|
||||
const int startPos = cursor.block().position() + charPos;
|
||||
|
||||
// Passende Klammer suchen — vorwärts oder rückwärts
|
||||
int depth = 0;
|
||||
int matchPos = -1;
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
// Vorwärts suchen
|
||||
QTextCursor search(document());
|
||||
search.setPosition(startPos);
|
||||
|
||||
while (!search.atEnd())
|
||||
{
|
||||
search.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
const QChar c = search.selectedText()[0];
|
||||
search.clearSelection();
|
||||
|
||||
if (c == ch) { ++depth; }
|
||||
else if (c == matchChar)
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
matchPos = search.position() - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rückwärts suchen
|
||||
QTextCursor search(document());
|
||||
search.setPosition(startPos + 1);
|
||||
|
||||
while (search.position() > 0)
|
||||
{
|
||||
search.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
||||
const QChar c = search.selectedText()[0];
|
||||
search.clearSelection();
|
||||
|
||||
if (c == ch) { ++depth; }
|
||||
else if (c == matchChar)
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
matchPos = search.position();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formatierung
|
||||
QTextCharFormat matchFormat;
|
||||
if (matchPos >= 0)
|
||||
{
|
||||
// Gefunden — beide Klammern grün hervorheben
|
||||
matchFormat.setBackground(QColor("#1a5a1a"));
|
||||
matchFormat.setForeground(QColor("#88ff88"));
|
||||
matchFormat.setFontWeight(QFont::Bold);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Kein Match — rot markieren
|
||||
matchFormat.setBackground(QColor("#5a1a1a"));
|
||||
matchFormat.setForeground(QColor("#ff8888"));
|
||||
matchFormat.setFontWeight(QFont::Bold);
|
||||
}
|
||||
|
||||
// Öffnende / schließende Klammer markieren
|
||||
QTextEdit::ExtraSelection sel1;
|
||||
sel1.cursor = QTextCursor(document());
|
||||
sel1.cursor.setPosition(startPos);
|
||||
sel1.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
sel1.format = matchFormat;
|
||||
selections.append(sel1);
|
||||
|
||||
// Passende Klammer markieren (nur wenn gefunden)
|
||||
if (matchPos >= 0)
|
||||
{
|
||||
QTextEdit::ExtraSelection sel2;
|
||||
sel2.cursor = QTextCursor(document());
|
||||
sel2.cursor.setPosition(matchPos);
|
||||
sel2.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
sel2.format = matchFormat;
|
||||
selections.append(sel2);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
const int tabSize = m_settings->tabSize();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shift+Tab: Einrückung zurückziehen
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Backtab ||
|
||||
(event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier))
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||
|
||||
// Wenn die Selektion genau am Anfang des letzten Blocks endet,
|
||||
// diesen Block nicht mit einbeziehen — der Cursor steht dort nur
|
||||
// mit Position 0, der Nutzer hat die Zeile nicht markiert
|
||||
if (cursor.hasSelection() &&
|
||||
cursor.selectionEnd() == endBlock.position() &&
|
||||
endBlock != startBlock)
|
||||
{
|
||||
endBlock = endBlock.previous();
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
const QString lineText = b.text();
|
||||
int toRemove = 0;
|
||||
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
|
||||
{
|
||||
if (lineText[i] == ' ') { ++toRemove; }
|
||||
else { break; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!lineText.isEmpty() && lineText[0] == '\t')
|
||||
{
|
||||
toRemove = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove > 0)
|
||||
{
|
||||
QTextCursor lineCursor(b);
|
||||
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
lineCursor.movePosition(QTextCursor::Right,
|
||||
QTextCursor::KeepAnchor,
|
||||
toRemove);
|
||||
lineCursor.removeSelectedText();
|
||||
}
|
||||
}
|
||||
cursor.endEditBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Tab: Einrücken (Leerzeichen oder echter Tab)
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Tab)
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
if (cursor.hasSelection())
|
||||
{
|
||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||
|
||||
// Gleiche Korrektur: Cursor am Zeilenanfang → Zeile nicht einrücken
|
||||
if (cursor.selectionEnd() == endBlock.position() &&
|
||||
endBlock != startBlock)
|
||||
{
|
||||
endBlock = endBlock.previous();
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
QTextCursor lineCursor(b);
|
||||
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
lineCursor.insertText(QString(tabSize, ' '));
|
||||
}
|
||||
else
|
||||
{
|
||||
lineCursor.insertText("\t");
|
||||
}
|
||||
}
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
// Zum nächsten Tab-Stop auffüllen
|
||||
const int col = cursor.columnNumber();
|
||||
const int spacesNeeded = tabSize - (col % tabSize);
|
||||
cursor.insertText(QString(spacesNeeded, ' '));
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor.insertText("\t");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Smart Backspace: springt zur vorherigen Einrückungsstufe
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Backspace
|
||||
&& !textCursor().hasSelection()
|
||||
&& m_settings->useSpacesForTabs())
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
if (col > 0)
|
||||
{
|
||||
// Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen
|
||||
const QString lineText = cursor.block().text();
|
||||
const QString leftOfCursor = lineText.left(col);
|
||||
const bool onlySpaces = leftOfCursor.trimmed().isEmpty();
|
||||
|
||||
if (onlySpaces && col > 0)
|
||||
{
|
||||
// Zur vorherigen Tab-Stop-Position springen
|
||||
const int targetCol = ((col - 1) / tabSize) * tabSize;
|
||||
const int toDelete = col - targetCol;
|
||||
|
||||
cursor.movePosition(QTextCursor::Left,
|
||||
QTextCursor::KeepAnchor,
|
||||
toDelete);
|
||||
cursor.removeSelectedText();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Enter / Return: Auto-Indent
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
const QString currentLine = cursor.block().text();
|
||||
|
||||
// Führende Leerzeichen der aktuellen Zeile zählen
|
||||
int leadingSpaces = 0;
|
||||
for (const QChar &ch : currentLine)
|
||||
{
|
||||
if (ch == ' ')
|
||||
{
|
||||
++leadingSpaces;
|
||||
}
|
||||
else if (ch == '\t')
|
||||
{
|
||||
leadingSpaces += tabSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
|
||||
if (leadingSpaces > 0)
|
||||
{
|
||||
const QString indent = m_settings->useSpacesForTabs()
|
||||
? QString(leadingSpaces, ' ')
|
||||
: QString(leadingSpaces / tabSize, '\t');
|
||||
textCursor().insertText(indent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
|
||||
// Variablen-Popup nach jedem Tastendruck aktualisieren
|
||||
m_varCompleter->handleKeyPress(event);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fokus verloren — z. B. beim Wechsel zu einem anderen Tab.
|
||||
// Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst
|
||||
// bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen
|
||||
// Dateien den Fokus blockieren.
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::focusOutEvent(QFocusEvent *event)
|
||||
{
|
||||
QPlainTextEdit::focusOutEvent(event);
|
||||
m_varCompleter->notifyFocusLost();
|
||||
}
|
||||
|
||||
86
barecode/src/editor/CodeEditor.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QFont>
|
||||
#include <QString>
|
||||
#include <QTextBlock>
|
||||
|
||||
class LineNumberArea;
|
||||
class Settings;
|
||||
class SyntaxHighlighter;
|
||||
class ColorIndicator;
|
||||
class SignatureHelper;
|
||||
class FunctionIndex;
|
||||
class VariableCompleter;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CodeEditor – Core editing widget.
|
||||
// Features:
|
||||
// • Line number gutter
|
||||
// • Current-line highlight
|
||||
// • Auto-indent on Enter
|
||||
// • Tab → spaces (configurable)
|
||||
// • Syntax highlighting (via pluggable SyntaxHighlighter)
|
||||
// ---------------------------------------------------------------------------
|
||||
class CodeEditor : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CodeEditor(Settings *settings, QWidget *parent = nullptr);
|
||||
~CodeEditor() override;
|
||||
|
||||
void loadFile(const QString &filePath);
|
||||
void applySettings();
|
||||
|
||||
// Speichern
|
||||
bool save();
|
||||
bool saveAs();
|
||||
|
||||
// Called by LineNumberArea
|
||||
int lineNumberAreaWidth() const;
|
||||
void lineNumberAreaPaintEvent(QPaintEvent *event);
|
||||
|
||||
QString filePath() const;
|
||||
bool isModified() const;
|
||||
|
||||
// Öffentliche Hilfsmethoden für ColorIndicator
|
||||
// (die Qt-Originale sind protected und von außen nicht erreichbar)
|
||||
QTextBlock firstVisibleBlockPublic() const { return firstVisibleBlock(); }
|
||||
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
|
||||
QPointF contentOffsetPublic() const { return contentOffset(); }
|
||||
|
||||
void setFunctionIndex(FunctionIndex *index);
|
||||
|
||||
signals:
|
||||
void fileSaved(const QString &filePath);
|
||||
void navigateToRequested(const QString &filePath, int line);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
void focusOutEvent(QFocusEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void updateLineNumberAreaWidth(int newBlockCount);
|
||||
void highlightCurrentLine();
|
||||
void updateLineNumberArea(const QRect &rect, int dy);
|
||||
|
||||
private:
|
||||
void setupEditor();
|
||||
void installHighlighter(const QString &filePath);
|
||||
bool writeToFile(const QString &filePath);
|
||||
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
|
||||
|
||||
Settings *m_settings = nullptr;
|
||||
LineNumberArea *m_lineNumberArea = nullptr;
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
ColorIndicator *m_colorIndicator = nullptr;
|
||||
SignatureHelper *m_signatureHelper = nullptr;
|
||||
FunctionIndex *m_functionIndex = nullptr;
|
||||
VariableCompleter *m_varCompleter = nullptr;
|
||||
QString m_filePath;
|
||||
};
|
||||
361
barecode/src/editor/ColorIndicator.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
#include "ColorIndicator.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QColorDialog>
|
||||
#include <QTextBlock>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
#include <QScrollBar>
|
||||
#include <QHash>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kombinierter Regex — erfasst alle CSS-Farbformate in einer Runde
|
||||
// ---------------------------------------------------------------------------
|
||||
const QRegularExpression ColorIndicator::s_colorRegex(
|
||||
// #rgb / #rrggbb / #rrggbbaa
|
||||
R"(#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?=[^0-9A-Fa-f]|$))"
|
||||
R"(|rgba?\s*\([^)]+\))"
|
||||
R"(|hsla?\s*\([^)]+\))",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
ColorIndicator::ColorIndicator(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Cache invalidieren und Viewport neu zeichnen wenn sich der Text ändert
|
||||
connect(m_editor->document(), &QTextDocument::contentsChanged,
|
||||
this, [this]()
|
||||
{
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
m_editor->viewport()->update();
|
||||
});
|
||||
|
||||
// Auch beim Scrollen neu zeichnen (Cache bleibt gültig, nur Position ändert sich)
|
||||
connect(m_editor->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this]()
|
||||
{
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paint – wird aus CodeEditor::paintEvent aufgerufen
|
||||
// ---------------------------------------------------------------------------
|
||||
void ColorIndicator::paint(QPainter &painter, QPaintEvent *event)
|
||||
{
|
||||
rebuildCache();
|
||||
|
||||
const int squareSize = m_editor->fontMetrics().height() - 4;
|
||||
const int radius = 2;
|
||||
|
||||
for (const ColorMatch &m : m_cache)
|
||||
{
|
||||
if (!event->rect().intersects(m.rect))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rahmen
|
||||
painter.setPen(QColor(0, 0, 0, 80));
|
||||
painter.setBrush(m.color);
|
||||
painter.drawRoundedRect(m.rect, radius, radius);
|
||||
|
||||
// Schachbrettmuster als Hintergrund für transparente Farben
|
||||
if (m.color.alpha() < 255)
|
||||
{
|
||||
const int half = squareSize / 2;
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(180, 180, 180));
|
||||
painter.drawRect(m.rect.x(), m.rect.y(), half, half);
|
||||
painter.drawRect(m.rect.x() + half, m.rect.y() + half, half, half);
|
||||
painter.setBrush(m.color);
|
||||
painter.drawRoundedRect(m.rect, radius, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mouse – wird aus CodeEditor::mousePressEvent aufgerufen
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ColorIndicator::handleMousePress(QMouseEvent *event)
|
||||
{
|
||||
for (const ColorMatch &m : m_cache)
|
||||
{
|
||||
if (!m.rect.contains(event->pos()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Farb-Dialog öffnen
|
||||
QColorDialog dlg(m.color, m_editor);
|
||||
dlg.setOption(QColorDialog::ShowAlphaChannel, true);
|
||||
dlg.setWindowTitle(QObject::tr("Farbe wählen"));
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const QColor newColor = dlg.selectedColor();
|
||||
|
||||
// Ursprünglichen Farbwert im Dokument ersetzen
|
||||
QTextBlock block = m_editor->document()->findBlockByNumber(m.blockNumber);
|
||||
if (!block.isValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Neuen Farbwert als Hex-String formatieren
|
||||
QString newValue;
|
||||
if (newColor.alpha() < 255)
|
||||
{
|
||||
newValue = newColor.name(QColor::HexArgb); // #aarrggbb
|
||||
// CSS erwartet #rrggbbaa — Bytes umstellen
|
||||
// Qt liefert #aarrggbb, CSS will #rrggbbaa
|
||||
newValue = QString("#%1%2%3%4")
|
||||
.arg(newColor.red(), 2, 16, QChar('0'))
|
||||
.arg(newColor.green(), 2, 16, QChar('0'))
|
||||
.arg(newColor.blue(), 2, 16, QChar('0'))
|
||||
.arg(newColor.alpha(), 2, 16, QChar('0'));
|
||||
}
|
||||
else
|
||||
{
|
||||
newValue = newColor.name(QColor::HexRgb); // #rrggbb
|
||||
}
|
||||
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(block.position() + m.posInBlock);
|
||||
cursor.setPosition(block.position() + m.posInBlock + m.length,
|
||||
QTextCursor::KeepAnchor);
|
||||
cursor.insertText(newValue);
|
||||
|
||||
// Cache invalidieren
|
||||
m_cache.clear();
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache aufbauen – nur für sichtbare Blöcke
|
||||
// ---------------------------------------------------------------------------
|
||||
void ColorIndicator::rebuildCache()
|
||||
{
|
||||
QTextBlock firstVisible = m_editor->firstVisibleBlockPublic();
|
||||
const int firstNum = firstVisible.blockNumber();
|
||||
|
||||
// Letzten sichtbaren Block bestimmen
|
||||
int lastNum = firstNum;
|
||||
{
|
||||
QTextBlock b = firstVisible;
|
||||
const int bot = m_editor->viewport()->height();
|
||||
while (b.isValid())
|
||||
{
|
||||
const QRectF r = m_editor->blockBoundingGeometryPublic(b)
|
||||
.translated(m_editor->contentOffsetPublic());
|
||||
if (r.top() > bot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
lastNum = b.blockNumber();
|
||||
b = b.next();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache noch aktuell?
|
||||
if (firstNum == m_cacheFirstBlock && lastNum == m_cacheLastBlock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_cache.clear();
|
||||
m_cacheFirstBlock = firstNum;
|
||||
m_cacheLastBlock = lastNum;
|
||||
|
||||
const int squareSize = m_editor->fontMetrics().height() - 4;
|
||||
const int scrollX = m_editor->horizontalScrollBar()->value();
|
||||
|
||||
QTextBlock block = firstVisible;
|
||||
while (block.isValid() && block.blockNumber() <= lastNum)
|
||||
{
|
||||
const QRectF blockRect = m_editor->blockBoundingGeometryPublic(block)
|
||||
.translated(m_editor->contentOffsetPublic());
|
||||
|
||||
const QList<ColorMatch> found = findColorsInBlock(block.text(),
|
||||
block.blockNumber());
|
||||
for (ColorMatch m : found)
|
||||
{
|
||||
// X-Position des Farbwerts im Viewport berechnen
|
||||
const QTextLayout *layout = block.layout();
|
||||
if (!layout || layout->lineCount() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const QTextLine line = layout->lineAt(0);
|
||||
// Position nach dem Ende des Farbwerts
|
||||
const qreal endCharX = line.cursorToX(m.posInBlock + m.length);
|
||||
const int x = static_cast<int>(blockRect.left() + endCharX)
|
||||
- scrollX + 3;
|
||||
|
||||
if (x + squareSize > m_editor->viewport()->width())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int y = static_cast<int>(blockRect.top())
|
||||
+ (static_cast<int>(blockRect.height()) - squareSize) / 2;
|
||||
|
||||
m.rect = QRect(x, y, squareSize, squareSize);
|
||||
m_cache.append(m);
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Farbwerte in einer Zeile suchen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<ColorIndicator::ColorMatch> ColorIndicator::findColorsInBlock(
|
||||
const QString &text, int blockNumber) const
|
||||
{
|
||||
QList<ColorMatch> result;
|
||||
|
||||
QRegularExpressionMatchIterator it = s_colorRegex.globalMatch(text);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
const QString token = match.captured(0);
|
||||
const QColor color = parseColor(token);
|
||||
|
||||
if (!color.isValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ColorMatch m;
|
||||
m.blockNumber = blockNumber;
|
||||
m.posInBlock = static_cast<int>(match.capturedStart());
|
||||
m.length = static_cast<int>(match.capturedLength());
|
||||
m.color = color;
|
||||
result.append(m);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Farb-Parser
|
||||
// ---------------------------------------------------------------------------
|
||||
QColor ColorIndicator::parseColor(const QString &token)
|
||||
{
|
||||
const QString t = token.trimmed();
|
||||
|
||||
if (t.startsWith('#')) { return parseHex(t); }
|
||||
if (t.startsWith("rgba", Qt::CaseInsensitive)) { return parseRgba(t); }
|
||||
if (t.startsWith("rgb", Qt::CaseInsensitive)) { return parseRgb(t); }
|
||||
if (t.startsWith("hsla", Qt::CaseInsensitive)) { return parseHsla(t); }
|
||||
if (t.startsWith("hsl", Qt::CaseInsensitive)) { return parseHsl(t); }
|
||||
|
||||
return QColor();
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHex(const QString &s)
|
||||
{
|
||||
// #rgb → #rrggbb
|
||||
if (s.length() == 4)
|
||||
{
|
||||
return QColor(QString("#%1%1%2%2%3%3")
|
||||
.arg(s[1]).arg(s[2]).arg(s[3]));
|
||||
}
|
||||
// #rrggbb
|
||||
if (s.length() == 7)
|
||||
{
|
||||
return QColor(s);
|
||||
}
|
||||
// #rrggbbaa (CSS) → Qt braucht #aarrggbb
|
||||
if (s.length() == 9)
|
||||
{
|
||||
const QString rr = s.mid(1, 2);
|
||||
const QString gg = s.mid(3, 2);
|
||||
const QString bb = s.mid(5, 2);
|
||||
const QString aa = s.mid(7, 2);
|
||||
return QColor(QString("#%1%2%3%4").arg(aa, rr, gg, bb));
|
||||
}
|
||||
return QColor();
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseRgb(const QString &s)
|
||||
{
|
||||
// rgb(r, g, b)
|
||||
static const QRegularExpression re(
|
||||
R"(rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
return QColor(m.captured(1).toInt(),
|
||||
m.captured(2).toInt(),
|
||||
m.captured(3).toInt());
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseRgba(const QString &s)
|
||||
{
|
||||
// rgba(r, g, b, a) — a ist 0.0–1.0
|
||||
static const QRegularExpression re(
|
||||
R"(rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9.]+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
return QColor(m.captured(1).toInt(),
|
||||
m.captured(2).toInt(),
|
||||
m.captured(3).toInt(),
|
||||
qRound(m.captured(4).toDouble() * 255.0));
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHsl(const QString &s)
|
||||
{
|
||||
// hsl(h, s%, l%)
|
||||
static const QRegularExpression re(
|
||||
R"(hsl\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
QColor c;
|
||||
c.setHsl(m.captured(1).toInt(),
|
||||
qRound(m.captured(2).toInt() * 2.55),
|
||||
qRound(m.captured(3).toInt() * 2.55));
|
||||
return c;
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHsla(const QString &s)
|
||||
{
|
||||
// hsla(h, s%, l%, a)
|
||||
static const QRegularExpression re(
|
||||
R"(hsla\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([0-9.]+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
QColor c;
|
||||
c.setHsl(m.captured(1).toInt(),
|
||||
qRound(m.captured(2).toInt() * 2.55),
|
||||
qRound(m.captured(3).toInt() * 2.55),
|
||||
qRound(m.captured(4).toDouble() * 255.0));
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
|
||||
67
barecode/src/editor/ColorIndicator.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QColor>
|
||||
#include <QRect>
|
||||
#include <QList>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
class CodeEditor;
|
||||
class QPainter;
|
||||
class QPaintEvent;
|
||||
class QMouseEvent;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ColorIndicator – Zeichnet kleine Farbquadrate neben CSS-Farbwerten und
|
||||
// öffnet einen QColorDialog wenn der Nutzer darauf klickt.
|
||||
//
|
||||
// Unterstützte Formate:
|
||||
// #rgb #rrggbb #rrggbbaa
|
||||
// rgb(r, g, b) rgba(r, g, b, a)
|
||||
// hsl(h, s%, l%) hsla(h, s%, l%, a)
|
||||
// 140 benannte CSS-Farben (red, blue, cornflowerblue, ...)
|
||||
// ---------------------------------------------------------------------------
|
||||
class ColorIndicator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ColorIndicator(CodeEditor *editor);
|
||||
|
||||
// Wird aus CodeEditor::paintEvent aufgerufen
|
||||
void paint(QPainter &painter, QPaintEvent *event);
|
||||
|
||||
// Wird aus CodeEditor::mousePressEvent aufgerufen
|
||||
// Gibt true zurück wenn der Klick auf einem Farbquadrat war
|
||||
bool handleMousePress(QMouseEvent *event);
|
||||
|
||||
private:
|
||||
struct ColorMatch
|
||||
{
|
||||
QRect rect; // Position des Quadrats im Viewport
|
||||
QColor color; // Erkannte Farbe
|
||||
int blockNumber;
|
||||
int posInBlock; // Zeichenposition des Farbwerts im Block
|
||||
int length; // Länge des Farbwerts im Text
|
||||
};
|
||||
|
||||
void rebuildCache();
|
||||
QList<ColorMatch> findColorsInBlock(const QString &text,
|
||||
int blockNumber) const;
|
||||
|
||||
static QColor parseColor(const QString &token);
|
||||
static QColor parseHex(const QString &s);
|
||||
static QColor parseRgb(const QString &s);
|
||||
static QColor parseRgba(const QString &s);
|
||||
static QColor parseHsl(const QString &s);
|
||||
static QColor parseHsla(const QString &s);
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
QList<ColorMatch> m_cache;
|
||||
int m_cacheFirstBlock = -1;
|
||||
int m_cacheLastBlock = -1;
|
||||
|
||||
// Kombinierter Regex für alle Farbformate
|
||||
static const QRegularExpression s_colorRegex;
|
||||
};
|
||||
181
barecode/src/editor/DeadCodeAnalyzer.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "DeadCodeAnalyzer.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QFileInfo>
|
||||
|
||||
DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
}
|
||||
|
||||
QList<DeadCodeAnalyzer::DeadFunction> DeadCodeAnalyzer::analyze(
|
||||
const QString &projectRoot,
|
||||
const QStringList &extensions,
|
||||
const QStringList &excludeDirs) const
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 1: Alle Dateien sammeln (mit Exclude-Filter)
|
||||
// -----------------------------------------------------------------------
|
||||
QStringList allFiles;
|
||||
QDirIterator it(projectRoot, extensions, QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString path = it.next();
|
||||
|
||||
// Exclude-Verzeichnisse prüfen
|
||||
bool excluded = false;
|
||||
if (!excludeDirs.isEmpty())
|
||||
{
|
||||
const QStringList parts = path.split('/');
|
||||
for (const QString &excl : excludeDirs)
|
||||
{
|
||||
if (parts.contains(excl.trimmed(), Qt::CaseInsensitive))
|
||||
{
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!excluded)
|
||||
{
|
||||
allFiles.append(path);
|
||||
}
|
||||
}
|
||||
|
||||
const int totalFiles = allFiles.size();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 2: Alle Funktionsdefinitionen aus nicht-excluded Dateien
|
||||
// -----------------------------------------------------------------------
|
||||
QHash<QString, FunctionScanner::FunctionInfo> definitions;
|
||||
|
||||
for (const QString &path : allFiles)
|
||||
{
|
||||
const QList<FunctionScanner::FunctionInfo> funcs = m_scanner->scanFile(path);
|
||||
for (const FunctionScanner::FunctionInfo &func : funcs)
|
||||
{
|
||||
if (func.name.startsWith("__"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const QString key = func.name.toLower();
|
||||
if (!definitions.contains(key))
|
||||
{
|
||||
definitions.insert(key, func);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (definitions.isEmpty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 3: Alle Dateien EINMAL lesen, alle Aufrufe sammeln
|
||||
// -----------------------------------------------------------------------
|
||||
QSet<QString> calledFunctions;
|
||||
|
||||
static const QRegularExpression identifierRegex(
|
||||
R"((?:->|::|\b)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()"
|
||||
R"(|['"]([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)['"])"
|
||||
);
|
||||
|
||||
static const QRegularExpression defLineRegex(
|
||||
R"(\bfunction\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))"
|
||||
);
|
||||
|
||||
int filesScanned = 0;
|
||||
|
||||
for (const QString &filePath : allFiles)
|
||||
{
|
||||
++filesScanned;
|
||||
|
||||
// Fortschritt signalisieren
|
||||
emit progressUpdate(filesScanned, totalFiles,
|
||||
QFileInfo(filePath).fileName());
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
const QString line = stream.readLine();
|
||||
const QString trimmed = line.trimmed();
|
||||
|
||||
if (trimmed.startsWith("//") ||
|
||||
trimmed.startsWith("*") ||
|
||||
trimmed.startsWith("#") ||
|
||||
trimmed.startsWith("/*"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Definitionen auf dieser Zeile nicht als Aufruf werten
|
||||
QSet<QString> definedOnThisLine;
|
||||
{
|
||||
QRegularExpressionMatchIterator dit = defLineRegex.globalMatch(line);
|
||||
while (dit.hasNext())
|
||||
{
|
||||
definedOnThisLine.insert(dit.next().captured(1).toLower());
|
||||
}
|
||||
}
|
||||
|
||||
QRegularExpressionMatchIterator mit = identifierRegex.globalMatch(line);
|
||||
while (mit.hasNext())
|
||||
{
|
||||
const QRegularExpressionMatch match = mit.next();
|
||||
const QString name = match.captured(1).isEmpty()
|
||||
? match.captured(2).toLower()
|
||||
: match.captured(1).toLower();
|
||||
|
||||
if (name.isEmpty() || definedOnThisLine.contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (definitions.contains(name))
|
||||
{
|
||||
calledFunctions.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 4: Nicht aufgerufene Funktionen zurückgeben
|
||||
// -----------------------------------------------------------------------
|
||||
QList<DeadFunction> dead;
|
||||
|
||||
for (auto it2 = definitions.begin(); it2 != definitions.end(); ++it2)
|
||||
{
|
||||
if (!calledFunctions.contains(it2.key()))
|
||||
{
|
||||
const FunctionScanner::FunctionInfo &func = it2.value();
|
||||
DeadFunction d;
|
||||
d.info = func;
|
||||
d.reason = func.className.isEmpty()
|
||||
? QObject::tr("Globale Funktion — kein Aufruf gefunden")
|
||||
: QObject::tr("Methode von %1 — kein Aufruf gefunden")
|
||||
.arg(func.className);
|
||||
dead.append(d);
|
||||
}
|
||||
}
|
||||
|
||||
return dead;
|
||||
}
|
||||
39
barecode/src/editor/DeadCodeAnalyzer.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeadCodeAnalyzer – Findet Funktionen die definiert aber nie aufgerufen werden.
|
||||
// ---------------------------------------------------------------------------
|
||||
class DeadCodeAnalyzer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct DeadFunction
|
||||
{
|
||||
FunctionScanner::FunctionInfo info;
|
||||
QString reason;
|
||||
};
|
||||
|
||||
explicit DeadCodeAnalyzer(QObject *parent = nullptr);
|
||||
|
||||
// excludeDirs: Verzeichnisnamen die komplett übersprungen werden
|
||||
// z.B. {"vendor", "lib", "node_modules"}
|
||||
QList<DeadFunction> analyze(const QString &projectRoot,
|
||||
const QStringList &extensions = {"*.php"},
|
||||
const QStringList &excludeDirs = {}) const;
|
||||
|
||||
signals:
|
||||
// Fortschritt während der Analyse (läuft im Thread)
|
||||
void progressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤tFile) const;
|
||||
|
||||
private:
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
};
|
||||
461
barecode/src/editor/DeadCodeDialog.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
#include "DeadCodeDialog.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFont>
|
||||
#include <QSettings>
|
||||
#include <QCloseEvent>
|
||||
#include <QFileDialog>
|
||||
#include <QTextStream>
|
||||
#include <QMessageBox>
|
||||
#include <QHeaderView>
|
||||
#include <QDateTime>
|
||||
#include <QMetaObject>
|
||||
|
||||
DeadCodeDialog::DeadCodeDialog(QWidget *parent)
|
||||
: QDialog(parent, Qt::Window)
|
||||
{
|
||||
setWindowTitle(tr("Tote Funktionen – BareCode"));
|
||||
setMinimumSize(650, 500);
|
||||
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
if (s.contains("deadcode/geometry"))
|
||||
{
|
||||
restoreGeometry(s.value("deadcode/geometry").toByteArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
resize(780, 620);
|
||||
}
|
||||
|
||||
m_analyzer = new DeadCodeAnalyzer(this);
|
||||
m_watcher = new QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>::finished,
|
||||
this, &DeadCodeDialog::onAnalysisFinished);
|
||||
|
||||
// Fortschritts-Signal aus dem Analyzer-Thread sicher in den UI-Thread leiten
|
||||
connect(m_analyzer, &DeadCodeAnalyzer::progressUpdate,
|
||||
this, &DeadCodeDialog::onProgressUpdate,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setSpacing(8);
|
||||
root->setContentsMargins(12, 12, 12, 12);
|
||||
|
||||
// ---- Warnbox ----
|
||||
QFrame *warningBox = new QFrame(this);
|
||||
warningBox->setStyleSheet(
|
||||
"QFrame {"
|
||||
" background: #3a2a00;"
|
||||
" border: 1px solid #7a5a00;"
|
||||
" border-radius: 4px;"
|
||||
"}"
|
||||
);
|
||||
QHBoxLayout *warnLayout = new QHBoxLayout(warningBox);
|
||||
warnLayout->setContentsMargins(8, 6, 8, 6);
|
||||
|
||||
QLabel *warnIcon = new QLabel(warningBox);
|
||||
warnIcon->setFixedSize(20, 20);
|
||||
warnIcon->setStyleSheet(
|
||||
"background: #ffcc00;"
|
||||
"color: #1a1a00;"
|
||||
"font-weight: bold;"
|
||||
"font-size: 13px;"
|
||||
"border-radius: 10px;"
|
||||
);
|
||||
warnIcon->setText("!");
|
||||
warnIcon->setAlignment(Qt::AlignCenter);
|
||||
warnLayout->addWidget(warnIcon);
|
||||
|
||||
QLabel *warnText = new QLabel(
|
||||
tr("Kandidatenliste — kein Aufruf per Regex gefunden. "
|
||||
"Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. "
|
||||
"Bitte vor dem Löschen manuell prüfen."),
|
||||
warningBox
|
||||
);
|
||||
warnText->setWordWrap(true);
|
||||
warnText->setStyleSheet("color: #ffcc88;");
|
||||
warnLayout->addWidget(warnText, 1);
|
||||
root->addWidget(warningBox);
|
||||
|
||||
// ---- Exclude-Verzeichnisse ----
|
||||
QHBoxLayout *excludeRow = new QHBoxLayout();
|
||||
excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this));
|
||||
|
||||
m_excludeEdit = new QLineEdit(this);
|
||||
m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)"));
|
||||
m_excludeEdit->setToolTip(tr(
|
||||
"Verzeichnisnamen die bei der Analyse übersprungen werden.\n"
|
||||
"Leerzeichen-getrennt, z.B.: vendor lib node_modules cache"
|
||||
));
|
||||
|
||||
// Letzte Einstellung wiederherstellen
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
m_excludeEdit->setText(s.value("deadcode/excludeDirs", "vendor lib node_modules").toString());
|
||||
|
||||
excludeRow->addWidget(m_excludeEdit, 1);
|
||||
|
||||
// Verzeichnis-Auswahl Button
|
||||
QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this);
|
||||
btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen"));
|
||||
connect(btnBrowse, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
const QString startPath = m_projectRoot.isEmpty()
|
||||
? QDir::homePath()
|
||||
: m_projectRoot;
|
||||
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this,
|
||||
tr("Verzeichnis ausschließen"),
|
||||
startPath,
|
||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
|
||||
);
|
||||
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad
|
||||
// So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse
|
||||
const QString dirName = QFileInfo(dir).fileName();
|
||||
|
||||
// Prüfen ob bereits in der Liste
|
||||
const QStringList current = m_excludeEdit->text()
|
||||
.simplified()
|
||||
.split(' ', Qt::SkipEmptyParts);
|
||||
if (current.contains(dirName, Qt::CaseInsensitive))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Anhängen
|
||||
QString newText = m_excludeEdit->text().trimmed();
|
||||
if (!newText.isEmpty())
|
||||
{
|
||||
newText += ' ';
|
||||
}
|
||||
newText += dirName;
|
||||
m_excludeEdit->setText(newText);
|
||||
});
|
||||
excludeRow->addWidget(btnBrowse);
|
||||
|
||||
// Löschen-Button — ganzes Feld leeren
|
||||
QPushButton *btnClear = new QPushButton(tr("✕"), this);
|
||||
btnClear->setFixedWidth(28);
|
||||
btnClear->setFlat(true);
|
||||
btnClear->setToolTip(tr("Ausschlussliste leeren"));
|
||||
connect(btnClear, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
m_excludeEdit->clear();
|
||||
});
|
||||
excludeRow->addWidget(btnClear);
|
||||
|
||||
root->addLayout(excludeRow);
|
||||
|
||||
// ---- Steuerleiste ----
|
||||
QHBoxLayout *ctrlRow = new QHBoxLayout();
|
||||
|
||||
m_btnAnalyze = new QPushButton(tr("Analyse starten"), this);
|
||||
ctrlRow->addWidget(m_btnAnalyze);
|
||||
|
||||
m_btnExport = new QPushButton(tr("Als TXT exportieren"), this);
|
||||
m_btnExport->setEnabled(false);
|
||||
ctrlRow->addWidget(m_btnExport);
|
||||
|
||||
ctrlRow->addStretch();
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
ctrlRow->addWidget(m_statusLabel);
|
||||
|
||||
root->addLayout(ctrlRow);
|
||||
|
||||
// ---- Fortschrittsbereich ----
|
||||
m_progress = new QProgressBar(this);
|
||||
m_progress->setRange(0, 100);
|
||||
m_progress->setTextVisible(true);
|
||||
m_progress->setFormat("%v / %m Dateien");
|
||||
m_progress->hide();
|
||||
root->addWidget(m_progress);
|
||||
|
||||
m_progressLabel = new QLabel(this);
|
||||
m_progressLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
|
||||
m_progressLabel->hide();
|
||||
root->addWidget(m_progressLabel);
|
||||
|
||||
// ---- Ergebnistabelle ----
|
||||
m_results = new QTreeWidget(this);
|
||||
m_results->setColumnCount(4);
|
||||
m_results->setHeaderLabels({
|
||||
tr("Funktion"),
|
||||
tr("Klasse"),
|
||||
tr("Datei"),
|
||||
tr("Zeile")
|
||||
});
|
||||
m_results->setRootIsDecorated(false);
|
||||
m_results->setAlternatingRowColors(true);
|
||||
m_results->setSortingEnabled(true);
|
||||
m_results->sortByColumn(0, Qt::AscendingOrder);
|
||||
m_results->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
|
||||
m_results->header()->setSectionResizeMode(0, QHeaderView::Interactive);
|
||||
m_results->header()->setSectionResizeMode(1, QHeaderView::Interactive);
|
||||
m_results->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_results->header()->setSectionResizeMode(3, QHeaderView::Fixed);
|
||||
m_results->header()->resizeSection(0, 200);
|
||||
m_results->header()->resizeSection(1, 120);
|
||||
m_results->header()->resizeSection(3, 60);
|
||||
|
||||
QFont mono("Monospace");
|
||||
mono.setStyleHint(QFont::Monospace);
|
||||
m_results->setFont(mono);
|
||||
|
||||
root->addWidget(m_results, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnAnalyze, &QPushButton::clicked, this, &DeadCodeDialog::onAnalyze);
|
||||
connect(m_btnExport, &QPushButton::clicked, this, &DeadCodeDialog::onExportClicked);
|
||||
connect(m_results, &QTreeWidget::itemActivated,
|
||||
this, &DeadCodeDialog::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::setProjectRoot(const QString &path)
|
||||
{
|
||||
// Alte Exclude-Liste für das vorherige Projekt speichern
|
||||
if (!m_projectRoot.isEmpty())
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString oldKey = "deadcode/exclude/" +
|
||||
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
|
||||
s.setValue(oldKey, m_excludeEdit->text());
|
||||
}
|
||||
|
||||
m_projectRoot = path;
|
||||
m_results->clear();
|
||||
m_statusLabel->setText(QString());
|
||||
m_btnExport->setEnabled(false);
|
||||
|
||||
// Exclude-Liste für das neue Projekt laden
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString key = "deadcode/exclude/" +
|
||||
QString(path).replace('/', '_').replace('\\', '_');
|
||||
|
||||
// Projektspezifisch vorhanden? Sonst globalen Fallback nehmen
|
||||
const QString saved = s.value(key,
|
||||
s.value("deadcode/excludeDirs", "vendor lib node_modules").toString()
|
||||
).toString();
|
||||
|
||||
m_excludeEdit->setText(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analyse starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onAnalyze()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
QMessageBox::information(this, tr("Analyse"),
|
||||
tr("Bitte zuerst ein Projekt öffnen."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
// Exclude-Liste projektspezifisch speichern
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString key = "deadcode/exclude/" +
|
||||
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
|
||||
s.setValue(key, m_excludeEdit->text());
|
||||
s.setValue("deadcode/excludeDirs", m_excludeEdit->text()); // globaler Fallback
|
||||
|
||||
const QStringList excludeDirs = m_excludeEdit->text()
|
||||
.simplified()
|
||||
.split(' ', Qt::SkipEmptyParts);
|
||||
|
||||
m_results->clear();
|
||||
m_btnAnalyze->setEnabled(false);
|
||||
m_btnExport->setEnabled(false);
|
||||
m_progress->setValue(0);
|
||||
m_progress->setMaximum(0); // Unbestimmter Modus bis wir die Dateizahl kennen
|
||||
m_progress->show();
|
||||
m_progressLabel->setText(tr("Dateien werden gezählt…"));
|
||||
m_progressLabel->show();
|
||||
m_statusLabel->setText(tr("Analysiere…"));
|
||||
|
||||
const QString root = m_projectRoot;
|
||||
DeadCodeAnalyzer *analyzer = m_analyzer;
|
||||
|
||||
QFuture<QList<DeadCodeAnalyzer::DeadFunction>> future =
|
||||
QtConcurrent::run([analyzer, root, excludeDirs]()
|
||||
{
|
||||
return analyzer->analyze(root, {"*.php"}, excludeDirs);
|
||||
});
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fortschritt aktualisieren (QueuedConnection — Thread-sicher)
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onProgressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤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<DeadCodeAnalyzer::DeadFunction> dead = m_watcher->result();
|
||||
|
||||
m_results->setSortingEnabled(false);
|
||||
|
||||
for (const DeadCodeAnalyzer::DeadFunction &d : dead)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(m_results);
|
||||
item->setText(0, d.info.signature);
|
||||
item->setText(1, d.info.className.isEmpty() ? tr("(global)") : d.info.className);
|
||||
item->setText(2, QFileInfo(d.info.filePath).fileName());
|
||||
item->setText(3, QString::number(d.info.line));
|
||||
item->setToolTip(0, d.reason);
|
||||
item->setToolTip(2, d.info.filePath);
|
||||
item->setData(0, Qt::UserRole, d.info.filePath);
|
||||
item->setData(0, Qt::UserRole + 1, d.info.line);
|
||||
|
||||
if (d.info.className.isEmpty())
|
||||
{
|
||||
item->setForeground(1, QColor("#888888"));
|
||||
}
|
||||
}
|
||||
|
||||
m_results->setSortingEnabled(true);
|
||||
m_results->sortByColumn(0, Qt::AscendingOrder);
|
||||
|
||||
if (dead.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("✓ Keine ungenutzten Funktionen gefunden."));
|
||||
m_statusLabel->setStyleSheet("color: #44bb44;");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Kandidaten gefunden").arg(dead.size()));
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
m_btnExport->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick auf Treffer
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onItemActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
if (!path.isEmpty() && line > 0)
|
||||
{
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onExportClicked()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Ergebnis exportieren"),
|
||||
QString("barecode_deadcode_%1.txt")
|
||||
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmm")),
|
||||
tr("Textdateien (*.txt)")
|
||||
);
|
||||
if (path.isEmpty()) { return; }
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Export"),
|
||||
tr("Datei konnte nicht geschrieben werden:\n%1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
out.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
out << "BareCode – Analyse ungenutzter Funktionen\n";
|
||||
out << "Projekt: " << m_projectRoot << "\n";
|
||||
out << "Ausgeschlossen: " << m_excludeEdit->text() << "\n";
|
||||
out << "Datum: " << QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm") << "\n";
|
||||
out << QString("─").repeated(70) << "\n\n";
|
||||
|
||||
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
||||
{
|
||||
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
||||
out << item->text(0) << "\n";
|
||||
out << " Klasse: " << item->text(1) << "\n";
|
||||
out << " Datei: " << item->toolTip(2) << "\n";
|
||||
out << " Zeile: " << item->text(3) << "\n\n";
|
||||
}
|
||||
|
||||
out << QString("─").repeated(70) << "\n";
|
||||
out << m_results->topLevelItemCount() << " Kandidaten\n";
|
||||
out << "Hinweis: Dynamische Aufrufe werden nicht erkannt.\n";
|
||||
|
||||
QMessageBox::information(this, tr("Export"),
|
||||
tr("Exportiert nach:\n%1").arg(path));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fenstergeometrie speichern
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
}
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("deadcode/geometry", saveGeometry());
|
||||
event->accept();
|
||||
}
|
||||
62
barecode/src/editor/DeadCodeDialog.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
|
||||
#include "DeadCodeAnalyzer.h"
|
||||
|
||||
class DeadCodeDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeadCodeDialog(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onAnalyze();
|
||||
void onAnalysisFinished();
|
||||
void onProgressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤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<QList<DeadCodeAnalyzer::DeadFunction>> *m_watcher = nullptr;
|
||||
};
|
||||
301
barecode/src/editor/EditorPanel.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "EditorPanel.h"
|
||||
#include "EditorTab.h"
|
||||
#include "CodeEditor.h"
|
||||
#include "SearchPanel.h"
|
||||
#include "FileSearchPanel.h"
|
||||
#include "FunctionListDialog.h"
|
||||
#include "DeadCodeDialog.h"
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
#include <QTextDocument>
|
||||
#include <QTextBlock>
|
||||
|
||||
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::setupUi()
|
||||
{
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(0);
|
||||
|
||||
m_tabWidget = new QTabWidget(this);
|
||||
m_tabWidget->setTabsClosable(true);
|
||||
m_tabWidget->setMovable(true);
|
||||
m_tabWidget->setDocumentMode(true);
|
||||
|
||||
m_searchPanel = new SearchPanel(this);
|
||||
m_fileSearch = new FileSearchPanel(this);
|
||||
m_funcDialog = new FunctionListDialog(window());
|
||||
m_deadCode = new DeadCodeDialog(window());
|
||||
m_funcIndex = new FunctionIndex(this);
|
||||
|
||||
m_layout->addWidget(m_tabWidget, 1);
|
||||
m_layout->addWidget(m_searchPanel, 0);
|
||||
m_layout->addWidget(m_fileSearch, 0);
|
||||
|
||||
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
|
||||
this, &EditorPanel::onTabCloseRequested);
|
||||
|
||||
connect(m_tabWidget, &QTabWidget::currentChanged,
|
||||
this, &EditorPanel::onCurrentTabChanged);
|
||||
|
||||
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_funcDialog, &FunctionListDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_deadCode, &DeadCodeDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hilfsmethoden
|
||||
// ---------------------------------------------------------------------------
|
||||
EditorTab *EditorPanel::currentTab() const
|
||||
{
|
||||
return qobject_cast<EditorTab *>(m_tabWidget->currentWidget());
|
||||
}
|
||||
|
||||
int EditorPanel::findTabForFile(const QString &filePath) const
|
||||
{
|
||||
EditorTab *tab = m_openTabs.value(filePath, nullptr);
|
||||
return tab ? m_tabWidget->indexOf(tab) : -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::openFile(const QString &filePath)
|
||||
{
|
||||
const int existing = findTabForFile(filePath);
|
||||
if (existing != -1)
|
||||
{
|
||||
m_tabWidget->setCurrentIndex(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget);
|
||||
const int index = m_tabWidget->addTab(tab, tab->fileName());
|
||||
m_tabWidget->setCurrentIndex(index);
|
||||
m_tabWidget->setTabToolTip(index, filePath);
|
||||
m_openTabs.insert(filePath, tab);
|
||||
|
||||
// FunctionIndex dem Editor mitgeben für Doppelklick-Navigation
|
||||
tab->editor()->setFunctionIndex(m_funcIndex);
|
||||
|
||||
// Doppelklick auf Funktion → zur Definition springen
|
||||
connect(tab->editor(), &CodeEditor::navigateToRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
// Änderungsindikator im Tab-Titel (● = ungespeichert)
|
||||
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
|
||||
this, [this, tab](bool modified)
|
||||
{
|
||||
const int idx = m_tabWidget->indexOf(tab);
|
||||
if (idx == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QString name = QFileInfo(tab->filePath()).fileName();
|
||||
m_tabWidget->setTabText(idx, modified ? "● " + name : name);
|
||||
});
|
||||
|
||||
// Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt)
|
||||
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
|
||||
{
|
||||
const int idx = m_tabWidget->indexOf(tab);
|
||||
if (idx != -1)
|
||||
{
|
||||
m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName());
|
||||
m_tabWidget->setTabToolTip(idx, savedPath);
|
||||
}
|
||||
emit currentFileSaved(savedPath);
|
||||
|
||||
// Index und Funktionsliste aktualisieren
|
||||
m_funcIndex->refresh();
|
||||
if (m_funcDialog->isVisible())
|
||||
{
|
||||
m_funcDialog->refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EditorPanel::saveCurrentFile()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->save();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::saveCurrentFileAs()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->saveAs();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::saveAllFiles()
|
||||
{
|
||||
for (int i = 0; i < m_tabWidget->count(); ++i)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
|
||||
if (tab && tab->isModified())
|
||||
{
|
||||
tab->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::showSearchPanel()
|
||||
{
|
||||
m_fileSearch->hide();
|
||||
m_searchPanel->activate();
|
||||
}
|
||||
|
||||
void EditorPanel::showFileSearchPanel()
|
||||
{
|
||||
m_searchPanel->hide();
|
||||
m_fileSearch->activate();
|
||||
}
|
||||
|
||||
void EditorPanel::showFunctionList()
|
||||
{
|
||||
m_funcDialog->show();
|
||||
m_funcDialog->raise();
|
||||
m_funcDialog->activateWindow();
|
||||
}
|
||||
|
||||
void EditorPanel::showDeadCode()
|
||||
{
|
||||
m_deadCode->show();
|
||||
m_deadCode->raise();
|
||||
m_deadCode->activateWindow();
|
||||
}
|
||||
|
||||
void EditorPanel::setSearchRoot(const QString &path)
|
||||
{
|
||||
m_fileSearch->setSearchRoot(path);
|
||||
m_funcDialog->setProjectRoot(path);
|
||||
m_deadCode->setProjectRoot(path);
|
||||
m_funcIndex->setProjectRoot(path);
|
||||
}
|
||||
|
||||
void EditorPanel::goToLine(const QString &filePath, int line)
|
||||
{
|
||||
// Datei öffnen falls noch nicht geöffnet
|
||||
openFile(filePath);
|
||||
|
||||
EditorTab *tab = m_openTabs.value(filePath, nullptr);
|
||||
if (!tab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_tabWidget->setCurrentWidget(tab);
|
||||
|
||||
// Zur gewünschten Zeile springen
|
||||
CodeEditor *editor = tab->editor();
|
||||
QTextBlock block = editor->document()->findBlockByLineNumber(line - 1);
|
||||
if (block.isValid())
|
||||
{
|
||||
QTextCursor cursor(block);
|
||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||
editor->setTextCursor(cursor);
|
||||
editor->centerCursor();
|
||||
editor->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList EditorPanel::openFilePaths() const
|
||||
{
|
||||
QStringList paths;
|
||||
for (int i = 0; i < m_tabWidget->count(); ++i)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
|
||||
if (tab)
|
||||
{
|
||||
paths.append(tab->filePath());
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
QString EditorPanel::activeFilePath() const
|
||||
{
|
||||
EditorTab *tab = currentTab();
|
||||
return tab ? tab->filePath() : QString();
|
||||
}
|
||||
|
||||
void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile)
|
||||
{
|
||||
for (const QString &path : files)
|
||||
{
|
||||
if (QFile::exists(path))
|
||||
{
|
||||
openFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Aktiven Tab wiederherstellen
|
||||
if (!activeFile.isEmpty())
|
||||
{
|
||||
const int idx = findTabForFile(activeFile);
|
||||
if (idx != -1)
|
||||
{
|
||||
m_tabWidget->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::undo()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->editor()->undo();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::redo()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->editor()->redo();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::onTabCloseRequested(int index)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(index));
|
||||
if (!tab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_openTabs.remove(tab->filePath());
|
||||
m_tabWidget->removeTab(index);
|
||||
tab->deleteLater();
|
||||
|
||||
m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr);
|
||||
}
|
||||
|
||||
void EditorPanel::onCurrentTabChanged(int /*index*/)
|
||||
{
|
||||
EditorTab *tab = currentTab();
|
||||
m_searchPanel->setEditor(tab ? tab->editor() : nullptr);
|
||||
}
|
||||
68
barecode/src/editor/EditorPanel.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
class EditorTab;
|
||||
class Settings;
|
||||
class SearchPanel;
|
||||
class FileSearchPanel;
|
||||
class FunctionListDialog;
|
||||
class DeadCodeDialog;
|
||||
class FunctionIndex;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
||||
// ---------------------------------------------------------------------------
|
||||
class EditorPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EditorPanel(Settings *settings, QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void openFile(const QString &filePath);
|
||||
void saveCurrentFile();
|
||||
void saveCurrentFileAs();
|
||||
void saveAllFiles();
|
||||
void setSearchRoot(const QString &path);
|
||||
void showSearchPanel();
|
||||
void showFileSearchPanel();
|
||||
void showFunctionList();
|
||||
void showDeadCode();
|
||||
void goToLine(const QString &filePath, int line);
|
||||
|
||||
// Session
|
||||
QStringList openFilePaths() const;
|
||||
QString activeFilePath() const;
|
||||
void restoreSession(const QStringList &files, const QString &activeFile);
|
||||
void undo();
|
||||
void redo();
|
||||
|
||||
signals:
|
||||
void currentFileSaved(const QString &filePath);
|
||||
|
||||
private slots:
|
||||
void onTabCloseRequested(int index);
|
||||
void onCurrentTabChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
int findTabForFile(const QString &filePath) const;
|
||||
EditorTab *currentTab() const;
|
||||
|
||||
Settings *m_settings = nullptr;
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QTabWidget *m_tabWidget = nullptr;
|
||||
SearchPanel *m_searchPanel = nullptr;
|
||||
FileSearchPanel *m_fileSearch = nullptr;
|
||||
FunctionListDialog *m_funcDialog = nullptr;
|
||||
DeadCodeDialog *m_deadCode = nullptr;
|
||||
FunctionIndex *m_funcIndex = nullptr;
|
||||
|
||||
QHash<QString, EditorTab *> m_openTabs;
|
||||
};
|
||||
53
barecode/src/editor/EditorTab.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "EditorTab.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_filePath(filePath)
|
||||
{
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(0);
|
||||
|
||||
m_editor = new CodeEditor(settings, this);
|
||||
m_editor->loadFile(filePath);
|
||||
|
||||
m_layout->addWidget(m_editor);
|
||||
}
|
||||
|
||||
QString EditorTab::filePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
QString EditorTab::fileName() const
|
||||
{
|
||||
return QFileInfo(m_filePath).fileName();
|
||||
}
|
||||
|
||||
CodeEditor *EditorTab::editor() const
|
||||
{
|
||||
return m_editor;
|
||||
}
|
||||
|
||||
bool EditorTab::isModified() const
|
||||
{
|
||||
return m_editor->isModified();
|
||||
}
|
||||
|
||||
bool EditorTab::save()
|
||||
{
|
||||
const bool ok = m_editor->save();
|
||||
// Path may have changed if this was an untitled buffer saved for the first time
|
||||
m_filePath = m_editor->filePath();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool EditorTab::saveAs()
|
||||
{
|
||||
const bool ok = m_editor->saveAs();
|
||||
m_filePath = m_editor->filePath();
|
||||
return ok;
|
||||
}
|
||||
33
barecode/src/editor/EditorTab.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class CodeEditor;
|
||||
class Settings;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorTab – Widget placed inside each tab of the tab bar.
|
||||
// Owns a CodeEditor for a single file.
|
||||
// ---------------------------------------------------------------------------
|
||||
class EditorTab : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr);
|
||||
|
||||
QString filePath() const;
|
||||
QString fileName() const;
|
||||
CodeEditor *editor() const;
|
||||
bool isModified() const;
|
||||
|
||||
bool save();
|
||||
bool saveAs();
|
||||
|
||||
private:
|
||||
QString m_filePath;
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
CodeEditor *m_editor = nullptr;
|
||||
};
|
||||
330
barecode/src/editor/FileSearchPanel.cpp
Normal file
@@ -0,0 +1,330 @@
|
||||
#include "FileSearchPanel.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QKeyEvent>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFuture>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
FileSearchPanel::FileSearchPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
hide();
|
||||
|
||||
m_watcher = new QFutureWatcher<QList<Match>>(this);
|
||||
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
|
||||
this, &FileSearchPanel::onSearchFinished);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(6, 4, 6, 4);
|
||||
root->setSpacing(4);
|
||||
|
||||
// ---- Zeile 1: Suchbegriff ----
|
||||
QHBoxLayout *row1 = new QHBoxLayout();
|
||||
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
|
||||
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
|
||||
m_searchEdit->setClearButtonEnabled(true);
|
||||
row1->addWidget(m_searchEdit, 1);
|
||||
|
||||
m_btnSearch = new QPushButton(tr("Suchen"), this);
|
||||
m_btnSearch->setDefault(true);
|
||||
row1->addWidget(m_btnSearch);
|
||||
|
||||
m_btnClose = new QPushButton(tr("✕"), this);
|
||||
m_btnClose->setFixedWidth(24);
|
||||
m_btnClose->setFlat(true);
|
||||
m_btnClose->setToolTip(tr("Schließen"));
|
||||
row1->addWidget(m_btnClose);
|
||||
|
||||
root->addLayout(row1);
|
||||
|
||||
// ---- Zeile 2: Optionen + Filter ----
|
||||
QHBoxLayout *row2 = new QHBoxLayout();
|
||||
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
||||
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
||||
m_chkRegex = new QCheckBox(tr("Regex"), this);
|
||||
|
||||
row2->addWidget(m_chkCase);
|
||||
row2->addWidget(m_chkWord);
|
||||
row2->addWidget(m_chkRegex);
|
||||
row2->addSpacing(12);
|
||||
|
||||
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
|
||||
m_filterEdit = new QLineEdit(this);
|
||||
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
|
||||
m_filterEdit->setFixedWidth(220);
|
||||
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
|
||||
row2->addWidget(m_filterEdit);
|
||||
row2->addStretch();
|
||||
|
||||
root->addLayout(row2);
|
||||
|
||||
// ---- Fortschritt + Status ----
|
||||
m_progress = new QProgressBar(this);
|
||||
m_progress->setRange(0, 0); // Unbestimmter Modus
|
||||
m_progress->setFixedHeight(4);
|
||||
m_progress->hide();
|
||||
root->addWidget(m_progress);
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
root->addWidget(m_statusLabel);
|
||||
|
||||
// ---- Ergebnisliste ----
|
||||
m_results = new QTreeWidget(this);
|
||||
m_results->setHeaderHidden(true);
|
||||
m_results->setRootIsDecorated(true);
|
||||
m_results->setIndentation(16);
|
||||
m_results->setUniformRowHeights(true);
|
||||
m_results->setAlternatingRowColors(true);
|
||||
root->addWidget(m_results, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
|
||||
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
|
||||
connect(m_btnClose, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
hide();
|
||||
});
|
||||
connect(m_results, &QTreeWidget::itemActivated,
|
||||
this, &FileSearchPanel::onResultActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::setSearchRoot(const QString &path)
|
||||
{
|
||||
m_searchRoot = path;
|
||||
}
|
||||
|
||||
void FileSearchPanel::activate()
|
||||
{
|
||||
show();
|
||||
m_searchEdit->setFocus();
|
||||
m_searchEdit->selectAll();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Suche starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onSearch()
|
||||
{
|
||||
const QString needle = m_searchEdit->text().trimmed();
|
||||
if (needle.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_searchRoot.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Laufende Suche abbrechen
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
m_results->clear();
|
||||
m_statusLabel->setText(tr("Suche läuft…"));
|
||||
m_progress->show();
|
||||
m_btnSearch->setEnabled(false);
|
||||
|
||||
const QString root = m_searchRoot;
|
||||
const bool cs = m_chkCase->isChecked();
|
||||
const bool word = m_chkWord->isChecked();
|
||||
const bool regex = m_chkRegex->isChecked();
|
||||
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
|
||||
Qt::SkipEmptyParts);
|
||||
|
||||
QFuture<QList<Match>> future = QtConcurrent::run(
|
||||
[this, root, needle, cs, word, regex, extensions]()
|
||||
{
|
||||
return searchInFiles(root, needle, cs, word, regex, extensions);
|
||||
}
|
||||
);
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Suchergebnisse anzeigen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onSearchFinished()
|
||||
{
|
||||
m_progress->hide();
|
||||
m_btnSearch->setEnabled(true);
|
||||
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Match> matches = m_watcher->result();
|
||||
|
||||
// Ergebnisse gruppiert nach Datei aufbauen
|
||||
QString currentFile;
|
||||
QTreeWidgetItem *fileItem = nullptr;
|
||||
int fileCount = 0;
|
||||
int matchCount = 0;
|
||||
|
||||
for (const Match &m : matches)
|
||||
{
|
||||
if (m.filePath != currentFile)
|
||||
{
|
||||
currentFile = m.filePath;
|
||||
++fileCount;
|
||||
|
||||
fileItem = new QTreeWidgetItem(m_results);
|
||||
fileItem->setText(0, QFileInfo(m.filePath).fileName());
|
||||
fileItem->setToolTip(0, m.filePath);
|
||||
fileItem->setData(0, Qt::UserRole, m.filePath);
|
||||
fileItem->setData(0, Qt::UserRole + 1, -1);
|
||||
|
||||
QFont boldFont = fileItem->font(0);
|
||||
boldFont.setBold(true);
|
||||
fileItem->setFont(0, boldFont);
|
||||
fileItem->setExpanded(true);
|
||||
}
|
||||
|
||||
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
|
||||
lineItem->setText(0, QString(" Zeile %1: %2")
|
||||
.arg(m.line)
|
||||
.arg(m.content.trimmed().left(120)));
|
||||
lineItem->setToolTip(0, m.content.trimmed());
|
||||
lineItem->setData(0, Qt::UserRole, m.filePath);
|
||||
lineItem->setData(0, Qt::UserRole + 1, m.line);
|
||||
|
||||
++matchCount;
|
||||
}
|
||||
|
||||
// Datei-Titelzeilen um Trefferanzahl ergänzen
|
||||
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
||||
{
|
||||
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
||||
const int count = item->childCount();
|
||||
item->setText(0, QString("%1 (%2 Treffer)")
|
||||
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
|
||||
.arg(count));
|
||||
}
|
||||
|
||||
if (matchCount == 0)
|
||||
{
|
||||
m_statusLabel->setText(tr("Keine Treffer gefunden."));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
|
||||
.arg(matchCount)
|
||||
.arg(fileCount));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick auf Treffer → Datei + Zeile öffnen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
|
||||
if (path.isEmpty() || line < 0)
|
||||
{
|
||||
// Datei-Titelzeile: nur auf-/zuklappen
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Eigentliche Suchroutine (läuft in Thread-Pool)
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
|
||||
const QString &root,
|
||||
const QString &needle,
|
||||
bool caseSensitive,
|
||||
bool wholeWord,
|
||||
bool useRegex,
|
||||
const QStringList &extensions) const
|
||||
{
|
||||
QList<Match> results;
|
||||
|
||||
// Regulären Ausdruck vorbereiten
|
||||
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
|
||||
if (wholeWord)
|
||||
{
|
||||
pattern = "\\b" + pattern + "\\b";
|
||||
}
|
||||
|
||||
QRegularExpression re(pattern,
|
||||
caseSensitive
|
||||
? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
if (!re.isValid())
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
// Verzeichnis rekursiv durchsuchen
|
||||
QDirIterator it(root,
|
||||
extensions.isEmpty()
|
||||
? QStringList("*")
|
||||
: extensions,
|
||||
QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const QString filePath = it.next();
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
int lineNumber = 0;
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
++lineNumber;
|
||||
const QString line = stream.readLine();
|
||||
|
||||
if (re.match(line).hasMatch())
|
||||
{
|
||||
results.append({ filePath, lineNumber, line });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
77
barecode/src/editor/FileSearchPanel.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileSearchPanel – Suche in allen Dateien eines Verzeichnisses.
|
||||
//
|
||||
// Ergebnisse werden als aufklappbare Liste angezeigt:
|
||||
// Dateiname (N Treffer)
|
||||
// └ Zeile 12: <Zeileninhalt>
|
||||
// └ Zeile 34: <Zeileninhalt>
|
||||
//
|
||||
// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FileSearchPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileSearchPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setSearchRoot(const QString &path);
|
||||
|
||||
public slots:
|
||||
void activate();
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onSearch();
|
||||
void onResultActivated(QTreeWidgetItem *item, int column);
|
||||
void onSearchFinished();
|
||||
|
||||
private:
|
||||
struct Match
|
||||
{
|
||||
QString filePath;
|
||||
int line;
|
||||
QString content;
|
||||
};
|
||||
|
||||
void setupUi();
|
||||
QList<Match> searchInFiles(const QString &root,
|
||||
const QString &needle,
|
||||
bool caseSensitive,
|
||||
bool wholeWord,
|
||||
bool useRegex,
|
||||
const QStringList &extensions) const;
|
||||
|
||||
QString m_searchRoot;
|
||||
|
||||
QLineEdit *m_searchEdit = nullptr;
|
||||
QCheckBox *m_chkCase = nullptr;
|
||||
QCheckBox *m_chkWord = nullptr;
|
||||
QCheckBox *m_chkRegex = nullptr;
|
||||
QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter
|
||||
QPushButton *m_btnSearch = nullptr;
|
||||
QPushButton *m_btnClose = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QTreeWidget *m_results = nullptr;
|
||||
QProgressBar *m_progress = nullptr;
|
||||
|
||||
QFutureWatcher<QList<Match>> *m_watcher = nullptr;
|
||||
};
|
||||
83
barecode/src/editor/FunctionIndex.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
FunctionIndex::FunctionIndex(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
|
||||
this, &FunctionIndex::onScanFinished);
|
||||
}
|
||||
|
||||
void FunctionIndex::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_projectRoot = path;
|
||||
m_ready = false;
|
||||
m_index.clear();
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionIndex::refresh()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
FunctionScanner *scanner = m_scanner;
|
||||
const QString root = m_projectRoot;
|
||||
|
||||
m_watcher->setFuture(
|
||||
QtConcurrent::run([scanner, root]()
|
||||
{
|
||||
return scanner->scanDirectory(root);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
void FunctionIndex::onScanFinished()
|
||||
{
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_index.clear();
|
||||
const QList<FunctionScanner::FunctionInfo> all = m_watcher->result();
|
||||
|
||||
for (const FunctionScanner::FunctionInfo &func : all)
|
||||
{
|
||||
const QString key = func.name.toLower();
|
||||
// Ersten Eintrag bevorzugen (globale Funktionen vor Methoden)
|
||||
if (!m_index.contains(key) || func.className.isEmpty())
|
||||
{
|
||||
m_index.insert(key, func);
|
||||
}
|
||||
}
|
||||
|
||||
m_ready = true;
|
||||
emit indexReady();
|
||||
}
|
||||
|
||||
FunctionScanner::FunctionInfo FunctionIndex::lookup(const QString &name) const
|
||||
{
|
||||
return m_index.value(name.toLower(), FunctionScanner::FunctionInfo{});
|
||||
}
|
||||
|
||||
bool FunctionIndex::isReady() const
|
||||
{
|
||||
return m_ready;
|
||||
}
|
||||
47
barecode/src/editor/FunctionIndex.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionIndex – Singleton-artiger Index aller Projektfunktionen.
|
||||
// Wird von CodeEditor (Doppelklick-Navigation) und SignatureHelper genutzt.
|
||||
// Aktualisiert sich asynchron wenn setProjectRoot() aufgerufen wird.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionIndex : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionIndex(QObject *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
void refresh();
|
||||
|
||||
// Sucht eine Funktion nach Name (Groß-/Kleinschreibung ignoriert)
|
||||
// Gibt ungültige FunctionInfo zurück wenn nicht gefunden (filePath ist leer)
|
||||
FunctionScanner::FunctionInfo lookup(const QString &name) const;
|
||||
|
||||
bool isReady() const;
|
||||
|
||||
signals:
|
||||
void indexReady();
|
||||
|
||||
private slots:
|
||||
void onScanFinished();
|
||||
|
||||
private:
|
||||
QString m_projectRoot;
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
|
||||
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
|
||||
|
||||
// name.toLower() → FunctionInfo
|
||||
QHash<QString, FunctionScanner::FunctionInfo> m_index;
|
||||
bool m_ready = false;
|
||||
};
|
||||
52
barecode/src/editor/FunctionListDialog.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "FunctionListDialog.h"
|
||||
#include "FunctionListPanel.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QSettings>
|
||||
#include <QCloseEvent>
|
||||
|
||||
FunctionListDialog::FunctionListDialog(QWidget *parent)
|
||||
: QDialog(parent, Qt::Window)
|
||||
{
|
||||
setWindowTitle(tr("Projektfunktionen – BareCode"));
|
||||
setMinimumSize(500, 400);
|
||||
|
||||
// Fenstergröße und -position wiederherstellen
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
if (s.contains("funclist/geometry"))
|
||||
{
|
||||
restoreGeometry(s.value("funclist/geometry").toByteArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
resize(600, 700);
|
||||
}
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_panel = new FunctionListPanel(this);
|
||||
m_panel->show(); // Immer sichtbar — Panel ist jetzt der gesamte Dialog-Inhalt
|
||||
layout->addWidget(m_panel);
|
||||
|
||||
connect(m_panel, &FunctionListPanel::fileLineRequested,
|
||||
this, &FunctionListDialog::fileLineRequested);
|
||||
}
|
||||
|
||||
void FunctionListDialog::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_panel->setProjectRoot(path);
|
||||
}
|
||||
|
||||
void FunctionListDialog::refresh()
|
||||
{
|
||||
m_panel->refresh();
|
||||
}
|
||||
|
||||
void FunctionListDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("funclist/geometry", saveGeometry());
|
||||
event->accept();
|
||||
}
|
||||
30
barecode/src/editor/FunctionListDialog.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class FunctionListPanel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionListDialog – Eigenständiges Fenster für die Projektfunktionen.
|
||||
// Bleibt offen während man im Editor arbeitet (non-modal).
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionListDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionListDialog(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
void refresh();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private:
|
||||
FunctionListPanel *m_panel = nullptr;
|
||||
};
|
||||
332
barecode/src/editor/FunctionListPanel.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
#include "FunctionListPanel.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QProgressBar>
|
||||
|
||||
FunctionListPanel::FunctionListPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
|
||||
this, &FunctionListPanel::onScanFinished);
|
||||
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
// ---- Kopfzeile ----
|
||||
QWidget *header = new QWidget(this);
|
||||
header->setStyleSheet("background: palette(mid);");
|
||||
QHBoxLayout *headerLayout = new QHBoxLayout(header);
|
||||
headerLayout->setContentsMargins(6, 4, 6, 4);
|
||||
headerLayout->setSpacing(4);
|
||||
|
||||
QLabel *title = new QLabel(tr("Projektfunktionen"), header);
|
||||
QFont boldFont = title->font();
|
||||
boldFont.setBold(true);
|
||||
title->setFont(boldFont);
|
||||
headerLayout->addWidget(title);
|
||||
headerLayout->addStretch();
|
||||
|
||||
m_btnRefresh = new QPushButton(tr("↻"), header);
|
||||
m_btnRefresh->setFixedSize(24, 24);
|
||||
m_btnRefresh->setFlat(true);
|
||||
m_btnRefresh->setToolTip(tr("Neu scannen"));
|
||||
headerLayout->addWidget(m_btnRefresh);
|
||||
|
||||
root->addWidget(header);
|
||||
|
||||
// ---- Filter + Gruppierung ----
|
||||
QHBoxLayout *toolRow = new QHBoxLayout();
|
||||
toolRow->setContentsMargins(6, 4, 6, 4);
|
||||
toolRow->setSpacing(4);
|
||||
|
||||
m_filterEdit = new QLineEdit(this);
|
||||
m_filterEdit->setPlaceholderText(tr("Funktion suchen…"));
|
||||
m_filterEdit->setClearButtonEnabled(true);
|
||||
toolRow->addWidget(m_filterEdit, 1);
|
||||
|
||||
m_groupCombo = new QComboBox(this);
|
||||
m_groupCombo->addItem(tr("Nach Datei"), "file");
|
||||
m_groupCombo->addItem(tr("Nach Klasse"), "class");
|
||||
m_groupCombo->addItem(tr("Alphabetisch"), "alpha");
|
||||
m_groupCombo->setFixedWidth(120);
|
||||
toolRow->addWidget(m_groupCombo);
|
||||
|
||||
root->addLayout(toolRow);
|
||||
|
||||
// ---- Status ----
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setContentsMargins(6, 0, 6, 2);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
|
||||
root->addWidget(m_statusLabel);
|
||||
|
||||
// ---- Ergebnisbaum ----
|
||||
m_tree = new QTreeWidget(this);
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setIndentation(14);
|
||||
m_tree->setAlternatingRowColors(false);
|
||||
m_tree->setAnimated(true);
|
||||
|
||||
// Monospace-Font für Parameter
|
||||
QFont monoFont("Monospace", m_tree->font().pointSize());
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
m_tree->setFont(monoFont);
|
||||
|
||||
root->addWidget(m_tree, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnRefresh, &QPushButton::clicked,
|
||||
this, &FunctionListPanel::refresh);
|
||||
|
||||
connect(m_filterEdit, &QLineEdit::textChanged,
|
||||
this, &FunctionListPanel::onFilterChanged);
|
||||
|
||||
connect(m_groupCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &FunctionListPanel::onGroupingChanged);
|
||||
|
||||
connect(m_tree, &QTreeWidget::itemActivated,
|
||||
this, &FunctionListPanel::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_projectRoot = path;
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tree->clear();
|
||||
m_statusLabel->setText(QString());
|
||||
m_lastResult.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::activate()
|
||||
{
|
||||
m_filterEdit->setFocus();
|
||||
m_filterEdit->selectAll();
|
||||
if (m_lastResult.isEmpty() && !m_projectRoot.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::refresh()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("Kein Projekt geöffnet."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
m_statusLabel->setText(tr("Scanne…"));
|
||||
m_btnRefresh->setEnabled(false);
|
||||
|
||||
const QString root = m_projectRoot;
|
||||
FunctionScanner *scanner = m_scanner;
|
||||
|
||||
QFuture<QList<FunctionScanner::FunctionInfo>> future =
|
||||
QtConcurrent::run([scanner, root]()
|
||||
{
|
||||
return scanner->scanDirectory(root);
|
||||
});
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan abgeschlossen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::onScanFinished()
|
||||
{
|
||||
m_btnRefresh->setEnabled(true);
|
||||
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastResult = m_watcher->result();
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Baum befüllen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::populateTree(const QList<FunctionScanner::FunctionInfo> &functions)
|
||||
{
|
||||
m_tree->clear();
|
||||
|
||||
const QString groupBy = m_groupCombo->currentData().toString();
|
||||
const QString filter = m_filterEdit->text().trimmed().toLower();
|
||||
|
||||
// Funktionen filtern
|
||||
QList<FunctionScanner::FunctionInfo> filtered;
|
||||
for (const auto &f : functions)
|
||||
{
|
||||
if (filter.isEmpty() ||
|
||||
f.name.toLower().contains(filter) ||
|
||||
f.parameters.toLower().contains(filter) ||
|
||||
f.className.toLower().contains(filter))
|
||||
{
|
||||
filtered.append(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBy == "alpha")
|
||||
{
|
||||
// Alphabetisch — keine Gruppen
|
||||
std::sort(filtered.begin(), filtered.end(),
|
||||
[](const FunctionScanner::FunctionInfo &a,
|
||||
const FunctionScanner::FunctionInfo &b)
|
||||
{
|
||||
return a.name.toLower() < b.name.toLower();
|
||||
});
|
||||
|
||||
for (const auto &f : filtered)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(m_tree);
|
||||
formatFunctionItem(item, f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gruppiert nach Datei oder Klasse
|
||||
QMap<QString, QList<FunctionScanner::FunctionInfo>> groups;
|
||||
|
||||
for (const auto &f : filtered)
|
||||
{
|
||||
QString key;
|
||||
if (groupBy == "class")
|
||||
{
|
||||
key = f.className.isEmpty() ? tr("(global)") : f.className;
|
||||
}
|
||||
else
|
||||
{
|
||||
key = QFileInfo(f.filePath).fileName();
|
||||
}
|
||||
groups[key].append(f);
|
||||
}
|
||||
|
||||
for (auto it = groups.begin(); it != groups.end(); ++it)
|
||||
{
|
||||
QTreeWidgetItem *groupItem = new QTreeWidgetItem(m_tree);
|
||||
groupItem->setText(0, QString("%1 (%2)")
|
||||
.arg(it.key())
|
||||
.arg(it.value().size()));
|
||||
groupItem->setData(0, Qt::UserRole, QString());
|
||||
groupItem->setData(0, Qt::UserRole + 1, -1);
|
||||
|
||||
QFont boldFont = groupItem->font(0);
|
||||
boldFont.setBold(true);
|
||||
groupItem->setFont(0, boldFont);
|
||||
groupItem->setExpanded(true);
|
||||
|
||||
// Funktionen innerhalb der Gruppe alphabetisch
|
||||
auto &list = it.value();
|
||||
std::sort(list.begin(), list.end(),
|
||||
[](const FunctionScanner::FunctionInfo &a,
|
||||
const FunctionScanner::FunctionInfo &b)
|
||||
{
|
||||
return a.name.toLower() < b.name.toLower();
|
||||
});
|
||||
|
||||
for (const auto &f : list)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(groupItem);
|
||||
formatFunctionItem(item, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Statuszeile
|
||||
const int total = functions.size();
|
||||
const int shown = filtered.size();
|
||||
if (filter.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Funktionen gefunden").arg(total));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 von %2 Funktionen").arg(shown).arg(total));
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::formatFunctionItem(QTreeWidgetItem *item,
|
||||
const FunctionScanner::FunctionInfo &f)
|
||||
{
|
||||
// Anzeige: name(parameter) — Zeilennummer klein dahinter
|
||||
const QString display = QString("%1(%2)")
|
||||
.arg(f.name)
|
||||
.arg(f.parameters);
|
||||
|
||||
item->setText(0, display);
|
||||
item->setToolTip(0, QString("%1\nZeile %2").arg(f.filePath).arg(f.line));
|
||||
item->setData(0, Qt::UserRole, f.filePath);
|
||||
item->setData(0, Qt::UserRole + 1, f.line);
|
||||
|
||||
// Zeilennummer in gedimmter Farbe als zweite Spalte simulieren
|
||||
// (über ToolTip gelöst da wir nur eine Spalte haben)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::onItemActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
|
||||
if (path.isEmpty() || line < 0)
|
||||
{
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
|
||||
void FunctionListPanel::onFilterChanged(const QString &text)
|
||||
{
|
||||
Q_UNUSED(text)
|
||||
if (!m_lastResult.isEmpty())
|
||||
{
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::onGroupingChanged(int /*index*/)
|
||||
{
|
||||
if (!m_lastResult.isEmpty())
|
||||
{
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
}
|
||||
67
barecode/src/editor/FunctionListPanel.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionListPanel – Zeigt alle im Projekt definierten Funktionen
|
||||
// als durchsuchbare, sortierbare Liste.
|
||||
//
|
||||
// Gruppierung: nach Datei oder nach Klasse
|
||||
// Klick: öffnet Datei und springt zur Definition
|
||||
// Aktualisierung: automatisch nach jedem Speichern
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionListPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionListPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
void activate();
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onScanFinished();
|
||||
void onItemActivated(QTreeWidgetItem *item, int column);
|
||||
void onFilterChanged(const QString &text);
|
||||
void onGroupingChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateTree(const QList<FunctionScanner::FunctionInfo> &functions);
|
||||
void applyFilter(const QString &text);
|
||||
static void formatFunctionItem(QTreeWidgetItem *item,
|
||||
const FunctionScanner::FunctionInfo &f);
|
||||
|
||||
QString m_projectRoot;
|
||||
|
||||
QLineEdit *m_filterEdit = nullptr;
|
||||
QComboBox *m_groupCombo = nullptr;
|
||||
QPushButton *m_btnRefresh = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QTreeWidget *m_tree = nullptr;
|
||||
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
|
||||
|
||||
// Letztes Scan-Ergebnis für Filter ohne Neuscan
|
||||
QList<FunctionScanner::FunctionInfo> m_lastResult;
|
||||
};
|
||||
158
barecode/src/editor/FunctionScanner.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
|
||||
FunctionScanner::FunctionScanner(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Verzeichnis rekursiv scannen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanDirectory(
|
||||
const QString &rootPath,
|
||||
const QStringList &extensions) const
|
||||
{
|
||||
QList<FunctionInfo> results;
|
||||
|
||||
QDirIterator it(rootPath,
|
||||
extensions,
|
||||
QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString path = it.next();
|
||||
results.append(scanFile(path));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Einzelne Datei scannen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanFile(const QString &filePath) const
|
||||
{
|
||||
QList<FunctionInfo> results;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
QStringList lines;
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
lines.append(stream.readLine());
|
||||
}
|
||||
|
||||
// Regex für Funktionsdefinitionen:
|
||||
// optional: public/protected/private/static/abstract/final
|
||||
// gefolgt von: function name(
|
||||
static const QRegularExpression funcRegex(
|
||||
R"((?:(?:public|protected|private|static|abstract|final)\s+)*)"
|
||||
R"(function\s+(&?\s*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i)
|
||||
{
|
||||
const QString &line = lines[i];
|
||||
|
||||
// Zeilen in Kommentaren überspringen
|
||||
const QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QRegularExpressionMatch match = funcRegex.match(line);
|
||||
if (!match.hasMatch())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FunctionInfo info;
|
||||
info.name = match.captured(1).trimmed();
|
||||
info.filePath = filePath;
|
||||
info.line = i + 1;
|
||||
info.className = detectClassContext(lines, i);
|
||||
info.parameters = extractParameters(line, static_cast<int>(match.capturedEnd()) - 1);
|
||||
info.signature = info.name + "(" + info.parameters + ")";
|
||||
|
||||
// Konstruktoren und magische Methoden kennzeichnen
|
||||
// aber trotzdem aufnehmen — sie sind nützlich in der Liste
|
||||
|
||||
results.append(info);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter aus der Funktionssignatur extrahieren
|
||||
// Behandelt auch mehrzeilige Signaturen mit öffnender Klammer am Ende
|
||||
// ---------------------------------------------------------------------------
|
||||
QString FunctionScanner::extractParameters(const QString &line, int parenPos)
|
||||
{
|
||||
// Inhalt zwischen ( und ) extrahieren
|
||||
// Einfache Version: nur die erste Zeile — reicht für 99% aller Fälle
|
||||
const int openParen = parenPos;
|
||||
int depth = 0;
|
||||
int closePos = -1;
|
||||
|
||||
for (int i = openParen; i < line.length(); ++i)
|
||||
{
|
||||
if (line[i] == '(') { ++depth; }
|
||||
else if (line[i] == ')')
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
closePos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closePos == -1)
|
||||
{
|
||||
// Klammer geht über Zeilenende — gekürzt anzeigen
|
||||
return line.mid(openParen + 1).trimmed() + "…";
|
||||
}
|
||||
|
||||
return line.mid(openParen + 1, closePos - openParen - 1).trimmed();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klassenkontext erkennen — rückwärts durch den Code suchen
|
||||
// ---------------------------------------------------------------------------
|
||||
QString FunctionScanner::detectClassContext(const QStringList &lines, int functionLineIndex)
|
||||
{
|
||||
static const QRegularExpression classRegex(
|
||||
R"((?:class|interface|trait|enum)\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
// Rückwärts suchen — letztes class/interface/trait vor dieser Zeile
|
||||
for (int i = functionLineIndex - 1; i >= 0; --i)
|
||||
{
|
||||
QRegularExpressionMatch match = classRegex.match(lines[i]);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
return match.captured(1);
|
||||
}
|
||||
}
|
||||
|
||||
return QString(); // Globale Funktion
|
||||
}
|
||||
47
barecode/src/editor/FunctionScanner.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionScanner – Scannt PHP-Dateien nach Funktionsdefinitionen.
|
||||
//
|
||||
// Liefert für jede gefundene Funktion:
|
||||
// - Name
|
||||
// - Parameter (vollständige Signatur)
|
||||
// - Dateipfad
|
||||
// - Zeilennummer
|
||||
//
|
||||
// Wird von FunctionListPanel (Anzeige) und DeadCodeAnalyzer (Stufe 1)
|
||||
// gemeinsam genutzt.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionScanner : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct FunctionInfo
|
||||
{
|
||||
QString name; // Funktionsname
|
||||
QString parameters; // Parameter wie definiert, z.B. "$id, $name = null"
|
||||
QString signature; // name(parameters) — fertig formatiert
|
||||
QString filePath; // Absoluter Pfad zur Datei
|
||||
int line = 0; // Zeilennummer der Definition
|
||||
QString className; // Klassen- oder Namespace-Kontext, leer = global
|
||||
};
|
||||
|
||||
explicit FunctionScanner(QObject *parent = nullptr);
|
||||
|
||||
// Synchroner Scan — für direkten Aufruf aus Threads
|
||||
QList<FunctionInfo> scanDirectory(const QString &rootPath,
|
||||
const QStringList &extensions = {"*.php"}) const;
|
||||
|
||||
// Scannt eine einzelne Datei
|
||||
QList<FunctionInfo> scanFile(const QString &filePath) const;
|
||||
|
||||
private:
|
||||
static QString extractParameters(const QString &line, int parenPos);
|
||||
static QString detectClassContext(const QStringList &lines, int functionLineIndex);
|
||||
};
|
||||
18
barecode/src/editor/LineNumberArea.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "LineNumberArea.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
LineNumberArea::LineNumberArea(CodeEditor *editor)
|
||||
: QWidget(editor)
|
||||
, m_codeEditor(editor)
|
||||
{
|
||||
}
|
||||
|
||||
QSize LineNumberArea::sizeHint() const
|
||||
{
|
||||
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
|
||||
}
|
||||
|
||||
void LineNumberArea::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
m_codeEditor->lineNumberAreaPaintEvent(event);
|
||||
}
|
||||
25
barecode/src/editor/LineNumberArea.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LineNumberArea – Thin widget painted on the left side of the CodeEditor.
|
||||
// Painted by CodeEditor::lineNumberAreaPaintEvent().
|
||||
// ---------------------------------------------------------------------------
|
||||
class LineNumberArea : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LineNumberArea(CodeEditor *editor);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
CodeEditor *m_codeEditor;
|
||||
};
|
||||
492
barecode/src/editor/SearchPanel.cpp
Normal file
@@ -0,0 +1,492 @@
|
||||
#include "SearchPanel.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QRegularExpression>
|
||||
#include <QMessageBox>
|
||||
#include <QKeyEvent>
|
||||
#include <QShortcut>
|
||||
|
||||
SearchPanel::SearchPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
hide();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::setupUi()
|
||||
{
|
||||
m_grid = new QGridLayout(this);
|
||||
m_grid->setContentsMargins(6, 4, 6, 4);
|
||||
m_grid->setSpacing(4);
|
||||
|
||||
// ---- Row 0: Suchen ----
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText(tr("Suchen…"));
|
||||
m_searchEdit->setClearButtonEnabled(true);
|
||||
|
||||
m_btnPrev = new QPushButton(tr("▲"), this);
|
||||
m_btnNext = new QPushButton(tr("▼"), this);
|
||||
m_btnPrev->setFixedWidth(28);
|
||||
m_btnNext->setFixedWidth(28);
|
||||
m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)"));
|
||||
m_btnNext->setToolTip(tr("Nächster Treffer (F3)"));
|
||||
|
||||
m_matchLabel = new QLabel(this);
|
||||
m_matchLabel->setMinimumWidth(80);
|
||||
|
||||
m_btnClose = new QPushButton(tr("✕"), this);
|
||||
m_btnClose->setFixedWidth(24);
|
||||
m_btnClose->setToolTip(tr("Schließen (Esc)"));
|
||||
m_btnClose->setFlat(true);
|
||||
|
||||
QHBoxLayout *searchRow = new QHBoxLayout();
|
||||
searchRow->addWidget(new QLabel(tr("Suchen:"), this));
|
||||
searchRow->addWidget(m_searchEdit, 1);
|
||||
searchRow->addWidget(m_btnPrev);
|
||||
searchRow->addWidget(m_btnNext);
|
||||
searchRow->addWidget(m_matchLabel);
|
||||
searchRow->addWidget(m_btnClose);
|
||||
m_grid->addLayout(searchRow, 0, 0);
|
||||
|
||||
// ---- Row 1: Ersetzen ----
|
||||
m_replaceEdit = new QLineEdit(this);
|
||||
m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…"));
|
||||
m_replaceEdit->setClearButtonEnabled(true);
|
||||
|
||||
m_btnReplace = new QPushButton(tr("Ersetzen"), this);
|
||||
m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this);
|
||||
m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this);
|
||||
|
||||
QHBoxLayout *replaceRow = new QHBoxLayout();
|
||||
replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this));
|
||||
replaceRow->addWidget(m_replaceEdit, 1);
|
||||
replaceRow->addWidget(m_btnReplace);
|
||||
replaceRow->addWidget(m_btnReplaceAll);
|
||||
replaceRow->addWidget(m_btnReplaceSelection);
|
||||
m_grid->addLayout(replaceRow, 1, 0);
|
||||
|
||||
// ---- Row 2: Optionen ----
|
||||
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
||||
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
||||
m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this);
|
||||
|
||||
QHBoxLayout *optRow = new QHBoxLayout();
|
||||
optRow->addWidget(m_chkCase);
|
||||
optRow->addWidget(m_chkWord);
|
||||
optRow->addWidget(m_chkRegex);
|
||||
optRow->addStretch();
|
||||
m_grid->addLayout(optRow, 2, 0);
|
||||
|
||||
// ---- Connections ----
|
||||
connect(m_searchEdit, &QLineEdit::textChanged,
|
||||
this, &SearchPanel::onSearchTextChanged);
|
||||
|
||||
connect(m_searchEdit, &QLineEdit::returnPressed,
|
||||
this, &SearchPanel::findNext);
|
||||
|
||||
connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext);
|
||||
connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious);
|
||||
|
||||
connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent);
|
||||
connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll);
|
||||
connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection);
|
||||
|
||||
connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked);
|
||||
|
||||
connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::setEditor(CodeEditor *editor)
|
||||
{
|
||||
clearHighlights();
|
||||
m_editor = editor;
|
||||
}
|
||||
|
||||
void SearchPanel::activate()
|
||||
{
|
||||
show();
|
||||
m_searchEdit->setFocus();
|
||||
m_searchEdit->selectAll();
|
||||
|
||||
// Pre-fill with selected text if short enough
|
||||
if (m_editor)
|
||||
{
|
||||
const QString sel = m_editor->textCursor().selectedText();
|
||||
if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200)
|
||||
{
|
||||
m_searchEdit->setText(sel);
|
||||
}
|
||||
}
|
||||
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const
|
||||
{
|
||||
QTextDocument::FindFlags flags;
|
||||
if (backwards) { flags |= QTextDocument::FindBackward; }
|
||||
if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; }
|
||||
if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; }
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool SearchPanel::performFind(bool backwards)
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(backwards);
|
||||
bool found = false;
|
||||
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(m_searchEdit->text());
|
||||
if (m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::NoPatternOption);
|
||||
}
|
||||
else
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
found = m_editor->find(re, flags);
|
||||
|
||||
// Wrap around
|
||||
if (!found)
|
||||
{
|
||||
QTextCursor c = m_editor->textCursor();
|
||||
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
|
||||
m_editor->setTextCursor(c);
|
||||
found = m_editor->find(re, flags);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
found = m_editor->find(m_searchEdit->text(), flags);
|
||||
|
||||
// Wrap around
|
||||
if (!found)
|
||||
{
|
||||
QTextCursor c = m_editor->textCursor();
|
||||
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
|
||||
m_editor->setTextCursor(c);
|
||||
found = m_editor->find(m_searchEdit->text(), flags);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void SearchPanel::highlightAllMatches()
|
||||
{
|
||||
if (!m_editor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QTextEdit::ExtraSelection> extras;
|
||||
|
||||
const QString needle = m_searchEdit->text();
|
||||
if (needle.isEmpty())
|
||||
{
|
||||
m_editor->setExtraSelections(extras);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCharFormat fmt;
|
||||
fmt.setBackground(QColor("#3a3a00"));
|
||||
fmt.setForeground(QColor("#ffff80"));
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
QTextEdit::ExtraSelection sel;
|
||||
sel.cursor = cursor;
|
||||
sel.format = fmt;
|
||||
extras.append(sel);
|
||||
}
|
||||
|
||||
m_editor->setExtraSelections(extras);
|
||||
}
|
||||
|
||||
void SearchPanel::clearHighlights()
|
||||
{
|
||||
if (m_editor)
|
||||
{
|
||||
m_editor->setExtraSelections({});
|
||||
}
|
||||
}
|
||||
|
||||
void SearchPanel::updateMatchLabel()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
m_matchLabel->setText(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Count total matches
|
||||
int count = 0;
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
++count;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
m_matchLabel->setText(tr("Kein Treffer"));
|
||||
m_matchLabel->setStyleSheet("color: #cc4444;");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_matchLabel->setText(tr("%1 Treffer").arg(count));
|
||||
m_matchLabel->setStyleSheet(QString());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::findNext()
|
||||
{
|
||||
performFind(false);
|
||||
}
|
||||
|
||||
void SearchPanel::findPrevious()
|
||||
{
|
||||
performFind(true);
|
||||
}
|
||||
|
||||
void SearchPanel::replaceCurrent()
|
||||
{
|
||||
if (!m_editor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = m_editor->textCursor();
|
||||
|
||||
// If current selection matches the search term, replace it
|
||||
// Otherwise just find the next occurrence first
|
||||
const bool hasMatch = !cursor.selectedText().isEmpty();
|
||||
if (!hasMatch)
|
||||
{
|
||||
performFind(false);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.insertText(m_replaceEdit->text());
|
||||
|
||||
// Move to next match
|
||||
performFind(false);
|
||||
updateMatchLabel();
|
||||
highlightAllMatches();
|
||||
}
|
||||
|
||||
void SearchPanel::replaceAll()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
cursor.beginEditBlock();
|
||||
|
||||
int count = 0;
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
const QString replacement = m_replaceEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.insertText(replacement);
|
||||
++count;
|
||||
}
|
||||
|
||||
cursor.endEditBlock();
|
||||
|
||||
updateMatchLabel();
|
||||
clearHighlights();
|
||||
|
||||
QMessageBox::information(this, tr("Alle ersetzen"),
|
||||
tr("%1 Ersetzung(en) durchgeführt.").arg(count));
|
||||
}
|
||||
|
||||
void SearchPanel::replaceInSelection()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor selCursor = m_editor->textCursor();
|
||||
if (!selCursor.hasSelection())
|
||||
{
|
||||
QMessageBox::information(this, tr("In Auswahl ersetzen"),
|
||||
tr("Es ist kein Text ausgewählt."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Work only within the selected region
|
||||
const int selStart = selCursor.selectionStart();
|
||||
const int selEnd = selCursor.selectionEnd();
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(selStart);
|
||||
cursor.beginEditBlock();
|
||||
|
||||
int count = 0;
|
||||
int offset = 0; // Replacement may be longer/shorter than search term
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
const QString replacement = m_replaceEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop if we've left the original selection
|
||||
if (cursor.selectionEnd() > selEnd + offset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
offset += replacement.length() - cursor.selectedText().length();
|
||||
cursor.insertText(replacement);
|
||||
++count;
|
||||
}
|
||||
|
||||
cursor.endEditBlock();
|
||||
|
||||
updateMatchLabel();
|
||||
clearHighlights();
|
||||
|
||||
QMessageBox::information(this, tr("In Auswahl ersetzen"),
|
||||
tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::onSearchTextChanged(const QString &/*text*/)
|
||||
{
|
||||
highlightAllMatches();
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
void SearchPanel::onOptionChanged()
|
||||
{
|
||||
highlightAllMatches();
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
void SearchPanel::onCloseClicked()
|
||||
{
|
||||
clearHighlights();
|
||||
m_matchLabel->setText(QString());
|
||||
hide();
|
||||
if (m_editor)
|
||||
{
|
||||
m_editor->setFocus();
|
||||
}
|
||||
}
|
||||
79
barecode/src/editor/SearchPanel.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTextDocument>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SearchPanel – Collapsible find/replace bar that operates on a CodeEditor.
|
||||
//
|
||||
// Capabilities:
|
||||
// • Nächsten / Vorherigen Treffer suchen
|
||||
// • Einzeln ersetzen
|
||||
// • Alle ersetzen
|
||||
// • Nur in Auswahl ersetzen
|
||||
// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke
|
||||
// ---------------------------------------------------------------------------
|
||||
class SearchPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SearchPanel(QWidget *parent = nullptr);
|
||||
|
||||
// Must be called whenever the active editor changes
|
||||
void setEditor(CodeEditor *editor);
|
||||
|
||||
// Toggle visibility and focus the search field
|
||||
void activate();
|
||||
|
||||
public slots:
|
||||
void findNext();
|
||||
void findPrevious();
|
||||
void replaceCurrent();
|
||||
void replaceAll();
|
||||
void replaceInSelection();
|
||||
|
||||
private slots:
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert)
|
||||
void onCloseClicked();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
QTextDocument::FindFlags buildFindFlags(bool backwards = false) const;
|
||||
bool performFind(bool backwards = false);
|
||||
void highlightAllMatches();
|
||||
void clearHighlights();
|
||||
void updateMatchLabel();
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
|
||||
// Search row
|
||||
QLineEdit *m_searchEdit = nullptr;
|
||||
QPushButton *m_btnPrev = nullptr;
|
||||
QPushButton *m_btnNext = nullptr;
|
||||
QLabel *m_matchLabel = nullptr;
|
||||
QPushButton *m_btnClose = nullptr;
|
||||
|
||||
// Replace row
|
||||
QLineEdit *m_replaceEdit = nullptr;
|
||||
QPushButton *m_btnReplace = nullptr;
|
||||
QPushButton *m_btnReplaceAll = nullptr;
|
||||
QPushButton *m_btnReplaceSelection = nullptr;
|
||||
|
||||
// Options row
|
||||
QCheckBox *m_chkCase = nullptr;
|
||||
QCheckBox *m_chkWord = nullptr;
|
||||
QCheckBox *m_chkRegex = nullptr;
|
||||
|
||||
QGridLayout *m_grid = nullptr;
|
||||
};
|
||||
222
barecode/src/editor/SignatureHelper.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "SignatureHelper.h"
|
||||
#include "SignatureTooltip.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRect>
|
||||
#include <QRegularExpression>
|
||||
|
||||
SignatureHelper::SignatureHelper(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Tooltip als Kind des Viewports — bleibt im Fenster
|
||||
m_tooltip = new SignatureTooltip(editor->window());
|
||||
|
||||
loadDatabase();
|
||||
|
||||
connect(m_editor, &CodeEditor::cursorPositionChanged,
|
||||
this, &SignatureHelper::onCursorPositionChanged);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Datenbank laden
|
||||
// ---------------------------------------------------------------------------
|
||||
void SignatureHelper::loadDatabase()
|
||||
{
|
||||
QFile f(":/php_functions.json");
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||
if (!doc.isArray())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &val : doc.array())
|
||||
{
|
||||
const QJsonObject obj = val.toObject();
|
||||
const QString name = obj["name"].toString();
|
||||
if (name.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FunctionInfo info;
|
||||
info.signature = obj["signature"].toString();
|
||||
info.description = obj["desc"].toString();
|
||||
m_functions.insert(name.toLower(), info);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cursor-Bewegung auswerten
|
||||
// ---------------------------------------------------------------------------
|
||||
void SignatureHelper::onCursorPositionChanged()
|
||||
{
|
||||
const QTextCursor cursor = m_editor->textCursor();
|
||||
const QString block = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
const QString leftText = block.left(col);
|
||||
|
||||
const QString funcName = extractFunctionName(leftText);
|
||||
|
||||
if (funcName.isEmpty())
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Klammern zählen — wenn alle geschlossen, Tooltip ausblenden
|
||||
if (countOpenParens(leftText) <= 0)
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString key = funcName.toLower();
|
||||
if (!m_functions.contains(key))
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const FunctionInfo &info = m_functions[key];
|
||||
|
||||
// Position unter dem Cursor berechnen
|
||||
const QRect cursorRect = m_editor->cursorRect(cursor);
|
||||
const QPoint globalPos = m_editor->viewport()->mapToGlobal(
|
||||
QPoint(cursorRect.left(), cursorRect.bottom() + 4)
|
||||
);
|
||||
|
||||
m_tooltip->showSignature(info.signature, info.description, globalPos);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Funktionsnamen links vor der öffnenden Klammer extrahieren
|
||||
// ---------------------------------------------------------------------------
|
||||
QString SignatureHelper::extractFunctionName(const QString &text) const
|
||||
{
|
||||
// Wir suchen das letzte '(' das zu einem Funktionsnamen gehört.
|
||||
// Dabei müssen wir verschachtelte Klammern korrekt behandeln.
|
||||
int depth = 0;
|
||||
int openPos = -1;
|
||||
|
||||
for (int i = text.length() - 1; i >= 0; --i)
|
||||
{
|
||||
const QChar ch = text[i];
|
||||
if (ch == ')')
|
||||
{
|
||||
++depth;
|
||||
}
|
||||
else if (ch == '(')
|
||||
{
|
||||
if (depth == 0)
|
||||
{
|
||||
openPos = i;
|
||||
break;
|
||||
}
|
||||
--depth;
|
||||
}
|
||||
}
|
||||
|
||||
if (openPos <= 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Funktionsnamen direkt links von '(' lesen
|
||||
int end = openPos - 1;
|
||||
|
||||
// Leerzeichen überspringen
|
||||
while (end >= 0 && text[end].isSpace())
|
||||
{
|
||||
--end;
|
||||
}
|
||||
|
||||
if (end < 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Bezeichner-Zeichen sammeln (Buchstaben, Ziffern, _, :, \)
|
||||
int start = end;
|
||||
while (start > 0 &&
|
||||
(text[start - 1].isLetterOrNumber() ||
|
||||
text[start - 1] == '_' ||
|
||||
text[start - 1] == ':' ||
|
||||
text[start - 1] == '\\'))
|
||||
{
|
||||
--start;
|
||||
}
|
||||
|
||||
const QString name = text.mid(start, end - start + 1);
|
||||
|
||||
// Schlüsselwörter und leere Namen ausschließen
|
||||
static const QStringList keywords = {
|
||||
"if", "else", "elseif", "while", "for", "foreach",
|
||||
"switch", "match", "catch", "function", "fn"
|
||||
};
|
||||
|
||||
if (name.isEmpty() || keywords.contains(name.toLower()))
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Nur den letzten Teil nach :: oder -> nehmen
|
||||
const int colonPos = name.lastIndexOf("::");
|
||||
const int arrowPos = name.lastIndexOf("->");
|
||||
const int backslashPos = name.lastIndexOf("\\");
|
||||
const int splitPos = qMax(backslashPos, qMax(colonPos, arrowPos));
|
||||
|
||||
if (splitPos >= 0)
|
||||
{
|
||||
return name.mid(splitPos + (name[splitPos] == ':' ? 2 : (name[splitPos] == '\\' ? 1 : 2)));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offene Klammern zählen
|
||||
// ---------------------------------------------------------------------------
|
||||
int SignatureHelper::countOpenParens(const QString &text) const
|
||||
{
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
QChar stringChar;
|
||||
|
||||
for (int i = 0; i < text.length(); ++i)
|
||||
{
|
||||
const QChar ch = text[i];
|
||||
|
||||
// Einfache String-Erkennung (kein vollständiger PHP-Parser)
|
||||
if (!inString && (ch == '\'' || ch == '"'))
|
||||
{
|
||||
inString = true;
|
||||
stringChar = ch;
|
||||
continue;
|
||||
}
|
||||
if (inString)
|
||||
{
|
||||
if (ch == stringChar && (i == 0 || text[i - 1] != '\\'))
|
||||
{
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '(') { ++depth; }
|
||||
else if (ch == ')') { --depth; }
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
51
barecode/src/editor/SignatureHelper.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
|
||||
class CodeEditor;
|
||||
class SignatureTooltip;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SignatureHelper – Lädt die PHP-Funktionsdatenbank und zeigt beim Tippen
|
||||
// automatisch die passende Funktionssignatur als Tooltip.
|
||||
//
|
||||
// Logik:
|
||||
// • Bei jedem Tastendruck: Text links vom Cursor analysieren
|
||||
// • Wenn "funktionsname(" erkannt wird → Tooltip anzeigen
|
||||
// • Wenn ")" die öffnende Klammer schließt → Tooltip verstecken
|
||||
// • Wenn Cursor sich weg bewegt → Tooltip verstecken
|
||||
// ---------------------------------------------------------------------------
|
||||
class SignatureHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignatureHelper(CodeEditor *editor);
|
||||
|
||||
private slots:
|
||||
void onCursorPositionChanged();
|
||||
|
||||
private:
|
||||
struct FunctionInfo
|
||||
{
|
||||
QString signature;
|
||||
QString description;
|
||||
};
|
||||
|
||||
void loadDatabase();
|
||||
void loadProjectFunctions();
|
||||
|
||||
// Extrahiert den Funktionsnamen direkt links vor dem letzten '('
|
||||
// Gibt leeren String zurück wenn kein Kontext gefunden
|
||||
QString extractFunctionName(const QString &textUpToCursor) const;
|
||||
|
||||
// Zählt offene Klammern — bei 0 ist der Aufruf abgeschlossen
|
||||
int countOpenParens(const QString &textUpToCursor) const;
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
SignatureTooltip *m_tooltip = nullptr;
|
||||
|
||||
QHash<QString, FunctionInfo> m_functions; // name → info
|
||||
};
|
||||
91
barecode/src/editor/SignatureTooltip.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "SignatureTooltip.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
SignatureTooltip::SignatureTooltip(QWidget *parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
setFrameShadow(QFrame::Raised);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
|
||||
// Dezentes Styling passend zu Hell- und Dunkeltheme
|
||||
setStyleSheet(
|
||||
"SignatureTooltip {"
|
||||
" background: palette(toolTipBase);"
|
||||
" border: 1px solid palette(mid);"
|
||||
" border-radius: 4px;"
|
||||
" padding: 4px;"
|
||||
"}"
|
||||
);
|
||||
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(8, 6, 8, 6);
|
||||
m_layout->setSpacing(3);
|
||||
|
||||
// Signatur — Monospace, deutlich hervorgehoben
|
||||
m_sigLabel = new QLabel(this);
|
||||
m_sigLabel->setTextFormat(Qt::PlainText);
|
||||
m_sigLabel->setWordWrap(false);
|
||||
QFont sigFont = m_sigLabel->font();
|
||||
sigFont.setFamily("Monospace");
|
||||
sigFont.setStyleHint(QFont::Monospace);
|
||||
sigFont.setPointSize(sigFont.pointSize());
|
||||
m_sigLabel->setFont(sigFont);
|
||||
m_sigLabel->setStyleSheet("color: palette(toolTipText); font-weight: bold;");
|
||||
m_layout->addWidget(m_sigLabel);
|
||||
|
||||
// Beschreibung — kleiner, gedimmt
|
||||
m_descLabel = new QLabel(this);
|
||||
m_descLabel->setTextFormat(Qt::PlainText);
|
||||
m_descLabel->setWordWrap(false);
|
||||
m_descLabel->setStyleSheet("color: palette(mid);");
|
||||
QFont descFont = m_descLabel->font();
|
||||
descFont.setPointSize(qMax(descFont.pointSize() - 1, 8));
|
||||
m_descLabel->setFont(descFont);
|
||||
m_layout->addWidget(m_descLabel);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void SignatureTooltip::showSignature(const QString &signature,
|
||||
const QString &description,
|
||||
const QPoint &globalPos)
|
||||
{
|
||||
m_sigLabel->setText(signature);
|
||||
|
||||
if (description.isEmpty())
|
||||
{
|
||||
m_descLabel->hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_descLabel->setText(description);
|
||||
m_descLabel->show();
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
|
||||
// Position so wählen dass das Popup nicht aus dem Bildschirm ragt
|
||||
QPoint pos = globalPos;
|
||||
const QRect screen = QApplication::primaryScreen()->availableGeometry();
|
||||
|
||||
if (pos.x() + width() > screen.right())
|
||||
{
|
||||
pos.setX(screen.right() - width() - 4);
|
||||
}
|
||||
if (pos.y() + height() > screen.bottom())
|
||||
{
|
||||
pos.setY(globalPos.y() - height() - 24);
|
||||
}
|
||||
|
||||
move(pos);
|
||||
show();
|
||||
raise();
|
||||
}
|
||||
|
||||
void SignatureTooltip::hide()
|
||||
{
|
||||
QFrame::hide();
|
||||
}
|
||||
29
barecode/src/editor/SignatureTooltip.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SignatureTooltip – Schwebendes Popup das die Signatur einer Funktion zeigt.
|
||||
// Erscheint unter dem Cursor, verschwindet automatisch wenn der Nutzer
|
||||
// die Klammer schließt oder den Kontext verlässt.
|
||||
// ---------------------------------------------------------------------------
|
||||
class SignatureTooltip : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignatureTooltip(QWidget *parent = nullptr);
|
||||
|
||||
void showSignature(const QString &signature,
|
||||
const QString &description,
|
||||
const QPoint &globalPos);
|
||||
void hide();
|
||||
|
||||
private:
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QLabel *m_sigLabel = nullptr;
|
||||
QLabel *m_descLabel = nullptr;
|
||||
};
|
||||
256
barecode/src/editor/VariableCompleter.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
#include "VariableCompleter.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QKeyEvent>
|
||||
#include <QListWidgetItem>
|
||||
#include <QAbstractItemView>
|
||||
#include <QScrollBar>
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
VariableCompleter::VariableCompleter(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Popup als reines Anzeige-Fenster — dieselbe Fensterart wie
|
||||
// SignatureTooltip, die nachweislich nie den Fokus an sich reißt.
|
||||
// Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst
|
||||
// behandelt der Fenstermanager das Popup als eigenständiges Fenster.
|
||||
// Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste
|
||||
// Popups entstehen konnten, die den Fokus übernehmen und die
|
||||
// Texteingabe blockieren.
|
||||
m_popup = new QListWidget(editor->window());
|
||||
m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
|
||||
m_popup->setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
m_popup->setFocusPolicy(Qt::NoFocus);
|
||||
m_popup->setMaximumHeight(200);
|
||||
m_popup->setMinimumWidth(180);
|
||||
m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_popup->setStyleSheet(
|
||||
"QListWidget {"
|
||||
" border: 1px solid palette(mid);"
|
||||
" background: palette(toolTipBase);"
|
||||
" color: palette(toolTipText);"
|
||||
" font-family: Monospace;"
|
||||
"}"
|
||||
"QListWidget::item:selected {"
|
||||
" background: palette(highlight);"
|
||||
" color: palette(highlightedText);"
|
||||
"}"
|
||||
);
|
||||
|
||||
connect(m_popup, &QListWidget::itemActivated,
|
||||
this, &VariableCompleter::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nach jedem Tastendruck aufrufen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::handleKeyPress(QKeyEvent *event)
|
||||
{
|
||||
// Popup-Navigation
|
||||
if (m_popup->isVisible())
|
||||
{
|
||||
if (event->key() == Qt::Key_Escape)
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
{
|
||||
if (m_popup->currentItem())
|
||||
{
|
||||
onItemActivated(m_popup->currentItem());
|
||||
}
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Down)
|
||||
{
|
||||
const int next = qMin(m_popup->currentRow() + 1,
|
||||
m_popup->count() - 1);
|
||||
m_popup->setCurrentRow(next);
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Up)
|
||||
{
|
||||
const int prev = qMax(m_popup->currentRow() - 1, 0);
|
||||
m_popup->setCurrentRow(prev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Nach dem normalen Tastendruck Popup aktualisieren
|
||||
// (der Event wurde bereits an QPlainTextEdit weitergegeben)
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Popup aktualisieren
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::updatePopup()
|
||||
{
|
||||
const QString prefix = currentPrefix();
|
||||
|
||||
// Nur bei $ und mindestens einem weiteren Zeichen anzeigen
|
||||
if (prefix.length() < 2 || !prefix.startsWith('$'))
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList allVars = collectVariables();
|
||||
const QString filter = prefix.toLower();
|
||||
|
||||
QStringList matches;
|
||||
for (const QString &var : allVars)
|
||||
{
|
||||
if (var.toLower().startsWith(filter) && var != prefix)
|
||||
{
|
||||
matches.append(var);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.isEmpty())
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Popup befüllen
|
||||
m_popup->clear();
|
||||
for (const QString &var : matches)
|
||||
{
|
||||
m_popup->addItem(var);
|
||||
}
|
||||
m_popup->setCurrentRow(0);
|
||||
|
||||
// Größe anpassen
|
||||
const int itemHeight = m_popup->sizeHintForRow(0) + 2;
|
||||
const int height = qMin(matches.size() * itemHeight + 4, 200);
|
||||
m_popup->setFixedHeight(height);
|
||||
|
||||
// Position unter dem Cursor
|
||||
const QRect cursorRect = m_editor->cursorRect();
|
||||
QPoint pos = m_editor->viewport()->mapToGlobal(
|
||||
QPoint(cursorRect.left(), cursorRect.bottom() + 2)
|
||||
);
|
||||
|
||||
// Nicht aus dem Bildschirm herausragen
|
||||
const QRect screen = QApplication::primaryScreen()->availableGeometry();
|
||||
if (pos.x() + m_popup->width() > screen.right())
|
||||
{
|
||||
pos.setX(screen.right() - m_popup->width());
|
||||
}
|
||||
if (pos.y() + height > screen.bottom())
|
||||
{
|
||||
pos.setY(cursorRect.top() - height - 2);
|
||||
}
|
||||
|
||||
m_popup->move(pos);
|
||||
m_popup->show();
|
||||
m_popup->raise();
|
||||
}
|
||||
|
||||
void VariableCompleter::hidePopup()
|
||||
{
|
||||
m_popup->hide();
|
||||
m_popup->clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::notifyFocusLost()
|
||||
{
|
||||
hidePopup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variablen im aktuellen Dokument sammeln
|
||||
// ---------------------------------------------------------------------------
|
||||
QStringList VariableCompleter::collectVariables() const
|
||||
{
|
||||
static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)");
|
||||
|
||||
QSet<QString> seen;
|
||||
QStringList result;
|
||||
|
||||
const QString text = m_editor->toPlainText();
|
||||
QRegularExpressionMatchIterator it = varRegex.globalMatch(text);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString var = it.next().captured(0);
|
||||
if (!seen.contains(var))
|
||||
{
|
||||
seen.insert(var);
|
||||
result.append(var);
|
||||
}
|
||||
}
|
||||
|
||||
result.sort(Qt::CaseInsensitive);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Text links vom Cursor — das angefangene $variable
|
||||
// ---------------------------------------------------------------------------
|
||||
QString VariableCompleter::currentPrefix() const
|
||||
{
|
||||
const QTextCursor cursor = m_editor->textCursor();
|
||||
const QString line = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
if (col == 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Rückwärts gehen bis zum $ oder einem Nicht-Bezeichner-Zeichen
|
||||
int start = col - 1;
|
||||
while (start > 0)
|
||||
{
|
||||
const QChar c = line[start - 1];
|
||||
if (!c.isLetterOrNumber() && c != '_' && c != '$')
|
||||
{
|
||||
break;
|
||||
}
|
||||
--start;
|
||||
}
|
||||
|
||||
const QString token = line.mid(start, col - start);
|
||||
return token.startsWith('$') ? token : QString();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick oder Enter — Variable einfügen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::onItemActivated(QListWidgetItem *item)
|
||||
{
|
||||
if (!item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString selected = item->text();
|
||||
const QString prefix = currentPrefix();
|
||||
|
||||
if (prefix.isEmpty())
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefix durch den vollständigen Variablennamen ersetzen
|
||||
QTextCursor cursor = m_editor->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left,
|
||||
QTextCursor::KeepAnchor,
|
||||
prefix.length());
|
||||
cursor.insertText(selected);
|
||||
|
||||
m_editor->setTextCursor(cursor);
|
||||
hidePopup();
|
||||
}
|
||||
43
barecode/src/editor/VariableCompleter.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QListWidget>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VariableCompleter – Zeigt ein Popup mit passenden Variablennamen
|
||||
// wenn der Nutzer $ tippt und weiter eingibt.
|
||||
//
|
||||
// Kein Autocomplete — nur Anzeige. Klick oder Enter übernimmt.
|
||||
// Escape oder Weiterschreiben ohne Treffer schließt das Popup.
|
||||
// ---------------------------------------------------------------------------
|
||||
class VariableCompleter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VariableCompleter(CodeEditor *editor);
|
||||
|
||||
// Muss nach jedem Tastendruck aufgerufen werden
|
||||
void handleKeyPress(QKeyEvent *event);
|
||||
|
||||
// Muss aufgerufen werden, wenn der Editor den Fokus verliert
|
||||
// (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup,
|
||||
// damit es nicht als "Geisterfenster" stehen bleibt.
|
||||
void notifyFocusLost();
|
||||
|
||||
private slots:
|
||||
void onItemActivated(QListWidgetItem *item);
|
||||
|
||||
private:
|
||||
void updatePopup();
|
||||
void hidePopup();
|
||||
QStringList collectVariables() const;
|
||||
QString currentPrefix() const; // "$me" etc. links vom Cursor
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
QListWidget *m_popup = nullptr;
|
||||
};
|
||||
17
barecode/src/filetree/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
set(FILETREE_SOURCES
|
||||
FileTreePanel.cpp
|
||||
FileTreePanel.h
|
||||
)
|
||||
|
||||
add_library(BareCode_FileTree STATIC ${FILETREE_SOURCES})
|
||||
|
||||
target_link_libraries(BareCode_FileTree PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
target_include_directories(BareCode_FileTree PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
)
|
||||
259
barecode/src/filetree/FileTreePanel.cpp
Normal file
@@ -0,0 +1,259 @@
|
||||
#include "FileTreePanel.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QPoint>
|
||||
|
||||
FileTreePanel::FileTreePanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileTreePanel::setupUi()
|
||||
{
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(0);
|
||||
|
||||
// Small header label showing the project name
|
||||
m_label = new QLabel(tr("Kein Projekt geöffnet"), this);
|
||||
m_label->setContentsMargins(6, 4, 6, 4);
|
||||
m_label->setStyleSheet("font-weight: bold; background: palette(mid);");
|
||||
m_label->setWordWrap(true);
|
||||
m_layout->addWidget(m_label);
|
||||
|
||||
// File system model – show only the project subtree
|
||||
m_model = new QFileSystemModel(this);
|
||||
m_model->setReadOnly(false);
|
||||
m_model->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
|
||||
// Tree view
|
||||
m_tree = new QTreeView(this);
|
||||
m_tree->setModel(m_model);
|
||||
m_tree->setAnimated(true);
|
||||
m_tree->setIndentation(16);
|
||||
m_tree->setSortingEnabled(true);
|
||||
m_tree->sortByColumn(0, Qt::AscendingOrder);
|
||||
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
// Hide all columns except the file name
|
||||
for (int col = 1; col < m_model->columnCount(); ++col)
|
||||
{
|
||||
m_tree->hideColumn(col);
|
||||
}
|
||||
|
||||
m_layout->addWidget(m_tree);
|
||||
|
||||
connect(m_tree, &QTreeView::activated,
|
||||
this, &FileTreePanel::onItemActivated);
|
||||
|
||||
connect(m_tree, &QTreeView::customContextMenuRequested,
|
||||
this, &FileTreePanel::onContextMenuRequested);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileTreePanel::setRootPath(const QString &path)
|
||||
{
|
||||
const QModelIndex root = m_model->setRootPath(path);
|
||||
m_tree->setRootIndex(root);
|
||||
|
||||
const QString projectName = QDir(path).dirName();
|
||||
m_label->setText(projectName.isEmpty() ? path : projectName);
|
||||
}
|
||||
|
||||
void FileTreePanel::clearRoot()
|
||||
{
|
||||
m_model->setRootPath(QString());
|
||||
m_tree->setRootIndex(QModelIndex());
|
||||
m_label->setText(tr("Kein Projekt geöffnet"));
|
||||
}
|
||||
|
||||
void FileTreePanel::triggerNewFile()
|
||||
{
|
||||
onNewFile();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
QString FileTreePanel::selectedDirectory() const
|
||||
{
|
||||
const QModelIndex index = m_tree->currentIndex();
|
||||
if (!index.isValid())
|
||||
{
|
||||
return m_model->rootPath();
|
||||
}
|
||||
|
||||
const QString path = m_model->filePath(index);
|
||||
const QFileInfo info(path);
|
||||
return info.isDir() ? path : info.absolutePath();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileTreePanel::onItemActivated(const QModelIndex &index)
|
||||
{
|
||||
const QString path = m_model->filePath(index);
|
||||
const QFileInfo info(path);
|
||||
|
||||
if (info.isFile())
|
||||
{
|
||||
emit fileActivated(path);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreePanel::onContextMenuRequested(const QPoint &pos)
|
||||
{
|
||||
QMenu menu(this);
|
||||
|
||||
QAction *actNewFile = menu.addAction(tr("Neue Datei…"));
|
||||
QAction *actNewFolder = menu.addAction(tr("Neuer Ordner…"));
|
||||
menu.addSeparator();
|
||||
QAction *actDelete = menu.addAction(tr("Löschen"));
|
||||
|
||||
// Disable delete if nothing is selected
|
||||
const QModelIndex index = m_tree->indexAt(pos);
|
||||
actDelete->setEnabled(index.isValid());
|
||||
|
||||
QAction *chosen = menu.exec(m_tree->viewport()->mapToGlobal(pos));
|
||||
|
||||
if (chosen == actNewFile)
|
||||
{
|
||||
onNewFile();
|
||||
}
|
||||
else if (chosen == actNewFolder)
|
||||
{
|
||||
onNewFolder();
|
||||
}
|
||||
else if (chosen == actDelete)
|
||||
{
|
||||
onDeleteEntry();
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreePanel::onNewFile()
|
||||
{
|
||||
const QString dir = selectedDirectory();
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this,
|
||||
tr("Neue Datei"),
|
||||
tr("Dateiname:"),
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&ok
|
||||
);
|
||||
|
||||
if (!ok || name.trimmed().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString filePath = QDir(dir).filePath(name.trimmed());
|
||||
|
||||
if (QFile::exists(filePath))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Neue Datei"),
|
||||
tr("Eine Datei mit diesem Namen existiert bereits:\n%1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Neue Datei"),
|
||||
tr("Datei konnte nicht angelegt werden:\n%1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
|
||||
emit fileCreated(filePath);
|
||||
|
||||
// Select the new file in the tree
|
||||
const QModelIndex newIndex = m_model->index(filePath);
|
||||
m_tree->setCurrentIndex(newIndex);
|
||||
m_tree->scrollTo(newIndex);
|
||||
}
|
||||
|
||||
void FileTreePanel::onNewFolder()
|
||||
{
|
||||
const QString dir = selectedDirectory();
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this,
|
||||
tr("Neuer Ordner"),
|
||||
tr("Ordnername:"),
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&ok
|
||||
);
|
||||
|
||||
if (!ok || name.trimmed().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDir(dir).mkdir(name.trimmed()))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Neuer Ordner"),
|
||||
tr("Ordner konnte nicht angelegt werden:\n%1")
|
||||
.arg(QDir(dir).filePath(name.trimmed())));
|
||||
}
|
||||
}
|
||||
|
||||
void FileTreePanel::onDeleteEntry()
|
||||
{
|
||||
const QModelIndex index = m_tree->currentIndex();
|
||||
if (!index.isValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = m_model->filePath(index);
|
||||
const QFileInfo info(path);
|
||||
const QString what = info.isDir() ? tr("Ordner") : tr("Datei");
|
||||
|
||||
const auto answer = QMessageBox::question(
|
||||
this,
|
||||
tr("%1 löschen").arg(what),
|
||||
tr("%1 wirklich löschen?\n%2").arg(what, path),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No
|
||||
);
|
||||
|
||||
if (answer != QMessageBox::Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.isDir())
|
||||
{
|
||||
QDir(path).removeRecursively();
|
||||
}
|
||||
else
|
||||
{
|
||||
QFile::remove(path);
|
||||
}
|
||||
}
|
||||
49
barecode/src/filetree/FileTreePanel.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTreeView>
|
||||
#include <QFileSystemModel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileTreePanel – Left panel showing the project directory tree.
|
||||
// Emits fileActivated(path) when the user double-clicks a file.
|
||||
// Supports creating new files/folders via context menu.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FileTreePanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileTreePanel(QWidget *parent = nullptr);
|
||||
|
||||
void setRootPath(const QString &path);
|
||||
void clearRoot();
|
||||
void triggerNewFile(); // Called from MainWindow menu action
|
||||
|
||||
signals:
|
||||
void fileActivated(const QString &filePath);
|
||||
void fileCreated(const QString &filePath);
|
||||
|
||||
private slots:
|
||||
void onItemActivated(const QModelIndex &index);
|
||||
void onContextMenuRequested(const QPoint &pos);
|
||||
void onNewFile();
|
||||
void onNewFolder();
|
||||
void onDeleteEntry();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
// Returns the directory of the currently selected item
|
||||
QString selectedDirectory() const;
|
||||
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QLabel *m_label = nullptr;
|
||||
QTreeView *m_tree = nullptr;
|
||||
QFileSystemModel *m_model = nullptr;
|
||||
};
|
||||
19
barecode/src/highlighter/CMakeLists.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
set(HIGHLIGHTER_SOURCES
|
||||
SyntaxHighlighter.cpp
|
||||
SyntaxHighlighter.h
|
||||
HighlighterFactory.cpp
|
||||
HighlighterFactory.h
|
||||
)
|
||||
|
||||
add_library(BareCode_Highlighter STATIC ${HIGHLIGHTER_SOURCES})
|
||||
|
||||
target_link_libraries(BareCode_Highlighter PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
target_include_directories(BareCode_Highlighter PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
)
|
||||
45
barecode/src/highlighter/HighlighterFactory.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "HighlighterFactory.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <functional>
|
||||
|
||||
SyntaxHighlighter *HighlighterFactory::createForFile(const QString &filePath,
|
||||
QTextDocument *document)
|
||||
{
|
||||
// Erweiterung → Highlighter-Fabrik
|
||||
// Neue Sprache hinzufügen: einfach einen Eintrag ergänzen.
|
||||
static const QHash<QString, std::function<SyntaxHighlighter *(QTextDocument *)>> registry =
|
||||
{
|
||||
// C / C++
|
||||
{ "c", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "cc", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "cpp", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "cxx", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "h", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "hpp", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
{ "hxx", [](QTextDocument *d) { return new CppHighlighter(d); } },
|
||||
|
||||
// CSS
|
||||
{ "css", [](QTextDocument *d) { return new CssHighlighter(d); } },
|
||||
|
||||
// HTML / Templates
|
||||
{ "html", [](QTextDocument *d) { return new HtmlHighlighter(d); } },
|
||||
{ "htm", [](QTextDocument *d) { return new HtmlHighlighter(d); } },
|
||||
{ "xhtml",[](QTextDocument *d) { return new HtmlHighlighter(d); } },
|
||||
|
||||
// PHP (HTML + eingebettetes PHP)
|
||||
{ "php", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "phtml",[](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "php3", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "php4", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "php5", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "php7", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
{ "php8", [](QTextDocument *d) { return new PhpHighlighter(d); } },
|
||||
};
|
||||
|
||||
const QString ext = QFileInfo(filePath).suffix().toLower();
|
||||
const auto it = registry.find(ext);
|
||||
|
||||
return (it != registry.end()) ? it.value()(document) : nullptr;
|
||||
}
|
||||
18
barecode/src/highlighter/HighlighterFactory.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QTextDocument>
|
||||
#include "SyntaxHighlighter.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HighlighterFactory – Maps file extensions to the correct highlighter.
|
||||
// To add a new language, register it in HighlighterFactory.cpp.
|
||||
// ---------------------------------------------------------------------------
|
||||
class HighlighterFactory
|
||||
{
|
||||
public:
|
||||
// Creates and returns a highlighter for the given file path.
|
||||
// Returns nullptr if no highlighter is registered for this file type.
|
||||
static SyntaxHighlighter *createForFile(const QString &filePath,
|
||||
QTextDocument *document);
|
||||
};
|
||||
589
barecode/src/highlighter/SyntaxHighlighter.cpp
Normal file
@@ -0,0 +1,589 @@
|
||||
#include "SyntaxHighlighter.h"
|
||||
#include <QTextDocument>
|
||||
|
||||
// ===========================================================================
|
||||
// SyntaxHighlighter – Basis
|
||||
// ===========================================================================
|
||||
SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent)
|
||||
: QSyntaxHighlighter(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
// Einzel-Zeilen-Regeln anwenden
|
||||
for (const HighlightRule &rule : m_rules)
|
||||
{
|
||||
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
setFormat(
|
||||
static_cast<int>(match.capturedStart()),
|
||||
static_cast<int>(match.capturedLength()),
|
||||
rule.format
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_hasMultiLineComment)
|
||||
{
|
||||
setCurrentBlockState(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mehrzeilige Kommentare
|
||||
// Zustand 0 = normal, 1 = mitten in /* ... */
|
||||
setCurrentBlockState(0);
|
||||
|
||||
// Leere Zeile mitten im Kommentar — Zustand weitertragen
|
||||
if (text.isEmpty())
|
||||
{
|
||||
if (previousBlockState() == 1)
|
||||
{
|
||||
setCurrentBlockState(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = 0;
|
||||
|
||||
if (previousBlockState() == 1)
|
||||
{
|
||||
// Vorherige Zeile war mitten im Kommentar — ab Zeilenanfang nach Ende suchen
|
||||
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, 0);
|
||||
|
||||
if (endMatch.hasMatch())
|
||||
{
|
||||
// Ende des Kommentars gefunden
|
||||
const int commentLength = static_cast<int>(endMatch.capturedStart())
|
||||
+ static_cast<int>(endMatch.capturedLength());
|
||||
setFormat(0, commentLength, m_multiLineCommentFormat);
|
||||
|
||||
// Nach weiteren Kommentaren in der gleichen Zeile suchen
|
||||
QRegularExpressionMatch nextStart =
|
||||
m_commentStartExpression.match(text, commentLength);
|
||||
startIndex = nextStart.hasMatch()
|
||||
? static_cast<int>(nextStart.capturedStart())
|
||||
: -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Noch kein Ende — gesamte Zeile ist Kommentar
|
||||
setFormat(0, text.length(), m_multiLineCommentFormat);
|
||||
setCurrentBlockState(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Neuen Kommentaranfang suchen
|
||||
QRegularExpressionMatch m = m_commentStartExpression.match(text);
|
||||
startIndex = m.hasMatch() ? static_cast<int>(m.capturedStart()) : -1;
|
||||
}
|
||||
|
||||
while (startIndex >= 0)
|
||||
{
|
||||
QRegularExpressionMatch endMatch =
|
||||
m_commentEndExpression.match(text, startIndex);
|
||||
|
||||
int commentLength = 0;
|
||||
|
||||
if (endMatch.hasMatch())
|
||||
{
|
||||
commentLength = static_cast<int>(endMatch.capturedStart())
|
||||
- startIndex
|
||||
+ static_cast<int>(endMatch.capturedLength());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Kommentar geht über Zeilenende
|
||||
setCurrentBlockState(1);
|
||||
commentLength = text.length() - startIndex;
|
||||
}
|
||||
|
||||
setFormat(startIndex, commentLength, m_multiLineCommentFormat);
|
||||
|
||||
if (!endMatch.hasMatch())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Nach weiteren Kommentaren in dieser Zeile suchen
|
||||
QRegularExpressionMatch nextStart =
|
||||
m_commentStartExpression.match(text, startIndex + commentLength);
|
||||
startIndex = nextStart.hasMatch()
|
||||
? static_cast<int>(nextStart.capturedStart())
|
||||
: -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// CppHighlighter
|
||||
// ===========================================================================
|
||||
CppHighlighter::CppHighlighter(QTextDocument *parent)
|
||||
: SyntaxHighlighter(parent)
|
||||
{
|
||||
m_hasMultiLineComment = true;
|
||||
|
||||
QTextCharFormat keywordFormat;
|
||||
keywordFormat.setForeground(QColor("#569CD6"));
|
||||
keywordFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
const QStringList keywords = {
|
||||
"alignas","alignof","and","and_eq","asm","auto","bitand","bitor",
|
||||
"bool","break","case","catch","char","char8_t","char16_t","char32_t",
|
||||
"class","compl","concept","const","consteval","constexpr","constinit",
|
||||
"const_cast","continue","co_await","co_return","co_yield","decltype",
|
||||
"default","delete","do","double","dynamic_cast","else","enum",
|
||||
"explicit","export","extern","false","float","for","friend","goto",
|
||||
"if","inline","int","long","mutable","namespace","new","noexcept",
|
||||
"not","not_eq","nullptr","operator","or","or_eq","private","protected",
|
||||
"public","register","reinterpret_cast","requires","return","short",
|
||||
"signed","sizeof","static","static_assert","static_cast","struct",
|
||||
"switch","template","this","thread_local","throw","true","try",
|
||||
"typedef","typeid","typename","union","unsigned","using","virtual",
|
||||
"void","volatile","wchar_t","while","xor","xor_eq","override","final"
|
||||
};
|
||||
|
||||
for (const QString &kw : keywords)
|
||||
{
|
||||
HighlightRule rule;
|
||||
rule.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw));
|
||||
rule.format = keywordFormat;
|
||||
m_rules.append(rule);
|
||||
}
|
||||
|
||||
QTextCharFormat preprocFormat;
|
||||
preprocFormat.setForeground(QColor("#C586C0"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression("^\\s*#\\s*\\w+"); r.format = preprocFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat stringFormat;
|
||||
stringFormat.setForeground(QColor("#CE9178"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*")"); r.format = stringFormat; m_rules.append(r); }
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"('(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat numberFormat;
|
||||
numberFormat.setForeground(QColor("#B5CEA8"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b(0[xX][0-9A-Fa-f]+[uUlL]*|[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?[fFlLuU]*)\b)"); r.format = numberFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat commentFormat;
|
||||
commentFormat.setForeground(QColor("#6A9955"));
|
||||
commentFormat.setFontItalic(true);
|
||||
{ HighlightRule r; r.pattern = QRegularExpression("//[^\n]*"); r.format = commentFormat; m_rules.append(r); }
|
||||
|
||||
m_multiLineCommentFormat = commentFormat;
|
||||
m_commentStartExpression = QRegularExpression(R"(/\*)");
|
||||
m_commentEndExpression = QRegularExpression(R"(\*/)");
|
||||
}
|
||||
|
||||
void CppHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
SyntaxHighlighter::highlightBlock(text);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// CssHighlighter
|
||||
// ===========================================================================
|
||||
CssHighlighter::CssHighlighter(QTextDocument *parent)
|
||||
: SyntaxHighlighter(parent)
|
||||
{
|
||||
m_hasMultiLineComment = true;
|
||||
|
||||
// Selektoren: .klasse #id element ::pseudo :pseudo
|
||||
QTextCharFormat selectorFormat;
|
||||
selectorFormat.setForeground(QColor("#D7BA7D"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"([.#]?[\w-]+\s*(?=\s*[,{]))"); r.format = selectorFormat; m_rules.append(r); }
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(:{1,2}[\w-]+)"); r.format = selectorFormat; m_rules.append(r); }
|
||||
|
||||
// Eigenschaften (property:)
|
||||
QTextCharFormat propFormat;
|
||||
propFormat.setForeground(QColor("#9CDCFE"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"([\w-]+\s*(?=:))"); r.format = propFormat; m_rules.append(r); }
|
||||
|
||||
// Werte – Farben #hex
|
||||
QTextCharFormat colorFormat;
|
||||
colorFormat.setForeground(QColor("#CE9178"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(#[0-9A-Fa-f]{3,8}\b)"); r.format = colorFormat; m_rules.append(r); }
|
||||
|
||||
// Zahlen + Einheiten
|
||||
QTextCharFormat numberFormat;
|
||||
numberFormat.setForeground(QColor("#B5CEA8"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*(px|em|rem|%|vh|vw|pt|cm|mm|s|ms)?\b)"); r.format = numberFormat; m_rules.append(r); }
|
||||
|
||||
// Strings
|
||||
QTextCharFormat stringFormat;
|
||||
stringFormat.setForeground(QColor("#CE9178"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = stringFormat; m_rules.append(r); }
|
||||
|
||||
// !important
|
||||
QTextCharFormat importantFormat;
|
||||
importantFormat.setForeground(QColor("#F44747"));
|
||||
importantFormat.setFontWeight(QFont::Bold);
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(!important)"); r.format = importantFormat; m_rules.append(r); }
|
||||
|
||||
// @-Regeln
|
||||
QTextCharFormat atFormat;
|
||||
atFormat.setForeground(QColor("#C586C0"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(@[\w-]+)"); r.format = atFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat commentFormat;
|
||||
commentFormat.setForeground(QColor("#6A9955"));
|
||||
commentFormat.setFontItalic(true);
|
||||
m_multiLineCommentFormat = commentFormat;
|
||||
m_commentStartExpression = QRegularExpression(R"(/\*)");
|
||||
m_commentEndExpression = QRegularExpression(R"(\*/)");
|
||||
}
|
||||
|
||||
void CssHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
SyntaxHighlighter::highlightBlock(text);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// HtmlHighlighter
|
||||
// ===========================================================================
|
||||
HtmlHighlighter::HtmlHighlighter(QTextDocument *parent)
|
||||
: SyntaxHighlighter(parent)
|
||||
{
|
||||
// Tag-Namen <div </div />
|
||||
QTextCharFormat tagFormat;
|
||||
tagFormat.setForeground(QColor("#569CD6"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(/?>)"); r.format = tagFormat; m_rules.append(r); }
|
||||
|
||||
// Attribute name=
|
||||
QTextCharFormat attrFormat;
|
||||
attrFormat.setForeground(QColor("#9CDCFE"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); }
|
||||
|
||||
// Attributwerte "wert" 'wert'
|
||||
QTextCharFormat valueFormat;
|
||||
valueFormat.setForeground(QColor("#CE9178"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); }
|
||||
|
||||
// DOCTYPE
|
||||
QTextCharFormat doctypeFormat;
|
||||
doctypeFormat.setForeground(QColor("#808080"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(<!DOCTYPE[^>]*>)", QRegularExpression::CaseInsensitiveOption); r.format = doctypeFormat; m_rules.append(r); }
|
||||
|
||||
// Entities & {
|
||||
QTextCharFormat entityFormat;
|
||||
entityFormat.setForeground(QColor("#D7BA7D"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); }
|
||||
|
||||
// Kommentare <!-- ... --> (mehrzeilig)
|
||||
QTextCharFormat commentFormat;
|
||||
commentFormat.setForeground(QColor("#6A9955"));
|
||||
commentFormat.setFontItalic(true);
|
||||
m_multiLineCommentFormat = commentFormat;
|
||||
m_commentStartExpression = QRegularExpression("<!--");
|
||||
m_commentEndExpression = QRegularExpression("-->");
|
||||
m_hasMultiLineComment = true;
|
||||
}
|
||||
|
||||
void HtmlHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
SyntaxHighlighter::highlightBlock(text);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// PhpHighlighter
|
||||
// ===========================================================================
|
||||
PhpHighlighter::PhpHighlighter(QTextDocument *parent)
|
||||
: SyntaxHighlighter(parent)
|
||||
{
|
||||
// ---- HTML-Regeln (Basis, für den Teil außerhalb von <?php ?>) ----
|
||||
|
||||
QTextCharFormat tagFormat;
|
||||
tagFormat.setForeground(QColor("#569CD6"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(</?[\w:-]+)"); r.format = tagFormat; m_rules.append(r); }
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(/?>)"); r.format = tagFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat attrFormat;
|
||||
attrFormat.setForeground(QColor("#9CDCFE"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b[\w:-]+=)"); r.format = attrFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat valueFormat;
|
||||
valueFormat.setForeground(QColor("#CE9178"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = valueFormat; m_rules.append(r); }
|
||||
|
||||
QTextCharFormat entityFormat;
|
||||
entityFormat.setForeground(QColor("#D7BA7D"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(&(?:#\d+|#x[0-9A-Fa-f]+|[\w]+);)"); r.format = entityFormat; m_rules.append(r); }
|
||||
|
||||
// HTML-Kommentare
|
||||
QTextCharFormat htmlCommentFormat;
|
||||
htmlCommentFormat.setForeground(QColor("#6A9955"));
|
||||
htmlCommentFormat.setFontItalic(true);
|
||||
m_multiLineCommentFormat = htmlCommentFormat;
|
||||
m_commentStartExpression = QRegularExpression("<!--");
|
||||
m_commentEndExpression = QRegularExpression("-->");
|
||||
m_hasMultiLineComment = true;
|
||||
|
||||
// ---- PHP-Tags hervorheben ----
|
||||
m_phpTagFormat.setForeground(QColor("#C586C0"));
|
||||
m_phpTagFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
// ---- PHP-spezifische Regeln ----
|
||||
m_phpStringFormat.setForeground(QColor("#CE9178"));
|
||||
m_phpCommentFormat.setForeground(QColor("#6A9955"));
|
||||
m_phpCommentFormat.setFontItalic(true);
|
||||
|
||||
// Keywords
|
||||
QTextCharFormat kwFormat;
|
||||
kwFormat.setForeground(QColor("#569CD6"));
|
||||
kwFormat.setFontWeight(QFont::Bold);
|
||||
|
||||
const QStringList phpKeywords = {
|
||||
"abstract","and","array","as","break","callable","case","catch",
|
||||
"class","clone","const","continue","declare","default","die","do",
|
||||
"echo","else","elseif","empty","enddeclare","endfor","endforeach",
|
||||
"endif","endswitch","endwhile","enum","extends","final","finally",
|
||||
"fn","for","foreach","function","global","goto","if","implements",
|
||||
"include","include_once","instanceof","insteadof","interface",
|
||||
"isset","list","match","namespace","new","or","print","private",
|
||||
"protected","public","readonly","require","require_once","return",
|
||||
"static","switch","throw","trait","try","unset","use","var",
|
||||
"while","xor","yield","null","true","false","NULL","TRUE","FALSE"
|
||||
};
|
||||
|
||||
for (const QString &kw : phpKeywords)
|
||||
{
|
||||
HighlightRule r;
|
||||
r.pattern = QRegularExpression(QString("\\b%1\\b").arg(kw));
|
||||
r.format = kwFormat;
|
||||
m_phpRules.append(r);
|
||||
}
|
||||
|
||||
// Variablen $var
|
||||
QTextCharFormat varFormat;
|
||||
varFormat.setForeground(QColor("#9CDCFE"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\$[\w]+)"); r.format = varFormat; m_phpRules.append(r); }
|
||||
|
||||
// Strings
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')"); r.format = m_phpStringFormat; m_phpRules.append(r); }
|
||||
|
||||
// Zahlen
|
||||
QTextCharFormat numFormat;
|
||||
numFormat.setForeground(QColor("#B5CEA8"));
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"(\b\d+\.?\d*\b)"); r.format = numFormat; m_phpRules.append(r); }
|
||||
|
||||
// Einzeilige Kommentare
|
||||
{ HighlightRule r; r.pattern = QRegularExpression(R"((//|#)[^\n]*)"); r.format = m_phpCommentFormat; m_phpRules.append(r); }
|
||||
|
||||
// Eingebaute Funktionen (Auswahl der häufigsten)
|
||||
QTextCharFormat builtinFormat;
|
||||
builtinFormat.setForeground(QColor("#DCDCAA"));
|
||||
const QStringList builtins = {
|
||||
"array_map","array_filter","array_keys","array_values","array_merge",
|
||||
"array_push","array_pop","array_shift","array_slice","array_splice",
|
||||
"count","strlen","substr","strpos","strtolower","strtoupper","trim",
|
||||
"ltrim","rtrim","explode","implode","str_replace","preg_match",
|
||||
"preg_replace","sprintf","printf","print_r","var_dump","isset",
|
||||
"empty","unset","intval","floatval","strval","is_array","is_string",
|
||||
"is_int","is_float","is_null","is_bool","is_numeric","date","time",
|
||||
"mktime","json_encode","json_decode","header","session_start",
|
||||
"htmlspecialchars","htmlentities","strip_tags","nl2br","round",
|
||||
"floor","ceil","abs","min","max","rand","in_array","array_key_exists",
|
||||
"sort","rsort","usort","ksort","krsort","ob_start","ob_get_clean"
|
||||
};
|
||||
|
||||
for (const QString &fn : builtins)
|
||||
{
|
||||
HighlightRule r;
|
||||
r.pattern = QRegularExpression(QString("\\b%1\\b").arg(fn));
|
||||
r.format = builtinFormat;
|
||||
m_phpRules.append(r);
|
||||
}
|
||||
}
|
||||
|
||||
void PhpHighlighter::highlightPhpRange(const QString &text, int start, int length)
|
||||
{
|
||||
if (length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString phpText = text.mid(start, length);
|
||||
|
||||
for (const HighlightRule &rule : m_phpRules)
|
||||
{
|
||||
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(phpText);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
setFormat(
|
||||
start + static_cast<int>(match.capturedStart()),
|
||||
static_cast<int>(match.capturedLength()),
|
||||
rule.format
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const QRegularExpression blockOpen(R"(/\*)");
|
||||
static const QRegularExpression blockClose(R"(\*/)");
|
||||
|
||||
int searchFrom = 0;
|
||||
|
||||
// Leere Zeile mitten im PHP-Blockkommentar — Zustand weitertragen
|
||||
if (phpText.trimmed().isEmpty())
|
||||
{
|
||||
if (previousBlockState() == 3)
|
||||
{
|
||||
setFormat(start, length, m_phpCommentFormat);
|
||||
setCurrentBlockState(3);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousBlockState() == 3)
|
||||
{
|
||||
QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0);
|
||||
if (closeMatch.hasMatch())
|
||||
{
|
||||
const int end = static_cast<int>(closeMatch.capturedStart())
|
||||
+ static_cast<int>(closeMatch.capturedLength());
|
||||
setFormat(start, end, m_phpCommentFormat);
|
||||
searchFrom = end;
|
||||
}
|
||||
else
|
||||
{
|
||||
setFormat(start, length, m_phpCommentFormat);
|
||||
setCurrentBlockState(3);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (searchFrom < phpText.length())
|
||||
{
|
||||
QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom);
|
||||
if (!openMatch.hasMatch())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const int openPos = static_cast<int>(openMatch.capturedStart());
|
||||
QRegularExpressionMatch closeMatch = blockClose.match(phpText, openPos + 2);
|
||||
|
||||
if (closeMatch.hasMatch())
|
||||
{
|
||||
const int closeEnd = static_cast<int>(closeMatch.capturedStart())
|
||||
+ static_cast<int>(closeMatch.capturedLength());
|
||||
setFormat(start + openPos, closeEnd - openPos, m_phpCommentFormat);
|
||||
searchFrom = closeEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
setFormat(start + openPos, length - openPos, m_phpCommentFormat);
|
||||
setCurrentBlockState(3);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhpHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
|
||||
// (setzt auch den State für HTML <!-- --> Kommentare)
|
||||
SyntaxHighlighter::highlightBlock(text);
|
||||
|
||||
// Block-Zustände:
|
||||
// 0 = HTML-Modus
|
||||
// 1 = HTML <!-- --> Kommentar (von Basisklasse verwaltet)
|
||||
// 2 = innerhalb PHP-Block (kein /* */ Kommentar)
|
||||
// 3 = innerhalb PHP /* */ Block-Kommentar
|
||||
|
||||
// State 1 (HTML-Kommentar) wurde von der Basisklasse gesetzt — nicht überschreiben
|
||||
if (currentBlockState() == 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn der vorherige Block ein HTML-Kommentar war und dieser noch nicht
|
||||
// abgeschlossen wurde, hat die Basisklasse das bereits korrekt behandelt.
|
||||
// Wir setzen nur dann auf 0 zurück wenn wir sicher nicht in HTML-Kommentar sind.
|
||||
if (previousBlockState() != 1)
|
||||
{
|
||||
setCurrentBlockState(0);
|
||||
}
|
||||
|
||||
// Leere Zeile — Zustand weitertragen
|
||||
if (text.isEmpty())
|
||||
{
|
||||
const int prev = previousBlockState();
|
||||
if (prev == 2 || prev == 3)
|
||||
{
|
||||
setCurrentBlockState(prev);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static const QRegularExpression phpClose(R"(\?>)");
|
||||
|
||||
int pos = 0;
|
||||
|
||||
if (previousBlockState() == 2 || previousBlockState() == 3)
|
||||
{
|
||||
// Wir befinden uns bereits in einem PHP-Block (ggf. in einem Kommentar)
|
||||
QRegularExpressionMatch closeMatch = phpClose.match(text, 0);
|
||||
if (closeMatch.hasMatch())
|
||||
{
|
||||
const int end = static_cast<int>(closeMatch.capturedStart())
|
||||
+ static_cast<int>(closeMatch.capturedLength());
|
||||
highlightPhpRange(text, 0, end);
|
||||
setFormat(static_cast<int>(closeMatch.capturedStart()),
|
||||
static_cast<int>(closeMatch.capturedLength()),
|
||||
m_phpTagFormat);
|
||||
pos = end;
|
||||
setCurrentBlockState(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
highlightPhpRange(text, 0, text.length());
|
||||
if (currentBlockState() != 3)
|
||||
{
|
||||
setCurrentBlockState(2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (pos < text.length())
|
||||
{
|
||||
QRegularExpressionMatch openMatch = phpOpen.match(text, pos);
|
||||
if (!openMatch.hasMatch())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const int openStart = static_cast<int>(openMatch.capturedStart());
|
||||
const int openEnd = openStart + static_cast<int>(openMatch.capturedLength());
|
||||
|
||||
setFormat(openStart, static_cast<int>(openMatch.capturedLength()), m_phpTagFormat);
|
||||
|
||||
QRegularExpressionMatch closeMatch = phpClose.match(text, openEnd);
|
||||
if (closeMatch.hasMatch())
|
||||
{
|
||||
const int closeStart = static_cast<int>(closeMatch.capturedStart());
|
||||
const int closeEnd = closeStart + static_cast<int>(closeMatch.capturedLength());
|
||||
|
||||
highlightPhpRange(text, openEnd, closeStart - openEnd);
|
||||
setFormat(closeStart, static_cast<int>(closeMatch.capturedLength()), m_phpTagFormat);
|
||||
|
||||
pos = closeEnd;
|
||||
setCurrentBlockState(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
highlightPhpRange(text, openEnd, text.length() - openEnd);
|
||||
if (currentBlockState() != 3)
|
||||
{
|
||||
setCurrentBlockState(2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
barecode/src/highlighter/SyntaxHighlighter.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextCharFormat>
|
||||
#include <QRegularExpression>
|
||||
#include <QVector>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SyntaxHighlighter – Regelbasierte Basis-Klasse.
|
||||
// Unterklassen befüllen m_rules und können highlightBlock() überschreiben
|
||||
// um mehrzeilige Konstrukte (Block-Kommentare, heredocs, …) zu behandeln.
|
||||
// ---------------------------------------------------------------------------
|
||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SyntaxHighlighter(QTextDocument *parent = nullptr);
|
||||
|
||||
protected:
|
||||
struct HighlightRule
|
||||
{
|
||||
QRegularExpression pattern;
|
||||
QTextCharFormat format;
|
||||
};
|
||||
|
||||
void highlightBlock(const QString &text) override;
|
||||
|
||||
// Unterklassen befüllen dies im Konstruktor
|
||||
QVector<HighlightRule> m_rules;
|
||||
|
||||
// Mehrzeilige Block-Kommentare (/* ... */)
|
||||
QRegularExpression m_commentStartExpression;
|
||||
QRegularExpression m_commentEndExpression;
|
||||
QTextCharFormat m_multiLineCommentFormat;
|
||||
bool m_hasMultiLineComment = false;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CppHighlighter – C und C++
|
||||
// ---------------------------------------------------------------------------
|
||||
class CppHighlighter : public SyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CppHighlighter(QTextDocument *parent = nullptr);
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CssHighlighter – CSS
|
||||
// ---------------------------------------------------------------------------
|
||||
class CssHighlighter : public SyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CssHighlighter(QTextDocument *parent = nullptr);
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HtmlHighlighter – HTML (mit eingebettetem CSS in <style> und JS in <script>)
|
||||
// ---------------------------------------------------------------------------
|
||||
class HtmlHighlighter : public SyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HtmlHighlighter(QTextDocument *parent = nullptr);
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PhpHighlighter – PHP (HTML + eingebettetes PHP zwischen <?php ... ?>)
|
||||
// ---------------------------------------------------------------------------
|
||||
class PhpHighlighter : public SyntaxHighlighter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PhpHighlighter(QTextDocument *parent = nullptr);
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
|
||||
private:
|
||||
// Hilfsmethode: wendet PHP-Regeln auf einen Teilbereich an
|
||||
void highlightPhpRange(const QString &text, int start, int length);
|
||||
|
||||
QVector<HighlightRule> m_phpRules;
|
||||
QTextCharFormat m_phpStringFormat;
|
||||
QTextCharFormat m_phpCommentFormat;
|
||||
QTextCharFormat m_phpTagFormat;
|
||||
};
|
||||
198
barecode/translations/barecode_de.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="de_DE" sourcelanguage="de_DE">
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message><source>&Datei</source><translation>&Datei</translation></message>
|
||||
<message><source>&Neue Datei…</source><translation>&Neue Datei…</translation></message>
|
||||
<message><source>Datei &öffnen…</source><translation>Datei &öffnen…</translation></message>
|
||||
<message><source>&Projekt öffnen…</source><translation>&Projekt öffnen…</translation></message>
|
||||
<message><source>Projekt &schließen</source><translation>Projekt &schließen</translation></message>
|
||||
<message><source>&Speichern</source><translation>&Speichern</translation></message>
|
||||
<message><source>Speichern &unter…</source><translation>Speichern &unter…</translation></message>
|
||||
<message><source>&Alles speichern</source><translation>&Alles speichern</translation></message>
|
||||
<message><source>&Beenden</source><translation>&Beenden</translation></message>
|
||||
<message><source>&Bearbeiten</source><translation>&Bearbeiten</translation></message>
|
||||
<message><source>&Rückgängig</source><translation>&Rückgängig</translation></message>
|
||||
<message><source>&Wiederholen</source><translation>&Wiederholen</translation></message>
|
||||
<message><source>&Suchen / Ersetzen…</source><translation>&Suchen / Ersetzen…</translation></message>
|
||||
<message><source>In &Dateien suchen…</source><translation>In &Dateien suchen…</translation></message>
|
||||
<message><source>&Projektfunktionen…</source><translation>&Projektfunktionen…</translation></message>
|
||||
<message><source>&Tote Funktionen suchen…</source><translation>&Tote Funktionen suchen…</translation></message>
|
||||
<message><source>&Ansicht</source><translation>&Ansicht</translation></message>
|
||||
<message><source>&Dark Mode</source><translation>&Dark Mode</translation></message>
|
||||
<message><source>Sprache</source><translation>Sprache</translation></message>
|
||||
<message><source>&Hilfe</source><translation>&Hilfe</translation></message>
|
||||
<message><source>&Über BareCode…</source><translation>&Über BareCode…</translation></message>
|
||||
<message><source>Bereit</source><translation>Bereit</translation></message>
|
||||
<message><source>Alle Dateien gespeichert</source><translation>Alle Dateien gespeichert</translation></message>
|
||||
<message><source>Gespeichert: %1</source><translation>Gespeichert: %1</translation></message>
|
||||
<message><source>Projekt geöffnet: %1</source><translation>Projekt geöffnet: %1</translation></message>
|
||||
<message><source>Projekt geschlossen</source><translation>Projekt geschlossen</translation></message>
|
||||
<message><source>Sprache geändert</source><translation>Sprache geändert</translation></message>
|
||||
<message><source>Die Sprache wird beim nächsten Start von BareCode aktiv.</source><translation>Die Sprache wird beim nächsten Start von BareCode aktiv.</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AboutDialog</name>
|
||||
<message><source>Über BareCode</source><translation>Über BareCode</translation></message>
|
||||
<message><source>Modularer Code-Editor</source><translation>Modularer Code-Editor</translation></message>
|
||||
<message><source>Version</source><translation>Version</translation></message>
|
||||
<message><source>Entwickler</source><translation>Entwickler</translation></message>
|
||||
<message><source>Projekt</source><translation>Projekt</translation></message>
|
||||
<message><source>Framework</source><translation>Framework</translation></message>
|
||||
<message><source>Sprache</source><translation>Sprache</translation></message>
|
||||
<message><source>Schließen</source><translation>Schließen</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FileTreePanel</name>
|
||||
<message><source>Kein Projekt geöffnet</source><translation>Kein Projekt geöffnet</translation></message>
|
||||
<message><source>Neue Datei…</source><translation>Neue Datei…</translation></message>
|
||||
<message><source>Neuer Ordner…</source><translation>Neuer Ordner…</translation></message>
|
||||
<message><source>Löschen</source><translation>Löschen</translation></message>
|
||||
<message><source>Neue Datei</source><translation>Neue Datei</translation></message>
|
||||
<message><source>Dateiname:</source><translation>Dateiname:</translation></message>
|
||||
<message><source>Eine Datei mit diesem Namen existiert bereits:
|
||||
%1</source><translation>Eine Datei mit diesem Namen existiert bereits:
|
||||
%1</translation></message>
|
||||
<message><source>Datei konnte nicht angelegt werden:
|
||||
%1</source><translation>Datei konnte nicht angelegt werden:
|
||||
%1</translation></message>
|
||||
<message><source>Neuer Ordner</source><translation>Neuer Ordner</translation></message>
|
||||
<message><source>Ordnername:</source><translation>Ordnername:</translation></message>
|
||||
<message><source>Ordner konnte nicht angelegt werden:
|
||||
%1</source><translation>Ordner konnte nicht angelegt werden:
|
||||
%1</translation></message>
|
||||
<message><source>%1 löschen</source><translation>%1 löschen</translation></message>
|
||||
<message><source>%1 wirklich löschen?
|
||||
%2</source><translation>%1 wirklich löschen?
|
||||
%2</translation></message>
|
||||
<message><source>Datei</source><translation>Datei</translation></message>
|
||||
<message><source>Ordner</source><translation>Ordner</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditorTab</name>
|
||||
<message><source>Speichern</source><translation>Speichern</translation></message>
|
||||
<message><source>Datei konnte nicht gespeichert werden:
|
||||
%1</source><translation>Datei konnte nicht gespeichert werden:
|
||||
%1</translation></message>
|
||||
<message><source>Speichern unter</source><translation>Speichern unter</translation></message>
|
||||
<message><source>Datei konnte nicht geschrieben werden:
|
||||
%1</source><translation>Datei konnte nicht geschrieben werden:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CodeEditor</name>
|
||||
<message><source>Datei öffnen</source><translation>Datei öffnen</translation></message>
|
||||
<message><source>Open File</source><translation>Datei öffnen</translation></message>
|
||||
<message><source>Cannot open file:
|
||||
%1</source><translation>Datei kann nicht geöffnet werden:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SearchPanel</name>
|
||||
<message><source>Suchen…</source><translation>Suchen…</translation></message>
|
||||
<message><source>Suchen:</source><translation>Suchen:</translation></message>
|
||||
<message><source>▲</source><translation>▲</translation></message>
|
||||
<message><source>▼</source><translation>▼</translation></message>
|
||||
<message><source>Vorheriger Treffer (Shift+F3)</source><translation>Vorheriger Treffer (Shift+F3)</translation></message>
|
||||
<message><source>Nächster Treffer (F3)</source><translation>Nächster Treffer (F3)</translation></message>
|
||||
<message><source>Schließen (Esc)</source><translation>Schließen (Esc)</translation></message>
|
||||
<message><source>✕</source><translation>✕</translation></message>
|
||||
<message><source>Ersetzen durch…</source><translation>Ersetzen durch…</translation></message>
|
||||
<message><source>Ersetzen:</source><translation>Ersetzen:</translation></message>
|
||||
<message><source>Ersetzen</source><translation>Ersetzen</translation></message>
|
||||
<message><source>Alle ersetzen</source><translation>Alle ersetzen</translation></message>
|
||||
<message><source>In Auswahl ersetzen</source><translation>In Auswahl ersetzen</translation></message>
|
||||
<message><source>Groß-/Kleinschreibung</source><translation>Groß-/Kleinschreibung</translation></message>
|
||||
<message><source>Ganzes Wort</source><translation>Ganzes Wort</translation></message>
|
||||
<message><source>Regulärer Ausdruck</source><translation>Regulärer Ausdruck</translation></message>
|
||||
<message><source>Kein Treffer</source><translation>Kein Treffer</translation></message>
|
||||
<message><source>%1 Treffer</source><translation>%1 Treffer</translation></message>
|
||||
<message><source>%1 Ersetzung(en) durchgeführt.</source><translation>%1 Ersetzung(en) durchgeführt.</translation></message>
|
||||
<message><source>Es ist kein Text ausgewählt.</source><translation>Es ist kein Text ausgewählt.</translation></message>
|
||||
<message><source>%1 Ersetzung(en) in der Auswahl durchgeführt.</source><translation>%1 Ersetzung(en) in der Auswahl durchgeführt.</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FileSearchPanel</name>
|
||||
<message><source>Suchen in Dateien:</source><translation>Suchen in Dateien:</translation></message>
|
||||
<message><source>Suchbegriff…</source><translation>Suchbegriff…</translation></message>
|
||||
<message><source>Suchen</source><translation>Suchen</translation></message>
|
||||
<message><source>✕</source><translation>✕</translation></message>
|
||||
<message><source>Schließen</source><translation>Schließen</translation></message>
|
||||
<message><source>Groß-/Kleinschreibung</source><translation>Groß-/Kleinschreibung</translation></message>
|
||||
<message><source>Ganzes Wort</source><translation>Ganzes Wort</translation></message>
|
||||
<message><source>Regex</source><translation>Regex</translation></message>
|
||||
<message><source>Dateitypen:</source><translation>Dateitypen:</translation></message>
|
||||
<message><source>Leerzeichen-getrennte Muster, z.B.: *.php *.html</source><translation>Leerzeichen-getrennte Muster, z.B.: *.php *.html</translation></message>
|
||||
<message><source>Suche läuft…</source><translation>Suche läuft…</translation></message>
|
||||
<message><source>Kein Projektverzeichnis geöffnet.</source><translation>Kein Projektverzeichnis geöffnet.</translation></message>
|
||||
<message><source>Keine Treffer gefunden.</source><translation>Keine Treffer gefunden.</translation></message>
|
||||
<message><source>%1 Treffer in %2 Datei(en).</source><translation>%1 Treffer in %2 Datei(en).</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FunctionListPanel</name>
|
||||
<message><source>Projektfunktionen</source><translation>Projektfunktionen</translation></message>
|
||||
<message><source>↻</source><translation>↻</translation></message>
|
||||
<message><source>Neu scannen</source><translation>Neu scannen</translation></message>
|
||||
<message><source>Funktion suchen…</source><translation>Funktion suchen…</translation></message>
|
||||
<message><source>Nach Datei</source><translation>Nach Datei</translation></message>
|
||||
<message><source>Nach Klasse</source><translation>Nach Klasse</translation></message>
|
||||
<message><source>Alphabetisch</source><translation>Alphabetisch</translation></message>
|
||||
<message><source>Kein Projekt geöffnet.</source><translation>Kein Projekt geöffnet.</translation></message>
|
||||
<message><source>Scanne…</source><translation>Scanne…</translation></message>
|
||||
<message><source>%1 Funktionen gefunden</source><translation>%1 Funktionen gefunden</translation></message>
|
||||
<message><source>%1 von %2 Funktionen</source><translation>%1 von %2 Funktionen</translation></message>
|
||||
<message><source>(global)</source><translation>(global)</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FunctionListDialog</name>
|
||||
<message><source>Projektfunktionen – BareCode</source><translation>Projektfunktionen – BareCode</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DeadCodeAnalyzer</name>
|
||||
<message><source>Globale Funktion — kein Aufruf gefunden</source><translation>Globale Funktion — kein Aufruf gefunden</translation></message>
|
||||
<message><source>Methode von %1 — kein Aufruf gefunden</source><translation>Methode von %1 — kein Aufruf gefunden</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DeadCodeDialog</name>
|
||||
<message><source>Tote Funktionen – BareCode</source><translation>Tote Funktionen – BareCode</translation></message>
|
||||
<message><source>Kandidatenliste — kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</source><translation>Kandidatenliste — kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</translation></message>
|
||||
<message><source>Ausschließen:</source><translation>Ausschließen:</translation></message>
|
||||
<message><source>vendor lib node_modules (leerzeichen-getrennt)</source><translation>vendor lib node_modules (leerzeichen-getrennt)</translation></message>
|
||||
<message><source>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</source><translation>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</translation></message>
|
||||
<message><source>+ Verzeichnis</source><translation>+ Verzeichnis</translation></message>
|
||||
<message><source>Ausschlussliste leeren</source><translation>Ausschlussliste leeren</translation></message>
|
||||
<message><source>Verzeichnis ausschließen</source><translation>Verzeichnis ausschließen</translation></message>
|
||||
<message><source>Analyse starten</source><translation>Analyse starten</translation></message>
|
||||
<message><source>Als TXT exportieren</source><translation>Als TXT exportieren</translation></message>
|
||||
<message><source>Analysiere…</source><translation>Analysiere…</translation></message>
|
||||
<message><source>Dateien werden gezählt…</source><translation>Dateien werden gezählt…</translation></message>
|
||||
<message><source>(%1 / %2) %3</source><translation>(%1 / %2) %3</translation></message>
|
||||
<message><source>Funktion</source><translation>Funktion</translation></message>
|
||||
<message><source>Klasse</source><translation>Klasse</translation></message>
|
||||
<message><source>Datei</source><translation>Datei</translation></message>
|
||||
<message><source>Zeile</source><translation>Zeile</translation></message>
|
||||
<message><source>(global)</source><translation>(global)</translation></message>
|
||||
<message><source>✓ Keine ungenutzten Funktionen gefunden.</source><translation>✓ Keine ungenutzten Funktionen gefunden.</translation></message>
|
||||
<message><source>%1 Kandidaten gefunden</source><translation>%1 Kandidaten gefunden</translation></message>
|
||||
<message><source>Analyse</source><translation>Analyse</translation></message>
|
||||
<message><source>Bitte zuerst ein Projekt öffnen.</source><translation>Bitte zuerst ein Projekt öffnen.</translation></message>
|
||||
<message><source>Ergebnis exportieren</source><translation>Ergebnis exportieren</translation></message>
|
||||
<message><source>Textdateien (*.txt)</source><translation>Textdateien (*.txt)</translation></message>
|
||||
<message><source>Export</source><translation>Export</translation></message>
|
||||
<message><source>Datei konnte nicht geschrieben werden:
|
||||
%1</source><translation>Datei konnte nicht geschrieben werden:
|
||||
%1</translation></message>
|
||||
<message><source>Exportiert nach:
|
||||
%1</source><translation>Exportiert nach:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ColorIndicator</name>
|
||||
<message><source>Farbe wählen</source><translation>Farbe wählen</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SignatureTooltip</name>
|
||||
<message><source>Farbe wählen</source><translation>Farbe wählen</translation></message>
|
||||
</context>
|
||||
</TS>
|
||||
194
barecode/translations/barecode_en.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="en_US" sourcelanguage="de_DE">
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message><source>&Datei</source><translation>&File</translation></message>
|
||||
<message><source>&Neue Datei…</source><translation>&New File…</translation></message>
|
||||
<message><source>Datei &öffnen…</source><translation>&Open File…</translation></message>
|
||||
<message><source>&Projekt öffnen…</source><translation>Open &Project…</translation></message>
|
||||
<message><source>Projekt &schließen</source><translation>&Close Project</translation></message>
|
||||
<message><source>&Speichern</source><translation>&Save</translation></message>
|
||||
<message><source>Speichern &unter…</source><translation>Save &As…</translation></message>
|
||||
<message><source>&Alles speichern</source><translation>Save A&ll</translation></message>
|
||||
<message><source>&Beenden</source><translation>&Quit</translation></message>
|
||||
<message><source>&Bearbeiten</source><translation>&Edit</translation></message>
|
||||
<message><source>&Rückgängig</source><translation>&Undo</translation></message>
|
||||
<message><source>&Wiederholen</source><translation>&Redo</translation></message>
|
||||
<message><source>&Suchen / Ersetzen…</source><translation>&Find / Replace…</translation></message>
|
||||
<message><source>In &Dateien suchen…</source><translation>Find in &Files…</translation></message>
|
||||
<message><source>&Projektfunktionen…</source><translation>&Project Functions…</translation></message>
|
||||
<message><source>&Tote Funktionen suchen…</source><translation>Find &Dead Code…</translation></message>
|
||||
<message><source>&Ansicht</source><translation>&View</translation></message>
|
||||
<message><source>&Dark Mode</source><translation>&Dark Mode</translation></message>
|
||||
<message><source>Sprache</source><translation>Language</translation></message>
|
||||
<message><source>&Hilfe</source><translation>&Help</translation></message>
|
||||
<message><source>&Über BareCode…</source><translation>&About BareCode…</translation></message>
|
||||
<message><source>Bereit</source><translation>Ready</translation></message>
|
||||
<message><source>Alle Dateien gespeichert</source><translation>All files saved</translation></message>
|
||||
<message><source>Gespeichert: %1</source><translation>Saved: %1</translation></message>
|
||||
<message><source>Projekt geöffnet: %1</source><translation>Project opened: %1</translation></message>
|
||||
<message><source>Projekt geschlossen</source><translation>Project closed</translation></message>
|
||||
<message><source>Sprache geändert</source><translation>Language changed</translation></message>
|
||||
<message><source>Die Sprache wird beim nächsten Start von BareCode aktiv.</source><translation>The language will be applied the next time BareCode starts.</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>AboutDialog</name>
|
||||
<message><source>Über BareCode</source><translation>About BareCode</translation></message>
|
||||
<message><source>Modularer Code-Editor</source><translation>Modular Code Editor</translation></message>
|
||||
<message><source>Version</source><translation>Version</translation></message>
|
||||
<message><source>Entwickler</source><translation>Developer</translation></message>
|
||||
<message><source>Projekt</source><translation>Project</translation></message>
|
||||
<message><source>Framework</source><translation>Framework</translation></message>
|
||||
<message><source>Sprache</source><translation>Language</translation></message>
|
||||
<message><source>Schließen</source><translation>Close</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FileTreePanel</name>
|
||||
<message><source>Kein Projekt geöffnet</source><translation>No project open</translation></message>
|
||||
<message><source>Neue Datei…</source><translation>New File…</translation></message>
|
||||
<message><source>Neuer Ordner…</source><translation>New Folder…</translation></message>
|
||||
<message><source>Löschen</source><translation>Delete</translation></message>
|
||||
<message><source>Neue Datei</source><translation>New File</translation></message>
|
||||
<message><source>Dateiname:</source><translation>File name:</translation></message>
|
||||
<message><source>Eine Datei mit diesem Namen existiert bereits:
|
||||
%1</source><translation>A file with this name already exists:
|
||||
%1</translation></message>
|
||||
<message><source>Datei konnte nicht angelegt werden:
|
||||
%1</source><translation>Could not create file:
|
||||
%1</translation></message>
|
||||
<message><source>Neuer Ordner</source><translation>New Folder</translation></message>
|
||||
<message><source>Ordnername:</source><translation>Folder name:</translation></message>
|
||||
<message><source>Ordner konnte nicht angelegt werden:
|
||||
%1</source><translation>Could not create folder:
|
||||
%1</translation></message>
|
||||
<message><source>%1 löschen</source><translation>Delete %1</translation></message>
|
||||
<message><source>%1 wirklich löschen?
|
||||
%2</source><translation>Really delete %1?
|
||||
%2</translation></message>
|
||||
<message><source>Datei</source><translation>file</translation></message>
|
||||
<message><source>Ordner</source><translation>folder</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EditorTab</name>
|
||||
<message><source>Speichern</source><translation>Save</translation></message>
|
||||
<message><source>Datei konnte nicht gespeichert werden:
|
||||
%1</source><translation>Could not save file:
|
||||
%1</translation></message>
|
||||
<message><source>Speichern unter</source><translation>Save As</translation></message>
|
||||
<message><source>Datei konnte nicht geschrieben werden:
|
||||
%1</source><translation>Could not write file:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CodeEditor</name>
|
||||
<message><source>Datei öffnen</source><translation>Open File</translation></message>
|
||||
<message><source>Open File</source><translation>Open File</translation></message>
|
||||
<message><source>Cannot open file:
|
||||
%1</source><translation>Cannot open file:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SearchPanel</name>
|
||||
<message><source>Suchen…</source><translation>Search…</translation></message>
|
||||
<message><source>Suchen:</source><translation>Find:</translation></message>
|
||||
<message><source>▲</source><translation>▲</translation></message>
|
||||
<message><source>▼</source><translation>▼</translation></message>
|
||||
<message><source>Vorheriger Treffer (Shift+F3)</source><translation>Previous match (Shift+F3)</translation></message>
|
||||
<message><source>Nächster Treffer (F3)</source><translation>Next match (F3)</translation></message>
|
||||
<message><source>Schließen (Esc)</source><translation>Close (Esc)</translation></message>
|
||||
<message><source>✕</source><translation>✕</translation></message>
|
||||
<message><source>Ersetzen durch…</source><translation>Replace with…</translation></message>
|
||||
<message><source>Ersetzen:</source><translation>Replace:</translation></message>
|
||||
<message><source>Ersetzen</source><translation>Replace</translation></message>
|
||||
<message><source>Alle ersetzen</source><translation>Replace All</translation></message>
|
||||
<message><source>In Auswahl ersetzen</source><translation>Replace in Selection</translation></message>
|
||||
<message><source>Groß-/Kleinschreibung</source><translation>Case Sensitive</translation></message>
|
||||
<message><source>Ganzes Wort</source><translation>Whole Word</translation></message>
|
||||
<message><source>Regulärer Ausdruck</source><translation>Regular Expression</translation></message>
|
||||
<message><source>Kein Treffer</source><translation>No matches</translation></message>
|
||||
<message><source>%1 Treffer</source><translation>%1 matches</translation></message>
|
||||
<message><source>%1 Ersetzung(en) durchgeführt.</source><translation>%1 replacement(s) made.</translation></message>
|
||||
<message><source>Es ist kein Text ausgewählt.</source><translation>No text selected.</translation></message>
|
||||
<message><source>%1 Ersetzung(en) in der Auswahl durchgeführt.</source><translation>%1 replacement(s) in selection.</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FileSearchPanel</name>
|
||||
<message><source>Suchen in Dateien:</source><translation>Search in files:</translation></message>
|
||||
<message><source>Suchbegriff…</source><translation>Search term…</translation></message>
|
||||
<message><source>Suchen</source><translation>Search</translation></message>
|
||||
<message><source>✕</source><translation>✕</translation></message>
|
||||
<message><source>Schließen</source><translation>Close</translation></message>
|
||||
<message><source>Groß-/Kleinschreibung</source><translation>Case Sensitive</translation></message>
|
||||
<message><source>Ganzes Wort</source><translation>Whole Word</translation></message>
|
||||
<message><source>Regex</source><translation>Regex</translation></message>
|
||||
<message><source>Dateitypen:</source><translation>File types:</translation></message>
|
||||
<message><source>Leerzeichen-getrennte Muster, z.B.: *.php *.html</source><translation>Space-separated patterns, e.g.: *.php *.html</translation></message>
|
||||
<message><source>Suche läuft…</source><translation>Searching…</translation></message>
|
||||
<message><source>Kein Projektverzeichnis geöffnet.</source><translation>No project directory open.</translation></message>
|
||||
<message><source>Keine Treffer gefunden.</source><translation>No matches found.</translation></message>
|
||||
<message><source>%1 Treffer in %2 Datei(en).</source><translation>%1 match(es) in %2 file(s).</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FunctionListPanel</name>
|
||||
<message><source>Projektfunktionen</source><translation>Project Functions</translation></message>
|
||||
<message><source>↻</source><translation>↻</translation></message>
|
||||
<message><source>Neu scannen</source><translation>Rescan</translation></message>
|
||||
<message><source>Funktion suchen…</source><translation>Search function…</translation></message>
|
||||
<message><source>Nach Datei</source><translation>By File</translation></message>
|
||||
<message><source>Nach Klasse</source><translation>By Class</translation></message>
|
||||
<message><source>Alphabetisch</source><translation>Alphabetically</translation></message>
|
||||
<message><source>Kein Projekt geöffnet.</source><translation>No project open.</translation></message>
|
||||
<message><source>Scanne…</source><translation>Scanning…</translation></message>
|
||||
<message><source>%1 Funktionen gefunden</source><translation>%1 functions found</translation></message>
|
||||
<message><source>%1 von %2 Funktionen</source><translation>%1 of %2 functions</translation></message>
|
||||
<message><source>(global)</source><translation>(global)</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FunctionListDialog</name>
|
||||
<message><source>Projektfunktionen – BareCode</source><translation>Project Functions – BareCode</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DeadCodeAnalyzer</name>
|
||||
<message><source>Globale Funktion — kein Aufruf gefunden</source><translation>Global function — no call found</translation></message>
|
||||
<message><source>Methode von %1 — kein Aufruf gefunden</source><translation>Method of %1 — no call found</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DeadCodeDialog</name>
|
||||
<message><source>Tote Funktionen – BareCode</source><translation>Dead Code – BareCode</translation></message>
|
||||
<message><source>Kandidatenliste — kein Aufruf per Regex gefunden. Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. Bitte vor dem Löschen manuell prüfen.</source><translation>Candidate list — no call found via regex. Dynamic calls (call_user_func, strings, hooks) are not detected. Please verify manually before deleting.</translation></message>
|
||||
<message><source>Ausschließen:</source><translation>Exclude:</translation></message>
|
||||
<message><source>vendor lib node_modules (leerzeichen-getrennt)</source><translation>vendor lib node_modules (space-separated)</translation></message>
|
||||
<message><source>Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen</source><translation>Select a directory from the project to add to the exclusion list</translation></message>
|
||||
<message><source>+ Verzeichnis</source><translation>+ Directory</translation></message>
|
||||
<message><source>Ausschlussliste leeren</source><translation>Clear exclusion list</translation></message>
|
||||
<message><source>Verzeichnis ausschließen</source><translation>Exclude Directory</translation></message>
|
||||
<message><source>Analyse starten</source><translation>Start Analysis</translation></message>
|
||||
<message><source>Als TXT exportieren</source><translation>Export as TXT</translation></message>
|
||||
<message><source>Analysiere…</source><translation>Analysing…</translation></message>
|
||||
<message><source>Dateien werden gezählt…</source><translation>Counting files…</translation></message>
|
||||
<message><source>(%1 / %2) %3</source><translation>(%1 / %2) %3</translation></message>
|
||||
<message><source>Funktion</source><translation>Function</translation></message>
|
||||
<message><source>Klasse</source><translation>Class</translation></message>
|
||||
<message><source>Datei</source><translation>File</translation></message>
|
||||
<message><source>Zeile</source><translation>Line</translation></message>
|
||||
<message><source>(global)</source><translation>(global)</translation></message>
|
||||
<message><source>✓ Keine ungenutzten Funktionen gefunden.</source><translation>✓ No unused functions found.</translation></message>
|
||||
<message><source>%1 Kandidaten gefunden</source><translation>%1 candidates found</translation></message>
|
||||
<message><source>Analyse</source><translation>Analysis</translation></message>
|
||||
<message><source>Bitte zuerst ein Projekt öffnen.</source><translation>Please open a project first.</translation></message>
|
||||
<message><source>Ergebnis exportieren</source><translation>Export Result</translation></message>
|
||||
<message><source>Textdateien (*.txt)</source><translation>Text files (*.txt)</translation></message>
|
||||
<message><source>Export</source><translation>Export</translation></message>
|
||||
<message><source>Datei konnte nicht geschrieben werden:
|
||||
%1</source><translation>Could not write file:
|
||||
%1</translation></message>
|
||||
<message><source>Exportiert nach:
|
||||
%1</source><translation>Exported to:
|
||||
%1</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ColorIndicator</name>
|
||||
<message><source>Farbe wählen</source><translation>Choose Color</translation></message>
|
||||
</context>
|
||||
</TS>
|
||||