README.md aktualisiert
264
README.md
@@ -1,130 +1,76 @@
|
||||
# BareCode
|
||||
|
||||
**BareCode – Schlanker Code-Editor für Web-Entwickler**
|
||||
Version 1.1.0
|
||||
**Schlanker, modularer Code-Editor für Web-Entwickler**
|
||||
Version 1.1.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de)
|
||||
|
||||
Kompiliert sauber auf **Linux**, **Windows** und **Haiku**.
|
||||
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 |
|
||||
|---|---|
|
||||
| Projektbaum | Linkes Panel — zeigt nur das gewählte Projektverzeichnis |
|
||||
| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare |
|
||||
| Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
|
||||
| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
|
||||
| Syntax-Highlighting | HTML, PHP, CSS, C/C++ inkl. mehrzeiliger Kommentare |
|
||||
| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
|
||||
| Einrück-Führungslinien | Vertikale Linien wie in VS Code |
|
||||
| Auto-Indent | Einrückungstiefe wird bei Enter übernommen |
|
||||
| Tab / Shift+Tab | Ein- und Ausrücken, auch für mehrere Zeilen gleichzeitig |
|
||||
| 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 |
|
||||
|
||||
### 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 |
|
||||
| Suchen & Ersetzen | Einzeln, Alle, In Auswahl, Regex, Live-Hervorhebung |
|
||||
| In Dateien suchen | Rekursive Projektsuche mit Ergebnisliste, direkt zur Zeile |
|
||||
| Dark / Light Mode | Umschaltbar, wird gespeichert |
|
||||
| Dark / Light Mode | Umschaltbar (Strg+Shift+D), wird gespeichert |
|
||||
| Session | Geöffnete Dateien und aktiver Tab werden wiederhergestellt |
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
BareCode/
|
||||
├── CMakeLists.txt
|
||||
├── main.cpp
|
||||
├── resources/
|
||||
│ ├── resources.qrc
|
||||
│ └── icon_*.png
|
||||
└── src/
|
||||
├── core/ # MainWindow, ProjectManager, Settings, ThemeManager, IPlugin
|
||||
├── editor/ # EditorPanel, EditorTab, CodeEditor, SearchPanel, FileSearchPanel
|
||||
├── filetree/ # FileTreePanel
|
||||
└── highlighter/ # SyntaxHighlighter, HighlighterFactory
|
||||
```
|
||||
|
||||
Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek.
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten & Bauen
|
||||
|
||||
### Linux
|
||||
|
||||
**Abhängigkeiten (Beispiel Ubuntu/Debian):**
|
||||
```bash
|
||||
sudo apt install cmake qt6-base-dev qt6-base-dev-tools libqt6concurrent6
|
||||
```
|
||||
|
||||
**Abhängigkeiten (Arch/Manjaro):**
|
||||
```bash
|
||||
sudo pacman -S cmake qt6-base
|
||||
```
|
||||
|
||||
**Bauen:**
|
||||
```bash
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
**Installieren:**
|
||||
```bash
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Windows (Visual Studio)
|
||||
|
||||
**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
|
||||
build\Release\BareCode.exe
|
||||
```
|
||||
|
||||
**Windows Installer erstellen** (mit [Inno Setup](https://jrsoftware.org/isinfo.php)):
|
||||
```bat
|
||||
windeployqt --release build\Release\BareCode.exe
|
||||
```
|
||||
Danach das Inno Setup Skript ausführen.
|
||||
|
||||
---
|
||||
|
||||
### Haiku
|
||||
|
||||
**Abhängigkeiten:**
|
||||
```bash
|
||||
pkgman install qt6_base qt6_base_devel
|
||||
```
|
||||
|
||||
> **Hinweis:** `qt6_base_devel` enthält auf Haiku sowohl die Header als auch
|
||||
> die Build-Tools (`moc`, `rcc`, `qmake`). Getestet auf Haiku R1/beta5.
|
||||
|
||||
**Bauen:**
|
||||
```bash
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
**Installieren:**
|
||||
```bash
|
||||
cmake --install build
|
||||
```
|
||||
|
||||
> Nach der Installation wird `mimeset` automatisch aufgerufen um den
|
||||
> MIME-Typ zu registrieren, damit BareCode im Haiku Tracker korrekt
|
||||
> als Anwendung erkannt wird.
|
||||
|
||||
---
|
||||
|
||||
## Tastaturkürzel
|
||||
|
||||
| Aktion | Kürzel |
|
||||
@@ -137,13 +83,105 @@ cmake --install build
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Erweitern
|
||||
## Projektstruktur
|
||||
|
||||
### Neuen Highlighter hinzufügen
|
||||
```
|
||||
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,
|
||||
│ # FunctionScanner, FunctionIndex,
|
||||
│ # FunctionListPanel, FunctionListDialog,
|
||||
│ # DeadCodeAnalyzer, DeadCodeDialog
|
||||
├── filetree/ # FileTreePanel
|
||||
└── highlighter/ # SyntaxHighlighter, HighlighterFactory
|
||||
```
|
||||
|
||||
Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek — Komponenten lassen sich unabhängig austauschen und erweitern.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
**Installer erstellen** (mit [Inno Setup](https://jrsoftware.org/isinfo.php)):
|
||||
```bat
|
||||
windeployqt --release build\Release\BareCode.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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. `qt6_base_devel` enthält auf Haiku
|
||||
> sowohl die Header als auch die Build-Tools.
|
||||
|
||||
---
|
||||
|
||||
## Neue Sprache hinzufügen
|
||||
|
||||
1. Subklasse von `SyntaxHighlighter` in `src/highlighter/` erstellen
|
||||
2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage)
|
||||
@@ -151,17 +189,11 @@ cmake --install build
|
||||
|
||||
Keine weiteren Dateien müssen geändert werden.
|
||||
|
||||
### Neues Panel / Plugin hinzufügen
|
||||
|
||||
1. `IPlugin`-Interface aus `src/core/IPlugin.h` implementieren
|
||||
2. `QWidget`-Subklasse für die UI erstellen
|
||||
3. In `MainWindow` instanziieren und verdrahten
|
||||
|
||||
---
|
||||
|
||||
## Code-Stil
|
||||
## Lizenz
|
||||
|
||||
Allman-Klammerstil, C++17 durchgehend.
|
||||
MIT License — Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
|
||||
|
||||
---
|
||||
|
||||
@@ -169,3 +201,5 @@ Allman-Klammerstil, C++17 durchgehend.
|
||||
|
||||
- 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)
|
||||
|
||||
BIN
barecode-1.1.0.tar.gz
Normal file
1
src/barecode-1.1.0.tar.gz
Symbolic link
@@ -0,0 +1 @@
|
||||
/home/diabolus/Arbeit/Projekt-Hirnfrei/BareCode/barecode-1.1.0.tar.gz
|
||||
2
src/barecode/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
{src/
|
||||
build/
|
||||
13
src/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
src/barecode/BareCode.rc
Normal file
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON "resources/BareCode.ico"
|
||||
47
src/barecode/BareCodeAUR/PKGBUILD
Normal file
@@ -0,0 +1,47 @@
|
||||
# Maintainer: Dany Thinnes <deine@email.de>
|
||||
|
||||
pkgname=barecode-git
|
||||
pkgver=r5.cb66172
|
||||
pkgrel=1
|
||||
pkgdesc="Modularer Code-Editor, entwickelt von Projekt Hirnfrei (git)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://git.projekt-hirnfrei.de/diabolus/BareCode"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja' 'git')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode')
|
||||
|
||||
source=("$pkgname::git+$url.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver()
|
||||
{
|
||||
cd "$pkgname"
|
||||
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build()
|
||||
{
|
||||
cmake \
|
||||
-B build \
|
||||
-S "$pkgname" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package()
|
||||
{
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
|
||||
install -Dm644 "$pkgname/barecode.desktop" \
|
||||
"$pkgdir/usr/share/applications/barecode.desktop"
|
||||
|
||||
# install -Dm644 "$pkgname/resources/barecode.png" \
|
||||
# "$pkgdir/usr/share/pixmaps/barecode.png"
|
||||
|
||||
install -Dm644 "$pkgname/LICENSE" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
47
src/barecode/BareCodeAUR/PKGBUILD-git
Normal file
@@ -0,0 +1,47 @@
|
||||
# Maintainer: Dany Thinnes <deine@email.de>
|
||||
|
||||
pkgname=barecode-git
|
||||
pkgver=r1.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="Modularer Code-Editor, entwickelt von Projekt Hirnfrei (git)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://git.projekt-hirnfrei.de/diabolus/BareCode"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja' 'git')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode')
|
||||
|
||||
source=("$pkgname::git+$url.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver()
|
||||
{
|
||||
cd "$pkgname"
|
||||
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build()
|
||||
{
|
||||
cmake \
|
||||
-B build \
|
||||
-S "$pkgname" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package()
|
||||
{
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
|
||||
install -Dm644 "$pkgname/barecode.desktop" \
|
||||
"$pkgdir/usr/share/applications/barecode.desktop"
|
||||
|
||||
# install -Dm644 "$pkgname/resources/barecode.png" \
|
||||
# "$pkgdir/usr/share/pixmaps/barecode.png"
|
||||
|
||||
install -Dm644 "$pkgname/LICENSE" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
41
src/barecode/BareCodeAUR/PKGBUILD-stable
Normal file
@@ -0,0 +1,41 @@
|
||||
# Maintainer: Dany Thinnes <deine@email.de>
|
||||
|
||||
pkgname=barecode
|
||||
pkgver=1.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="Modularer Code-Editor, entwickelt von Projekt Hirnfrei"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://git.projekt-hirnfrei.de/diabolus/BareCode"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja' 'git')
|
||||
|
||||
# Gitea erzeugt automatisch Tarballs unter:
|
||||
# https://DEIN-GITEA-SERVER/DEINNAME/BareCode/archive/v1.0.0.tar.gz
|
||||
#
|
||||
# sha256sum nach dem Taggen ermitteln:
|
||||
# curl -L https://DEIN-GITEA-SERVER/DEINNAME/BareCode/archive/v1.0.0.tar.gz | sha256sum
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
||||
sha256sums=('SKIP') # Ersetzen nach erstem Tag
|
||||
|
||||
build()
|
||||
{
|
||||
cmake \
|
||||
-B build \
|
||||
-S "BareCode-$pkgver" \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package()
|
||||
{
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
|
||||
install -Dm644 "BareCode-$pkgver/barecode.desktop" \
|
||||
"$pkgdir/usr/share/applications/barecode.desktop"
|
||||
|
||||
install -Dm644 "BareCode-$pkgver/LICENSE" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
1
src/barecode/BareCodeAUR/barecode-git/HEAD
Normal file
@@ -0,0 +1 @@
|
||||
ref: refs/heads/main
|
||||
9
src/barecode/BareCodeAUR/barecode-git/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[remote "origin"]
|
||||
url = https://git.projekt-hirnfrei.de/diabolus/BareCode.git
|
||||
tagOpt = --no-tags
|
||||
fetch = +refs/*:refs/*
|
||||
mirror = true
|
||||
1
src/barecode/BareCodeAUR/barecode-git/description
Normal file
@@ -0,0 +1 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
15
src/barecode/BareCodeAUR/barecode-git/hooks/applypatch-msg.sample
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message taken by
|
||||
# applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit. The hook is
|
||||
# allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "applypatch-msg".
|
||||
|
||||
. git-sh-setup
|
||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
||||
:
|
||||
74
src/barecode/BareCodeAUR/barecode-git/hooks/commit-msg.sample
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to check the commit log message.
|
||||
# Called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero
|
||||
# status after issuing an appropriate message if it wants to stop the
|
||||
# commit. The hook is allowed to edit the commit message file.
|
||||
#
|
||||
# To enable this hook, rename this file to "commit-msg".
|
||||
|
||||
# Uncomment the below to add a Signed-off-by line to the message.
|
||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
||||
# hook is more suited to it.
|
||||
#
|
||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
||||
|
||||
# This example catches duplicate Signed-off-by lines and messages that
|
||||
# would confuse 'git am'.
|
||||
|
||||
ret=0
|
||||
|
||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
||||
echo >&2 Duplicate Signed-off-by lines.
|
||||
ret=1
|
||||
}
|
||||
|
||||
comment_re="$(
|
||||
{
|
||||
git config --get-regexp "^core\.comment(char|string)\$" ||
|
||||
echo '#'
|
||||
} | sed -n -e '
|
||||
${
|
||||
s/^[^ ]* //
|
||||
s|[][*./\]|\\&|g
|
||||
s/^auto$/[#;@!$%^&|:]/
|
||||
p
|
||||
}'
|
||||
)"
|
||||
scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$"
|
||||
comment_line="^${comment_re}.*"
|
||||
blank_line='^[ ]*$'
|
||||
# Disallow lines starting with "diff -" or "Index: " in the body of the
|
||||
# message. Stop looking if we see a scissors line.
|
||||
line="$(sed -n -e "
|
||||
# Skip comments and blank lines at the start of the file.
|
||||
/${scissors_line}/q
|
||||
/${comment_line}/d
|
||||
/${blank_line}/d
|
||||
# The first paragraph will become the subject header so
|
||||
# does not need to be checked.
|
||||
: subject
|
||||
n
|
||||
/${scissors_line}/q
|
||||
/${blank_line}/!b subject
|
||||
# Check the body of the message for problematic
|
||||
# prefixes.
|
||||
: body
|
||||
n
|
||||
/${scissors_line}/q
|
||||
/${comment_line}/b body
|
||||
/^diff -/{p;q;}
|
||||
/^Index: /{p;q;}
|
||||
b body
|
||||
" "$1")"
|
||||
if test -n "$line"
|
||||
then
|
||||
echo >&2 "Message contains a diff that will confuse 'git am'."
|
||||
echo >&2 "To fix this indent the diff."
|
||||
ret=1
|
||||
fi
|
||||
|
||||
exit $ret
|
||||
168
src/barecode/BareCodeAUR/barecode-git/hooks/fsmonitor-watchman.sample
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use IPC::Open2;
|
||||
|
||||
# An example hook script to integrate Watchman
|
||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
||||
# new and modified files.
|
||||
#
|
||||
# The hook is passed a version (currently 2) and last update token
|
||||
# formatted as a string and outputs to stdout a new update token and
|
||||
# all files that have been modified since the update token. Paths must
|
||||
# be relative to the root of the working tree and separated by a single NUL.
|
||||
#
|
||||
# To enable this hook, rename this file to "query-watchman" and set
|
||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
||||
#
|
||||
my ($version, $last_update_token) = @ARGV;
|
||||
|
||||
# Uncomment for debugging
|
||||
# print STDERR "$0 $version $last_update_token\n";
|
||||
|
||||
# Check the hook interface version
|
||||
if ($version ne 2) {
|
||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
||||
"Falling back to scanning...\n";
|
||||
}
|
||||
|
||||
my $git_work_tree = get_working_dir();
|
||||
|
||||
my $json_pkg;
|
||||
eval {
|
||||
require JSON::XS;
|
||||
$json_pkg = "JSON::XS";
|
||||
1;
|
||||
} or do {
|
||||
require JSON::PP;
|
||||
$json_pkg = "JSON::PP";
|
||||
};
|
||||
|
||||
launch_watchman();
|
||||
|
||||
sub launch_watchman {
|
||||
my $o = watchman_query();
|
||||
if (is_work_tree_watched($o)) {
|
||||
output_result($o->{clock}, @{$o->{files}});
|
||||
}
|
||||
}
|
||||
|
||||
sub output_result {
|
||||
my ($clockid, @files) = @_;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# binmode $fh, ":utf8";
|
||||
# print $fh "$clockid\n@files\n";
|
||||
# close $fh;
|
||||
|
||||
binmode STDOUT, ":utf8";
|
||||
print $clockid;
|
||||
print "\0";
|
||||
local $, = "\0";
|
||||
print @files;
|
||||
}
|
||||
|
||||
sub watchman_clock {
|
||||
my $response = qx/watchman clock "$git_work_tree"/;
|
||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub watchman_query {
|
||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
||||
or die "open2() failed: $!\n" .
|
||||
"Falling back to scanning...\n";
|
||||
|
||||
# In the query expression below we're asking for names of files that
|
||||
# changed since $last_update_token but not from the .git folder.
|
||||
#
|
||||
# To accomplish this, we're using the "since" generator to use the
|
||||
# recency index to select candidate nodes and "fields" to limit the
|
||||
# output to file names only. Then we're using the "expression" term to
|
||||
# further constrain the results.
|
||||
my $last_update_line = "";
|
||||
if (substr($last_update_token, 0, 1) eq "c") {
|
||||
$last_update_token = "\"$last_update_token\"";
|
||||
$last_update_line = qq[\n"since": $last_update_token,];
|
||||
}
|
||||
my $query = <<" END";
|
||||
["query", "$git_work_tree", {$last_update_line
|
||||
"fields": ["name"],
|
||||
"expression": ["not", ["dirname", ".git"]]
|
||||
}]
|
||||
END
|
||||
|
||||
# Uncomment for debugging the watchman query
|
||||
# open (my $fh, ">", ".git/watchman-query.json");
|
||||
# print $fh $query;
|
||||
# close $fh;
|
||||
|
||||
print CHLD_IN $query;
|
||||
close CHLD_IN;
|
||||
my $response = do {local $/; <CHLD_OUT>};
|
||||
|
||||
# Uncomment for debugging the watch response
|
||||
# open ($fh, ">", ".git/watchman-response.json");
|
||||
# print $fh $response;
|
||||
# close $fh;
|
||||
|
||||
die "Watchman: command returned no output.\n" .
|
||||
"Falling back to scanning...\n" if $response eq "";
|
||||
die "Watchman: command returned invalid output: $response\n" .
|
||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
||||
|
||||
return $json_pkg->new->utf8->decode($response);
|
||||
}
|
||||
|
||||
sub is_work_tree_watched {
|
||||
my ($output) = @_;
|
||||
my $error = $output->{error};
|
||||
if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
||||
my $response = qx/watchman watch "$git_work_tree"/;
|
||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
||||
"Falling back to scanning...\n" if $? != 0;
|
||||
$output = $json_pkg->new->utf8->decode($response);
|
||||
$error = $output->{error};
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
# Uncomment for debugging watchman output
|
||||
# open (my $fh, ">", ".git/watchman-output.out");
|
||||
# close $fh;
|
||||
|
||||
# Watchman will always return all files on the first query so
|
||||
# return the fast "everything is dirty" flag to git and do the
|
||||
# Watchman query just to get it over with now so we won't pay
|
||||
# the cost in git to look up each individual file.
|
||||
my $o = watchman_clock();
|
||||
$error = $o->{error};
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
output_result($o->{clock}, ("/"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
die "Watchman: $error.\n" .
|
||||
"Falling back to scanning...\n" if $error;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_working_dir {
|
||||
my $working_dir;
|
||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
||||
$working_dir = Win32::GetCwd();
|
||||
$working_dir =~ tr/\\/\//;
|
||||
} else {
|
||||
require Cwd;
|
||||
$working_dir = Cwd::cwd();
|
||||
}
|
||||
|
||||
return $working_dir;
|
||||
}
|
||||
8
src/barecode/BareCodeAUR/barecode-git/hooks/post-update.sample
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare a packed repository for use over
|
||||
# dumb transports.
|
||||
#
|
||||
# To enable this hook, rename this file to "post-update".
|
||||
|
||||
exec git update-server-info
|
||||
14
src/barecode/BareCodeAUR/barecode-git/hooks/pre-applypatch.sample
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed
|
||||
# by applypatch from an e-mail message.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an
|
||||
# appropriate message if it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-applypatch".
|
||||
|
||||
. git-sh-setup
|
||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
||||
:
|
||||
49
src/barecode/BareCodeAUR/barecode-git/hooks/pre-commit.sample
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git commit" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message if
|
||||
# it wants to stop the commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-commit".
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
||||
then
|
||||
against=HEAD
|
||||
else
|
||||
# Initial commit: diff against an empty tree object
|
||||
against=$(git hash-object -t tree /dev/null)
|
||||
fi
|
||||
|
||||
# If you want to allow non-ASCII filenames set this variable to true.
|
||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
||||
|
||||
# Redirect output to stderr.
|
||||
exec 1>&2
|
||||
|
||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
||||
# them from being added to the repository. We exploit the fact that the
|
||||
# printable range starts at the space character and ends with tilde.
|
||||
if [ "$allownonascii" != "true" ] &&
|
||||
# Note that the use of brackets around a tr range is ok here, (it's
|
||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
||||
# the square bracket bytes happen to fall in the designated range.
|
||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
||||
then
|
||||
cat <<\EOF
|
||||
Error: Attempt to add a non-ASCII file name.
|
||||
|
||||
This can cause problems if you want to work with people on other platforms.
|
||||
|
||||
To be portable it is advisable to rename the file.
|
||||
|
||||
If you know what you are doing you can disable this check using:
|
||||
|
||||
git config hooks.allownonascii true
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If there are whitespace errors, print the offending file names and fail.
|
||||
exec git diff-index --check --cached $against --
|
||||
13
src/barecode/BareCodeAUR/barecode-git/hooks/pre-merge-commit.sample
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to verify what is about to be committed.
|
||||
# Called by "git merge" with no arguments. The hook should
|
||||
# exit with non-zero status after issuing an appropriate message to
|
||||
# stderr if it wants to stop the merge commit.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-merge-commit".
|
||||
|
||||
. git-sh-setup
|
||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
||||
exec "$GIT_DIR/hooks/pre-commit"
|
||||
:
|
||||
53
src/barecode/BareCodeAUR/barecode-git/hooks/pre-push.sample
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to verify what is about to be pushed. Called by "git
|
||||
# push" after it has checked the remote status, but before anything has been
|
||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
||||
#
|
||||
# This hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- Name of the remote to which the push is being done
|
||||
# $2 -- URL to which the push is being done
|
||||
#
|
||||
# If pushing without using a named remote those arguments will be equal.
|
||||
#
|
||||
# Information about the commits which are being pushed is supplied as lines to
|
||||
# the standard input in the form:
|
||||
#
|
||||
# <local ref> <local oid> <remote ref> <remote oid>
|
||||
#
|
||||
# This sample shows how to prevent push of commits where the log message starts
|
||||
# with "WIP" (work in progress).
|
||||
|
||||
remote="$1"
|
||||
url="$2"
|
||||
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
|
||||
while read local_ref local_oid remote_ref remote_oid
|
||||
do
|
||||
if test "$local_oid" = "$zero"
|
||||
then
|
||||
# Handle delete
|
||||
:
|
||||
else
|
||||
if test "$remote_oid" = "$zero"
|
||||
then
|
||||
# New branch, examine all commits
|
||||
range="$local_oid"
|
||||
else
|
||||
# Update to existing branch, examine new commits
|
||||
range="$remote_oid..$local_oid"
|
||||
fi
|
||||
|
||||
# Check for WIP commit
|
||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
||||
if test -n "$commit"
|
||||
then
|
||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
169
src/barecode/BareCodeAUR/barecode-git/hooks/pre-rebase.sample
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
||||
#
|
||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
||||
# its job, and can prevent the command from running by exiting with
|
||||
# non-zero status.
|
||||
#
|
||||
# The hook is called with the following parameters:
|
||||
#
|
||||
# $1 -- the upstream the series was forked from.
|
||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
||||
#
|
||||
# This sample shows how to prevent topic branches that are already
|
||||
# merged to 'next' branch from getting rebased, because allowing it
|
||||
# would result in rebasing already published history.
|
||||
|
||||
publish=next
|
||||
basebranch="$1"
|
||||
if test "$#" = 2
|
||||
then
|
||||
topic="refs/heads/$2"
|
||||
else
|
||||
topic=`git symbolic-ref HEAD` ||
|
||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
||||
fi
|
||||
|
||||
case "$topic" in
|
||||
refs/heads/??/*)
|
||||
;;
|
||||
*)
|
||||
exit 0 ;# we do not interrupt others.
|
||||
;;
|
||||
esac
|
||||
|
||||
# Now we are dealing with a topic branch being rebased
|
||||
# on top of master. Is it OK to rebase it?
|
||||
|
||||
# Does the topic really exist?
|
||||
git show-ref -q "$topic" || {
|
||||
echo >&2 "No such branch $topic"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Is topic fully merged to master?
|
||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
||||
if test -z "$not_in_master"
|
||||
then
|
||||
echo >&2 "$topic is fully merged to master; better remove it."
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
fi
|
||||
|
||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
||||
if test "$only_next_1" = "$only_next_2"
|
||||
then
|
||||
not_in_topic=`git rev-list "^$topic" master`
|
||||
if test -z "$not_in_topic"
|
||||
then
|
||||
echo >&2 "$topic is already up to date with master"
|
||||
exit 1 ;# we could allow it, but there is no point.
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
||||
/usr/bin/perl -e '
|
||||
my $topic = $ARGV[0];
|
||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
||||
my (%not_in_next) = map {
|
||||
/^([0-9a-f]+) /;
|
||||
($1 => 1);
|
||||
} split(/\n/, $ARGV[1]);
|
||||
for my $elem (map {
|
||||
/^([0-9a-f]+) (.*)$/;
|
||||
[$1 => $2];
|
||||
} split(/\n/, $ARGV[2])) {
|
||||
if (!exists $not_in_next{$elem->[0]}) {
|
||||
if ($msg) {
|
||||
print STDERR $msg;
|
||||
undef $msg;
|
||||
}
|
||||
print STDERR " $elem->[1]\n";
|
||||
}
|
||||
}
|
||||
' "$topic" "$not_in_next" "$not_in_master"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
<<\DOC_END
|
||||
|
||||
This sample hook safeguards topic branches that have been
|
||||
published from being rewound.
|
||||
|
||||
The workflow assumed here is:
|
||||
|
||||
* Once a topic branch forks from "master", "master" is never
|
||||
merged into it again (either directly or indirectly).
|
||||
|
||||
* Once a topic branch is fully cooked and merged into "master",
|
||||
it is deleted. If you need to build on top of it to correct
|
||||
earlier mistakes, a new topic branch is created by forking at
|
||||
the tip of the "master". This is not strictly necessary, but
|
||||
it makes it easier to keep your history simple.
|
||||
|
||||
* Whenever you need to test or publish your changes to topic
|
||||
branches, merge them into "next" branch.
|
||||
|
||||
The script, being an example, hardcodes the publish branch name
|
||||
to be "next", but it is trivial to make it configurable via
|
||||
$GIT_DIR/config mechanism.
|
||||
|
||||
With this workflow, you would want to know:
|
||||
|
||||
(1) ... if a topic branch has ever been merged to "next". Young
|
||||
topic branches can have stupid mistakes you would rather
|
||||
clean up before publishing, and things that have not been
|
||||
merged into other branches can be easily rebased without
|
||||
affecting other people. But once it is published, you would
|
||||
not want to rewind it.
|
||||
|
||||
(2) ... if a topic branch has been fully merged to "master".
|
||||
Then you can delete it. More importantly, you should not
|
||||
build on top of it -- other people may already want to
|
||||
change things related to the topic as patches against your
|
||||
"master", so if you need further changes, it is better to
|
||||
fork the topic (perhaps with the same name) afresh from the
|
||||
tip of "master".
|
||||
|
||||
Let's look at this example:
|
||||
|
||||
o---o---o---o---o---o---o---o---o---o "next"
|
||||
/ / / /
|
||||
/ a---a---b A / /
|
||||
/ / / /
|
||||
/ / c---c---c---c B /
|
||||
/ / / \ /
|
||||
/ / / b---b C \ /
|
||||
/ / / / \ /
|
||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
||||
|
||||
|
||||
A, B and C are topic branches.
|
||||
|
||||
* A has one fix since it was merged up to "next".
|
||||
|
||||
* B has finished. It has been fully merged up to "master" and "next",
|
||||
and is ready to be deleted.
|
||||
|
||||
* C has not merged to "next" at all.
|
||||
|
||||
We would want to allow C to be rebased, refuse A, and encourage
|
||||
B to be deleted.
|
||||
|
||||
To compute (1):
|
||||
|
||||
git rev-list ^master ^topic next
|
||||
git rev-list ^master next
|
||||
|
||||
if these match, topic has not merged in next at all.
|
||||
|
||||
To compute (2):
|
||||
|
||||
git rev-list master..topic
|
||||
|
||||
if this is empty, it is fully merged to "master".
|
||||
|
||||
DOC_END
|
||||
24
src/barecode/BareCodeAUR/barecode-git/hooks/pre-receive.sample
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to make use of push options.
|
||||
# The example simply echoes all push options that start with 'echoback='
|
||||
# and rejects all pushes when the "reject" push option is used.
|
||||
#
|
||||
# To enable this hook, rename this file to "pre-receive".
|
||||
|
||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
||||
then
|
||||
i=0
|
||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
||||
do
|
||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
||||
case "$value" in
|
||||
echoback=*)
|
||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
||||
;;
|
||||
reject)
|
||||
exit 1
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
fi
|
||||
42
src/barecode/BareCodeAUR/barecode-git/hooks/prepare-commit-msg.sample
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to prepare the commit log message.
|
||||
# Called by "git commit" with the name of the file that has the
|
||||
# commit message, followed by the description of the commit
|
||||
# message's source. The hook's purpose is to edit the commit
|
||||
# message file. If the hook fails with a non-zero status,
|
||||
# the commit is aborted.
|
||||
#
|
||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
||||
|
||||
# This hook includes three examples. The first one removes the
|
||||
# "# Please enter the commit message..." help message.
|
||||
#
|
||||
# The second includes the output of "git diff --name-status -r"
|
||||
# into the message, just before the "git status" output. It is
|
||||
# commented because it doesn't cope with --amend or with squashed
|
||||
# commits.
|
||||
#
|
||||
# The third example adds a Signed-off-by line to the message, that can
|
||||
# still be edited. This is rarely a good idea.
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
||||
|
||||
# case "$COMMIT_SOURCE,$SHA1" in
|
||||
# ,|template,)
|
||||
# /usr/bin/perl -i.bak -pe '
|
||||
# print "\n" . `git diff --cached --name-status -r`
|
||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
||||
# *) ;;
|
||||
# esac
|
||||
|
||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
||||
# if test -z "$COMMIT_SOURCE"
|
||||
# then
|
||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
||||
# fi
|
||||
78
src/barecode/BareCodeAUR/barecode-git/hooks/push-to-checkout.sample
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to update a checked-out tree on a git push.
|
||||
#
|
||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
||||
# push and updates reference(s) in its repository, and when the push
|
||||
# tries to update the branch that is currently checked out and the
|
||||
# receive.denyCurrentBranch configuration variable is set to
|
||||
# updateInstead.
|
||||
#
|
||||
# By default, such a push is refused if the working tree and the index
|
||||
# of the remote repository has any difference from the currently
|
||||
# checked out commit; when both the working tree and the index match
|
||||
# the current commit, they are updated to match the newly pushed tip
|
||||
# of the branch. This hook is to be used to override the default
|
||||
# behaviour; however the code below reimplements the default behaviour
|
||||
# as a starting point for convenient modification.
|
||||
#
|
||||
# The hook receives the commit with which the tip of the current
|
||||
# branch is going to be updated:
|
||||
commit=$1
|
||||
|
||||
# It can exit with a non-zero status to refuse the push (when it does
|
||||
# so, it must not modify the index or the working tree).
|
||||
die () {
|
||||
echo >&2 "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Or it can make any necessary changes to the working tree and to the
|
||||
# index to bring them to the desired state when the tip of the current
|
||||
# branch is updated to the new commit, and exit with a zero status.
|
||||
#
|
||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
||||
# in order to emulate git fetch that is run in the reverse direction
|
||||
# with git push, as the two-tree form of git read-tree -u -m is
|
||||
# essentially the same as git switch or git checkout that switches
|
||||
# branches while keeping the local changes in the working tree that do
|
||||
# not interfere with the difference between the branches.
|
||||
|
||||
# The below is a more-or-less exact translation to shell of the C code
|
||||
# for the default behaviour for git's push-to-checkout hook defined in
|
||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
||||
#
|
||||
# Note that the hook will be executed from the repository directory,
|
||||
# not from the working tree, so if you want to perform operations on
|
||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
||||
# by adding "cd .." or using relative paths.
|
||||
|
||||
if ! git update-index -q --ignore-submodules --refresh
|
||||
then
|
||||
die "Up-to-date check failed"
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules --
|
||||
then
|
||||
die "Working directory has unstaged changes"
|
||||
fi
|
||||
|
||||
# This is a rough translation of:
|
||||
#
|
||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
||||
if git cat-file -e HEAD 2>/dev/null
|
||||
then
|
||||
head=HEAD
|
||||
else
|
||||
head=$(git hash-object -t tree --stdin </dev/null)
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
||||
then
|
||||
die "Working directory has staged changes"
|
||||
fi
|
||||
|
||||
if ! git read-tree -u -m "$commit"
|
||||
then
|
||||
die "Could not update working tree to new HEAD"
|
||||
fi
|
||||
77
src/barecode/BareCodeAUR/barecode-git/hooks/sendemail-validate.sample
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
|
||||
# An example hook script to validate a patch (and/or patch series) before
|
||||
# sending it via email.
|
||||
#
|
||||
# The hook should exit with non-zero status after issuing an appropriate
|
||||
# message if it wants to prevent the email(s) from being sent.
|
||||
#
|
||||
# To enable this hook, rename this file to "sendemail-validate".
|
||||
#
|
||||
# By default, it will only check that the patch(es) can be applied on top of
|
||||
# the default upstream branch without conflicts in a secondary worktree. After
|
||||
# validation (successful or not) of the last patch of a series, the worktree
|
||||
# will be deleted.
|
||||
#
|
||||
# The following config variables can be set to change the default remote and
|
||||
# remote ref that are used to apply the patches against:
|
||||
#
|
||||
# sendemail.validateRemote (default: origin)
|
||||
# sendemail.validateRemoteRef (default: HEAD)
|
||||
#
|
||||
# Replace the TODO placeholders with appropriate checks according to your
|
||||
# needs.
|
||||
|
||||
validate_cover_letter () {
|
||||
file="$1"
|
||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
||||
true
|
||||
}
|
||||
|
||||
validate_patch () {
|
||||
file="$1"
|
||||
# Ensure that the patch applies without conflicts.
|
||||
git am -3 "$file" || return
|
||||
# TODO: Replace with appropriate checks for this patch
|
||||
# (e.g. checkpatch.pl).
|
||||
true
|
||||
}
|
||||
|
||||
validate_series () {
|
||||
# TODO: Replace with appropriate checks for the whole series
|
||||
# (e.g. quick build, coding style checks, etc.).
|
||||
true
|
||||
}
|
||||
|
||||
# main -------------------------------------------------------------------------
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
||||
then
|
||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
||||
else
|
||||
worktree=$(git config --get sendemail.validateWorktree)
|
||||
fi || {
|
||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
unset GIT_DIR GIT_WORK_TREE
|
||||
cd "$worktree" &&
|
||||
|
||||
if grep -q "^diff --git " "$1"
|
||||
then
|
||||
validate_patch "$1"
|
||||
else
|
||||
validate_cover_letter "$1"
|
||||
fi &&
|
||||
|
||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
||||
then
|
||||
git config --unset-all sendemail.validateWorktree &&
|
||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
||||
validate_series
|
||||
fi
|
||||
128
src/barecode/BareCodeAUR/barecode-git/hooks/update.sample
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# An example hook script to block unannotated tags from entering.
|
||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
||||
#
|
||||
# To enable this hook, rename this file to "update".
|
||||
#
|
||||
# Config
|
||||
# ------
|
||||
# hooks.allowunannotated
|
||||
# This boolean sets whether unannotated tags will be allowed into the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowdeletetag
|
||||
# This boolean sets whether deleting tags will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.allowmodifytag
|
||||
# This boolean sets whether a tag may be modified after creation. By default
|
||||
# it won't be.
|
||||
# hooks.allowdeletebranch
|
||||
# This boolean sets whether deleting branches will be allowed in the
|
||||
# repository. By default they won't be.
|
||||
# hooks.denycreatebranch
|
||||
# This boolean sets whether remotely creating branches will be denied
|
||||
# in the repository. By default this is allowed.
|
||||
#
|
||||
|
||||
# --- Command line
|
||||
refname="$1"
|
||||
oldrev="$2"
|
||||
newrev="$3"
|
||||
|
||||
# --- Safety check
|
||||
if [ -z "$GIT_DIR" ]; then
|
||||
echo "Don't run this script from the command line." >&2
|
||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Config
|
||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
||||
|
||||
# check for no description
|
||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
||||
case "$projectdesc" in
|
||||
"Unnamed repository"* | "")
|
||||
echo "*** Project description file hasn't been set" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Check types
|
||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
||||
if [ "$newrev" = "$zero" ]; then
|
||||
newrev_type=delete
|
||||
else
|
||||
newrev_type=$(git cat-file -t $newrev)
|
||||
fi
|
||||
|
||||
case "$refname","$newrev_type" in
|
||||
refs/tags/*,commit)
|
||||
# un-annotated tag
|
||||
short_refname=${refname##refs/tags/}
|
||||
if [ "$allowunannotated" != "true" ]; then
|
||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,delete)
|
||||
# delete tag
|
||||
if [ "$allowdeletetag" != "true" ]; then
|
||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/tags/*,tag)
|
||||
# annotated tag
|
||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
||||
then
|
||||
echo "*** Tag '$refname' already exists." >&2
|
||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,commit)
|
||||
# branch
|
||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/heads/*,delete)
|
||||
# delete branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
refs/remotes/*,commit)
|
||||
# tracking branch
|
||||
;;
|
||||
refs/remotes/*,delete)
|
||||
# delete tracking branch
|
||||
if [ "$allowdeletebranch" != "true" ]; then
|
||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Anything else (is there anything else?)
|
||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Finished
|
||||
exit 0
|
||||
1
src/barecode/BareCodeAUR/barecode-git/info/attributes
Normal file
@@ -0,0 +1 @@
|
||||
* -export-subst -export-ignore
|
||||
6
src/barecode/BareCodeAUR/barecode-git/info/exclude
Normal file
@@ -0,0 +1,6 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
4
src/barecode/BareCodeAUR/barecode-git/packed-refs
Normal file
@@ -0,0 +1,4 @@
|
||||
# pack-refs with: peeled fully-peeled sorted
|
||||
cb6617222dbcac30aaf15bf336b150fed440cc7f refs/heads/main
|
||||
ca573209e715409e5989324aaeaa4efbd4da5642 refs/tags/v1.0.0
|
||||
^36e074f43d5b76d940d953956b0ed80899e71848
|
||||
3020
src/barecode/BareCodeAUR/pkg/barecode-git/.BUILDINFO
Normal file
BIN
src/barecode/BareCodeAUR/pkg/barecode-git/.MTREE
Normal file
19
src/barecode/BareCodeAUR/pkg/barecode-git/.PKGINFO
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by makepkg 7.1.0
|
||||
# using fakeroot version 1.37.2
|
||||
pkgname = barecode-git
|
||||
pkgbase = barecode-git
|
||||
xdata = pkgtype=pkg
|
||||
pkgver = r5.cb66172-1
|
||||
pkgdesc = Modularer Code-Editor, entwickelt von Projekt Hirnfrei (git)
|
||||
url = https://git.projekt-hirnfrei.de/diabolus/BareCode
|
||||
builddate = 1781069213
|
||||
packager = Unknown Packager
|
||||
size = 1191503
|
||||
arch = x86_64
|
||||
license = MIT
|
||||
conflict = barecode
|
||||
provides = barecode
|
||||
depend = qt6-base
|
||||
makedepend = cmake
|
||||
makedepend = ninja
|
||||
makedepend = git
|
||||
BIN
src/barecode/BareCodeAUR/pkg/barecode-git/usr/bin/BareCode
Executable 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
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 824 B |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -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.
|
||||
143
src/barecode/CMakeLists.txt
Normal file
@@ -0,0 +1,143 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(BareCode
|
||||
VERSION 1.1.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
|
||||
# Haiku definiert kein eigenes CMake-Flag — wir erkennen es über den
|
||||
# Systemnamen. CMAKE_SYSTEM_NAME ist "Haiku" auf Haiku OS.
|
||||
# ---------------------------------------------------------------------------
|
||||
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()
|
||||
|
||||
# Include cmake modules
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
# Collect sources
|
||||
add_subdirectory(src)
|
||||
|
||||
# Resources
|
||||
qt_add_resources(BARECODE_RESOURCES resources/resources.qrc)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Executable
|
||||
# Unter Windows: .rc-Datei für EXE-Icon einbinden
|
||||
# ---------------------------------------------------------------------------
|
||||
if(PLATFORM_WINDOWS)
|
||||
qt_add_executable(BareCode
|
||||
main.cpp
|
||||
BareCode.rc
|
||||
${BARECODE_RESOURCES}
|
||||
)
|
||||
else()
|
||||
qt_add_executable(BareCode
|
||||
main.cpp
|
||||
${BARECODE_RESOURCES}
|
||||
)
|
||||
endif()
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
# ---- Linux (FreeDesktop) --------------------------------------------------
|
||||
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()
|
||||
|
||||
# ---- Haiku ----------------------------------------------------------------
|
||||
# Haiku verwendet kein FreeDesktop-System. Icons und MIME-Typen werden
|
||||
# nativ über 'mimeset' gesetzt. Die PNG-Icons legen wir in den
|
||||
# Haiku-typischen Pfad, ein post-install Skript ruft mimeset auf.
|
||||
if(PLATFORM_HAIKU)
|
||||
install(FILES resources/icon_256.png
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/BareCode
|
||||
RENAME BareCode.png
|
||||
)
|
||||
|
||||
# mimeset nach der Installation ausführen um MIME-Typ zu registrieren
|
||||
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 – MIME-Typ muss manuell gesetzt werden.\")
|
||||
endif()
|
||||
")
|
||||
endif()
|
||||
|
||||
# ---- macOS ----------------------------------------------------------------
|
||||
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
src/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.
|
||||
171
src/barecode/README.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# BareCode
|
||||
|
||||
**BareCode – Schlanker Code-Editor für Web-Entwickler**
|
||||
Version 1.1.0
|
||||
|
||||
Kompiliert sauber auf **Linux**, **Windows** und **Haiku**.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Details |
|
||||
|---|---|
|
||||
| Projektbaum | Linkes Panel — zeigt nur das gewählte Projektverzeichnis |
|
||||
| Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
|
||||
| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
|
||||
| Syntax-Highlighting | HTML, PHP, CSS, C/C++ inkl. mehrzeiliger Kommentare |
|
||||
| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
|
||||
| Einrück-Führungslinien | Vertikale Linien wie in VS Code |
|
||||
| Auto-Indent | Einrückungstiefe wird bei Enter übernommen |
|
||||
| Tab / Shift+Tab | Ein- und Ausrücken, auch für mehrere Zeilen gleichzeitig |
|
||||
| Smart Backspace | Springt zur vorherigen Tab-Stop-Position |
|
||||
| Speichern | Speichern, Speichern unter, Alles speichern |
|
||||
| Suchen & Ersetzen | Einzeln, Alle, In Auswahl, Regex, Live-Hervorhebung |
|
||||
| In Dateien suchen | Rekursive Projektsuche mit Ergebnisliste, direkt zur Zeile |
|
||||
| Dark / Light Mode | Umschaltbar, wird gespeichert |
|
||||
| Session | Geöffnete Dateien und aktiver Tab werden wiederhergestellt |
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
BareCode/
|
||||
├── CMakeLists.txt
|
||||
├── main.cpp
|
||||
├── resources/
|
||||
│ ├── resources.qrc
|
||||
│ └── icon_*.png
|
||||
└── src/
|
||||
├── core/ # MainWindow, ProjectManager, Settings, ThemeManager, IPlugin
|
||||
├── editor/ # EditorPanel, EditorTab, CodeEditor, SearchPanel, FileSearchPanel
|
||||
├── filetree/ # FileTreePanel
|
||||
└── highlighter/ # SyntaxHighlighter, HighlighterFactory
|
||||
```
|
||||
|
||||
Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek.
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten & Bauen
|
||||
|
||||
### Linux
|
||||
|
||||
**Abhängigkeiten (Beispiel Ubuntu/Debian):**
|
||||
```bash
|
||||
sudo apt install cmake qt6-base-dev qt6-base-dev-tools libqt6concurrent6
|
||||
```
|
||||
|
||||
**Abhängigkeiten (Arch/Manjaro):**
|
||||
```bash
|
||||
sudo pacman -S cmake qt6-base
|
||||
```
|
||||
|
||||
**Bauen:**
|
||||
```bash
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
**Installieren:**
|
||||
```bash
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Windows (Visual Studio)
|
||||
|
||||
**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
|
||||
build\Release\BareCode.exe
|
||||
```
|
||||
|
||||
**Windows Installer erstellen** (mit [Inno Setup](https://jrsoftware.org/isinfo.php)):
|
||||
```bat
|
||||
windeployqt --release build\Release\BareCode.exe
|
||||
```
|
||||
Danach das Inno Setup Skript ausführen.
|
||||
|
||||
---
|
||||
|
||||
### Haiku
|
||||
|
||||
**Abhängigkeiten:**
|
||||
```bash
|
||||
pkgman install qt6_base qt6_base_devel
|
||||
```
|
||||
|
||||
> **Hinweis:** `qt6_base_devel` enthält auf Haiku sowohl die Header als auch
|
||||
> die Build-Tools (`moc`, `rcc`, `qmake`). Getestet auf Haiku R1/beta5.
|
||||
|
||||
**Bauen:**
|
||||
```bash
|
||||
cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
./build/BareCode
|
||||
```
|
||||
|
||||
**Installieren:**
|
||||
```bash
|
||||
cmake --install build
|
||||
```
|
||||
|
||||
> Nach der Installation wird `mimeset` automatisch aufgerufen um den
|
||||
> MIME-Typ zu registrieren, damit BareCode im Haiku Tracker korrekt
|
||||
> als Anwendung erkannt wird.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| Dark Mode | Strg+Shift+D |
|
||||
|
||||
---
|
||||
|
||||
## Erweitern
|
||||
|
||||
### Neuen Highlighter 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
|
||||
|
||||
Keine weiteren Dateien müssen geändert werden.
|
||||
|
||||
### Neues Panel / Plugin hinzufügen
|
||||
|
||||
1. `IPlugin`-Interface aus `src/core/IPlugin.h` implementieren
|
||||
2. `QWidget`-Subklasse für die UI erstellen
|
||||
3. In `MainWindow` instanziieren und verdrahten
|
||||
|
||||
---
|
||||
|
||||
## Code-Stil
|
||||
|
||||
Allman-Klammerstil, C++17 durchgehend.
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [www.projekt-hirnfrei.de](https://www.projekt-hirnfrei.de)
|
||||
- Discord: [discord.projekt-hirnfrei.de](https://discord.projekt-hirnfrei.de)
|
||||
13
src/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
|
||||
28
src/barecode/main.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include <QApplication>
|
||||
#include <QIcon>
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
app.setApplicationName("BareCode");
|
||||
app.setApplicationVersion("1.1.0");
|
||||
app.setOrganizationName("BareCode");
|
||||
|
||||
// Icon in allen verfügbaren Größen setzen
|
||||
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);
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
BIN
src/barecode/resources/BareCode.ico
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
src/barecode/resources/icon_128.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/barecode/resources/icon_16.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
src/barecode/resources/icon_24.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/barecode/resources/icon_256.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/barecode/resources/icon_32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/barecode/resources/icon_48.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/barecode/resources/icon_512.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
src/barecode/resources/icon_64.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
1197
src/barecode/resources/php_functions.json
Normal file
12
src/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
src/barecode/src/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(editor)
|
||||
add_subdirectory(filetree)
|
||||
add_subdirectory(highlighter)
|
||||
105
src/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.1.0");
|
||||
makeRow(tr("Entwickler"), "Dany Thinnes");
|
||||
makeRow(tr("Projekt"), "Projekt Hirnfrei");
|
||||
makeRow(tr("Framework"), QString("Qt %1").arg(qVersion()));
|
||||
makeRow(tr("Sprache"), "C++17");
|
||||
|
||||
root->addLayout(info);
|
||||
root->addStretch();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Trennlinie + Schließen-Button
|
||||
// -----------------------------------------------------------------------
|
||||
QFrame *line = new QFrame(this);
|
||||
line->setFrameShape(QFrame::HLine);
|
||||
line->setFrameShadow(QFrame::Sunken);
|
||||
root->addWidget(line);
|
||||
|
||||
QHBoxLayout *btnRow = new QHBoxLayout();
|
||||
btnRow->setContentsMargins(12, 8, 12, 12);
|
||||
btnRow->addStretch();
|
||||
|
||||
QPushButton *btnClose = new QPushButton(tr("Schließen"), this);
|
||||
btnClose->setDefault(true);
|
||||
btnClose->setFixedWidth(110);
|
||||
connect(btnClose, &QPushButton::clicked, this, &QDialog::accept);
|
||||
btnRow->addWidget(btnClose);
|
||||
|
||||
root->addLayout(btnRow);
|
||||
}
|
||||
14
src/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
src/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
src/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() {}
|
||||
};
|
||||
336
src/barecode/src/core/MainWindow.cpp
Normal file
@@ -0,0 +1,336 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QCloseEvent>
|
||||
#include <QSettings>
|
||||
|
||||
#include "AboutDialog.h"
|
||||
#include "filetree/FileTreePanel.h"
|
||||
#include "editor/EditorPanel.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_projectManager(std::make_unique<ProjectManager>())
|
||||
, m_settings(std::make_unique<Settings>())
|
||||
, m_themeManager(std::make_unique<ThemeManager>())
|
||||
{
|
||||
setWindowTitle("BareCode");
|
||||
setMinimumSize(900, 600);
|
||||
|
||||
setupUi();
|
||||
setupMenuBar();
|
||||
setupStatusBar();
|
||||
connectSignals();
|
||||
restoreWindowState();
|
||||
applyInitialTheme();
|
||||
|
||||
// Letztes Projekt wieder öffnen
|
||||
const QString lastPath = m_settings->lastProjectPath();
|
||||
if (!lastPath.isEmpty())
|
||||
{
|
||||
m_projectManager->openProject(lastPath);
|
||||
}
|
||||
|
||||
// Letzte Session wiederherstellen (geöffnete Dateien + aktiver Tab)
|
||||
m_editor->restoreSession(
|
||||
m_settings->lastOpenFiles(),
|
||||
m_settings->lastActiveFile()
|
||||
);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI aufbauen
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::setupUi()
|
||||
{
|
||||
m_splitter = new QSplitter(Qt::Horizontal, this);
|
||||
setCentralWidget(m_splitter);
|
||||
|
||||
m_fileTree = new FileTreePanel(m_splitter);
|
||||
m_editor = new EditorPanel(m_settings.get(), m_splitter);
|
||||
|
||||
m_splitter->addWidget(m_fileTree);
|
||||
m_splitter->addWidget(m_editor);
|
||||
|
||||
const int treeWidth = m_settings->fileTreeWidth();
|
||||
m_splitter->setSizes({treeWidth, width() - treeWidth});
|
||||
m_splitter->setStretchFactor(0, 0);
|
||||
m_splitter->setStretchFactor(1, 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Menüleiste
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::setupMenuBar()
|
||||
{
|
||||
// ---- Datei ----
|
||||
QMenu *mDatei = menuBar()->addMenu(tr("&Datei"));
|
||||
|
||||
m_actNewFile = mDatei->addAction(tr("&Neue Datei…"), this, &MainWindow::onNewFile);
|
||||
m_actNewFile->setShortcut(QKeySequence::New);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actOpenFile = mDatei->addAction(tr("Datei &öffnen…"), this, &MainWindow::onOpenFile);
|
||||
m_actOpenFile->setShortcut(QKeySequence::Open);
|
||||
|
||||
m_actOpenProject = mDatei->addAction(tr("&Projekt öffnen…"), this, &MainWindow::onOpenProject);
|
||||
m_actOpenProject->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_O);
|
||||
|
||||
m_actClose = mDatei->addAction(tr("Projekt &schließen"), this, &MainWindow::onCloseProject);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actSave = mDatei->addAction(tr("&Speichern"), this, &MainWindow::onSave);
|
||||
m_actSave->setShortcut(QKeySequence::Save);
|
||||
|
||||
m_actSaveAs = mDatei->addAction(tr("Speichern &unter…"), this, &MainWindow::onSaveAs);
|
||||
m_actSaveAs->setShortcut(QKeySequence::SaveAs);
|
||||
|
||||
m_actSaveAll = mDatei->addAction(tr("&Alles speichern"), this, &MainWindow::onSaveAll);
|
||||
m_actSaveAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
|
||||
|
||||
mDatei->addSeparator();
|
||||
|
||||
m_actQuit = mDatei->addAction(tr("&Beenden"), qApp, &QApplication::quit);
|
||||
m_actQuit->setShortcut(QKeySequence::Quit);
|
||||
|
||||
// ---- Bearbeiten ----
|
||||
QMenu *mBearbeiten = menuBar()->addMenu(tr("&Bearbeiten"));
|
||||
|
||||
m_actUndo = mBearbeiten->addAction(tr("&Rückgängig"), this, &MainWindow::onUndo);
|
||||
m_actUndo->setShortcut(QKeySequence::Undo);
|
||||
|
||||
m_actRedo = mBearbeiten->addAction(tr("&Wiederholen"), this, &MainWindow::onRedo);
|
||||
m_actRedo->setShortcut(QKeySequence::Redo);
|
||||
|
||||
mBearbeiten->addSeparator();
|
||||
|
||||
m_actSearch = mBearbeiten->addAction(tr("&Suchen / Ersetzen…"), this, &MainWindow::onShowSearch);
|
||||
m_actSearch->setShortcut(QKeySequence::Find);
|
||||
|
||||
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
|
||||
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
|
||||
|
||||
// ---- Ansicht ----
|
||||
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
||||
|
||||
m_actDarkMode = mAnsicht->addAction(tr("&Dark Mode"));
|
||||
m_actDarkMode->setCheckable(true);
|
||||
m_actDarkMode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_D);
|
||||
connect(m_actDarkMode, &QAction::toggled, this, &MainWindow::onToggleDarkMode);
|
||||
|
||||
// ---- Hilfe ----
|
||||
QMenu *mHilfe = menuBar()->addMenu(tr("&Hilfe"));
|
||||
m_actAbout = mHilfe->addAction(tr("&Über BareCode…"), this, &MainWindow::onAbout);
|
||||
}
|
||||
|
||||
void MainWindow::setupStatusBar()
|
||||
{
|
||||
statusBar()->showMessage(tr("Bereit"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Signale verbinden
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::connectSignals()
|
||||
{
|
||||
connect(m_projectManager.get(), &ProjectManager::projectOpened,
|
||||
this, &MainWindow::onProjectOpened);
|
||||
|
||||
connect(m_projectManager.get(), &ProjectManager::projectClosed,
|
||||
this, &MainWindow::onProjectClosed);
|
||||
|
||||
// Dateibaum → Editor
|
||||
connect(m_fileTree, &FileTreePanel::fileActivated,
|
||||
m_editor, &EditorPanel::openFile);
|
||||
|
||||
connect(m_fileTree, &FileTreePanel::fileCreated,
|
||||
m_editor, &EditorPanel::openFile);
|
||||
|
||||
// Gespeichert → Statusleiste
|
||||
connect(m_editor, &EditorPanel::currentFileSaved, this, [this](const QString &path)
|
||||
{
|
||||
statusBar()->showMessage(tr("Gespeichert: %1").arg(path), 3000);
|
||||
});
|
||||
|
||||
// Splitter-Breite merken
|
||||
connect(m_splitter, &QSplitter::splitterMoved, this, [this](int pos, int)
|
||||
{
|
||||
m_settings->setFileTreeWidth(pos);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme beim Start
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::applyInitialTheme()
|
||||
{
|
||||
const bool dark = m_settings->darkMode();
|
||||
// Block damit toggled-Signal nicht doppelt feuert
|
||||
m_actDarkMode->blockSignals(true);
|
||||
m_actDarkMode->setChecked(dark);
|
||||
m_actDarkMode->blockSignals(false);
|
||||
|
||||
m_themeManager->applyTheme(dark ? ThemeManager::Theme::Dark
|
||||
: ThemeManager::Theme::Light);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Datei
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onNewFile()
|
||||
{
|
||||
m_fileTree->triggerNewFile();
|
||||
}
|
||||
|
||||
void MainWindow::onOpenFile()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Datei öffnen"),
|
||||
m_settings->lastProjectPath()
|
||||
);
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
m_editor->openFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onOpenProject()
|
||||
{
|
||||
const QString path = QFileDialog::getExistingDirectory(
|
||||
this,
|
||||
tr("Projektverzeichnis öffnen"),
|
||||
m_settings->lastProjectPath()
|
||||
);
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
m_projectManager->openProject(path);
|
||||
m_settings->setLastProjectPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onCloseProject()
|
||||
{
|
||||
m_projectManager->closeProject();
|
||||
}
|
||||
|
||||
void MainWindow::onSave()
|
||||
{
|
||||
m_editor->saveCurrentFile();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveAs()
|
||||
{
|
||||
m_editor->saveCurrentFileAs();
|
||||
}
|
||||
|
||||
void MainWindow::onSaveAll()
|
||||
{
|
||||
m_editor->saveAllFiles();
|
||||
statusBar()->showMessage(tr("Alle Dateien gespeichert"), 3000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Bearbeiten
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onUndo()
|
||||
{
|
||||
m_editor->undo();
|
||||
}
|
||||
|
||||
void MainWindow::onRedo()
|
||||
{
|
||||
m_editor->redo();
|
||||
}
|
||||
|
||||
void MainWindow::onShowSearch()
|
||||
{
|
||||
m_editor->showSearchPanel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Ansicht
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onToggleDarkMode(bool checked)
|
||||
{
|
||||
m_themeManager->applyTheme(checked ? ThemeManager::Theme::Dark
|
||||
: ThemeManager::Theme::Light);
|
||||
m_settings->setDarkMode(checked);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Hilfe
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onAbout()
|
||||
{
|
||||
AboutDialog dlg(this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Projekt
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::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();
|
||||
}
|
||||
87
src/barecode/src/core/MainWindow.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QSplitter>
|
||||
#include <QMenuBar>
|
||||
#include <QStatusBar>
|
||||
#include <QAction>
|
||||
#include <memory>
|
||||
|
||||
#include "ProjectManager.h"
|
||||
#include "Settings.h"
|
||||
#include "ThemeManager.h"
|
||||
|
||||
class FileTreePanel;
|
||||
class EditorPanel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainWindow – Hauptfenster. Besitzt alle zentralen Dienste und das Layout.
|
||||
// ---------------------------------------------------------------------------
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
// Datei
|
||||
void onNewFile();
|
||||
void onOpenFile();
|
||||
void onOpenProject();
|
||||
void onCloseProject();
|
||||
void onSave();
|
||||
void onSaveAs();
|
||||
void onSaveAll();
|
||||
// Bearbeiten
|
||||
void onUndo();
|
||||
void onRedo();
|
||||
void onShowSearch();
|
||||
void onShowFileSearch();
|
||||
// Ansicht
|
||||
void onToggleDarkMode(bool checked);
|
||||
// Hilfe
|
||||
void onAbout();
|
||||
// Intern
|
||||
void onProjectOpened(const QString &path);
|
||||
void onProjectClosed();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void setupMenuBar();
|
||||
void setupStatusBar();
|
||||
void connectSignals();
|
||||
void applyInitialTheme();
|
||||
void saveWindowState();
|
||||
void restoreWindowState();
|
||||
|
||||
// Dienste
|
||||
std::unique_ptr<ProjectManager> m_projectManager;
|
||||
std::unique_ptr<Settings> m_settings;
|
||||
std::unique_ptr<ThemeManager> m_themeManager;
|
||||
|
||||
// Layout
|
||||
QSplitter *m_splitter = nullptr;
|
||||
FileTreePanel *m_fileTree = nullptr;
|
||||
EditorPanel *m_editor = nullptr;
|
||||
|
||||
// Aktionen
|
||||
QAction *m_actNewFile = nullptr;
|
||||
QAction *m_actOpenFile = nullptr;
|
||||
QAction *m_actOpenProject = nullptr;
|
||||
QAction *m_actClose = nullptr;
|
||||
QAction *m_actSave = nullptr;
|
||||
QAction *m_actSaveAs = nullptr;
|
||||
QAction *m_actSaveAll = nullptr;
|
||||
QAction *m_actQuit = nullptr;
|
||||
QAction *m_actUndo = nullptr;
|
||||
QAction *m_actRedo = nullptr;
|
||||
QAction *m_actSearch = nullptr;
|
||||
QAction *m_actFileSearch = nullptr;
|
||||
QAction *m_actDarkMode = nullptr;
|
||||
QAction *m_actAbout = nullptr;
|
||||
};
|
||||
33
src/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
src/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
src/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
src/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
src/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
src/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;
|
||||
};
|
||||
35
src/barecode/src/editor/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
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
|
||||
)
|
||||
|
||||
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}/..
|
||||
)
|
||||
556
src/barecode/src/editor/CodeEditor.cpp
Normal file
@@ -0,0 +1,556 @@
|
||||
#include "CodeEditor.h"
|
||||
#include "LineNumberArea.h"
|
||||
#include "ColorIndicator.h"
|
||||
#include "SignatureHelper.h"
|
||||
|
||||
#include "core/Settings.h"
|
||||
#include "highlighter/HighlighterFactory.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTextBlock>
|
||||
#include <QPaintEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#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);
|
||||
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::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
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::highlightCurrentLine()
|
||||
{
|
||||
QList<QTextEdit::ExtraSelection> extraSelections;
|
||||
|
||||
if (!isReadOnly())
|
||||
{
|
||||
QTextEdit::ExtraSelection selection;
|
||||
|
||||
const QColor lineColor = palette().color(QPalette::AlternateBase);
|
||||
selection.format.setBackground(lineColor);
|
||||
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
selection.cursor = textCursor();
|
||||
selection.cursor.clearSelection();
|
||||
|
||||
extraSelections.append(selection);
|
||||
}
|
||||
|
||||
setExtraSelections(extraSelections);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Key handling – 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());
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
const QString lineText = b.text();
|
||||
int toRemove = 0;
|
||||
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
// Bis zu tabSize führende Leerzeichen entfernen
|
||||
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
|
||||
{
|
||||
if (lineText[i] == ' ')
|
||||
{
|
||||
++toRemove;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Einen führenden Tab entfernen
|
||||
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())
|
||||
{
|
||||
// Mehrere Zeilen einrücken
|
||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
QTextCursor lineCursor(b);
|
||||
lineCursor.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);
|
||||
}
|
||||
|
||||
76
src/barecode/src/editor/CodeEditor.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QFont>
|
||||
#include <QString>
|
||||
#include <QTextBlock>
|
||||
|
||||
class LineNumberArea;
|
||||
class Settings;
|
||||
class SyntaxHighlighter;
|
||||
class ColorIndicator;
|
||||
class SignatureHelper;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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(); }
|
||||
|
||||
signals:
|
||||
void fileSaved(const QString &filePath);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void updateLineNumberAreaWidth(int newBlockCount);
|
||||
void highlightCurrentLine();
|
||||
void updateLineNumberArea(const QRect &rect, int dy);
|
||||
|
||||
private:
|
||||
void setupEditor();
|
||||
void installHighlighter(const QString &filePath);
|
||||
bool writeToFile(const QString &filePath);
|
||||
|
||||
Settings *m_settings = nullptr;
|
||||
LineNumberArea *m_lineNumberArea = nullptr;
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
ColorIndicator *m_colorIndicator = nullptr;
|
||||
SignatureHelper *m_signatureHelper = nullptr;
|
||||
QString m_filePath;
|
||||
};
|
||||
361
src/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
src/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;
|
||||
};
|
||||
258
src/barecode/src/editor/EditorPanel.cpp
Normal file
@@ -0,0 +1,258 @@
|
||||
#include "EditorPanel.h"
|
||||
#include "EditorTab.h"
|
||||
#include "CodeEditor.h"
|
||||
#include "SearchPanel.h"
|
||||
#include "FileSearchPanel.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_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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
|
||||
// Ä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);
|
||||
});
|
||||
}
|
||||
|
||||
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::setSearchRoot(const QString &path)
|
||||
{
|
||||
m_fileSearch->setSearchRoot(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);
|
||||
}
|
||||
60
src/barecode/src/editor/EditorPanel.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
class EditorTab;
|
||||
class Settings;
|
||||
class SearchPanel;
|
||||
class FileSearchPanel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 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;
|
||||
|
||||
QHash<QString, EditorTab *> m_openTabs;
|
||||
};
|
||||
53
src/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
src/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
src/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
src/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;
|
||||
};
|
||||
18
src/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
src/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
src/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
src/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
src/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
src/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
src/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
src/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;
|
||||
};
|
||||
17
src/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
src/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
src/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
src/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
src/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;
|
||||
}
|
||||