Dateien, die als Parameter übergeben werden, werden nach dem Start direkt geladen
This commit is contained in:
49
barecode/src/editor/CMakeLists.txt
Normal file
49
barecode/src/editor/CMakeLists.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
set(EDITOR_SOURCES
|
||||
EditorPanel.cpp
|
||||
EditorPanel.h
|
||||
CodeEditor.cpp
|
||||
CodeEditor.h
|
||||
LineNumberArea.cpp
|
||||
LineNumberArea.h
|
||||
EditorTab.cpp
|
||||
EditorTab.h
|
||||
SearchPanel.cpp
|
||||
SearchPanel.h
|
||||
FileSearchPanel.cpp
|
||||
FileSearchPanel.h
|
||||
ColorIndicator.cpp
|
||||
ColorIndicator.h
|
||||
SignatureHelper.cpp
|
||||
SignatureHelper.h
|
||||
SignatureTooltip.cpp
|
||||
SignatureTooltip.h
|
||||
FunctionScanner.cpp
|
||||
FunctionScanner.h
|
||||
FunctionIndex.cpp
|
||||
FunctionIndex.h
|
||||
FunctionListPanel.cpp
|
||||
FunctionListPanel.h
|
||||
FunctionListDialog.cpp
|
||||
FunctionListDialog.h
|
||||
DeadCodeAnalyzer.cpp
|
||||
DeadCodeAnalyzer.h
|
||||
DeadCodeDialog.cpp
|
||||
DeadCodeDialog.h
|
||||
VariableCompleter.cpp
|
||||
VariableCompleter.h
|
||||
)
|
||||
|
||||
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
||||
|
||||
target_link_libraries(BareCode_Editor PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Concurrent
|
||||
BareCode_Highlighter
|
||||
)
|
||||
|
||||
target_include_directories(BareCode_Editor PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
)
|
||||
762
barecode/src/editor/CodeEditor.cpp
Normal file
762
barecode/src/editor/CodeEditor.cpp
Normal file
@@ -0,0 +1,762 @@
|
||||
#include "CodeEditor.h"
|
||||
#include "LineNumberArea.h"
|
||||
#include "ColorIndicator.h"
|
||||
#include "SignatureHelper.h"
|
||||
#include "FunctionIndex.h"
|
||||
#include "VariableCompleter.h"
|
||||
|
||||
#include "core/Settings.h"
|
||||
#include "highlighter/HighlighterFactory.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QTextBlock>
|
||||
#include <QPaintEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QFocusEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QFileInfo>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
CodeEditor::CodeEditor(Settings *settings, QWidget *parent)
|
||||
: QPlainTextEdit(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
m_lineNumberArea = new LineNumberArea(this);
|
||||
m_colorIndicator = new ColorIndicator(this);
|
||||
m_signatureHelper = new SignatureHelper(this);
|
||||
m_varCompleter = new VariableCompleter(this);
|
||||
setupEditor();
|
||||
|
||||
connect(this, &CodeEditor::blockCountChanged,
|
||||
this, &CodeEditor::updateLineNumberAreaWidth);
|
||||
|
||||
connect(this, &CodeEditor::updateRequest,
|
||||
this, &CodeEditor::updateLineNumberArea);
|
||||
|
||||
connect(this, &CodeEditor::cursorPositionChanged,
|
||||
this, &CodeEditor::highlightCurrentLine);
|
||||
|
||||
updateLineNumberAreaWidth(0);
|
||||
highlightCurrentLine();
|
||||
}
|
||||
|
||||
CodeEditor::~CodeEditor() = default;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::setupEditor()
|
||||
{
|
||||
applySettings();
|
||||
setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
}
|
||||
|
||||
void CodeEditor::applySettings()
|
||||
{
|
||||
setFont(m_settings->editorFont());
|
||||
|
||||
const int tabStop = m_settings->tabSize();
|
||||
// Set tab stop width in pixels using font metrics
|
||||
QFontMetrics fm(m_settings->editorFont());
|
||||
setTabStopDistance(static_cast<qreal>(tabStop) * fm.horizontalAdvance(' '));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File I/O
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::loadFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Open File"),
|
||||
tr("Cannot open file:\n%1").arg(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
m_filePath = filePath;
|
||||
|
||||
QTextStream in(&file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
in.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
in.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
setPlainText(in.readAll());
|
||||
document()->setModified(false);
|
||||
|
||||
installHighlighter(filePath);
|
||||
}
|
||||
|
||||
QString CodeEditor::filePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
bool CodeEditor::save()
|
||||
{
|
||||
if (m_filePath.isEmpty())
|
||||
{
|
||||
return saveAs();
|
||||
}
|
||||
|
||||
return writeToFile(m_filePath);
|
||||
}
|
||||
|
||||
bool CodeEditor::saveAs()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Speichern unter"),
|
||||
m_filePath
|
||||
);
|
||||
|
||||
if (path.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_filePath = path;
|
||||
installHighlighter(m_filePath);
|
||||
return writeToFile(m_filePath);
|
||||
}
|
||||
|
||||
bool CodeEditor::writeToFile(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Speichern"),
|
||||
tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath));
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
out.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
out.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
out << toPlainText();
|
||||
document()->setModified(false);
|
||||
emit fileSaved(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CodeEditor::installHighlighter(const QString &filePath)
|
||||
{
|
||||
// Remove old highlighter first
|
||||
delete m_highlighter;
|
||||
m_highlighter = nullptr;
|
||||
|
||||
m_highlighter = HighlighterFactory::createForFile(filePath, document());
|
||||
}
|
||||
|
||||
bool CodeEditor::isModified() const
|
||||
{
|
||||
return document()->isModified();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Line number area
|
||||
// ---------------------------------------------------------------------------
|
||||
int CodeEditor::lineNumberAreaWidth() const
|
||||
{
|
||||
int digits = 1;
|
||||
int max = qMax(1, blockCount());
|
||||
while (max >= 10)
|
||||
{
|
||||
max /= 10;
|
||||
++digits;
|
||||
}
|
||||
|
||||
const int padding = 8;
|
||||
return fontMetrics().horizontalAdvance('9') * digits + padding * 2;
|
||||
}
|
||||
|
||||
void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/)
|
||||
{
|
||||
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
||||
}
|
||||
|
||||
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||||
{
|
||||
if (dy != 0)
|
||||
{
|
||||
m_lineNumberArea->scroll(0, dy);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
|
||||
}
|
||||
|
||||
if (rect.contains(viewport()->rect()))
|
||||
{
|
||||
updateLineNumberAreaWidth(0);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeEditor::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
// Zuerst den normalen Editor-Inhalt zeichnen
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
|
||||
// Einrück-Führungslinien
|
||||
const int tabSize = m_settings->tabSize();
|
||||
if (tabSize > 0)
|
||||
{
|
||||
QPainter painter(viewport());
|
||||
|
||||
QColor guideColor = palette().color(QPalette::Text);
|
||||
guideColor.setAlpha(30);
|
||||
painter.setPen(QPen(guideColor, 1, Qt::SolidLine));
|
||||
|
||||
const QFontMetrics fm(font());
|
||||
const int spaceWidth = fm.horizontalAdvance(' ');
|
||||
const int tabPixels = tabSize * spaceWidth;
|
||||
|
||||
if (tabPixels > 0)
|
||||
{
|
||||
int textOriginX = 0;
|
||||
{
|
||||
QTextBlock firstBlock = firstVisibleBlock();
|
||||
if (!firstBlock.isValid())
|
||||
{
|
||||
firstBlock = document()->begin();
|
||||
}
|
||||
if (firstBlock.isValid())
|
||||
{
|
||||
const QRectF blockRect = blockBoundingGeometry(firstBlock)
|
||||
.translated(contentOffset());
|
||||
const QTextLayout *layout = firstBlock.layout();
|
||||
if (layout && layout->lineCount() > 0)
|
||||
{
|
||||
textOriginX = static_cast<int>(blockRect.left()
|
||||
+ layout->lineAt(0).position().x());
|
||||
}
|
||||
else
|
||||
{
|
||||
textOriginX = static_cast<int>(blockRect.left());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int scrollX = horizontalScrollBar()->value();
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
const int bottom = event->rect().bottom();
|
||||
|
||||
while (block.isValid())
|
||||
{
|
||||
const QRectF blockRect = blockBoundingGeometry(block)
|
||||
.translated(contentOffset());
|
||||
if (blockRect.top() > bottom) { break; }
|
||||
|
||||
if (block.isVisible())
|
||||
{
|
||||
const QString text = block.text();
|
||||
int indentSpaces = 0;
|
||||
for (const QChar &ch : text)
|
||||
{
|
||||
if (ch == ' ') { ++indentSpaces; }
|
||||
else if (ch == '\t') { indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize; }
|
||||
else { break; }
|
||||
}
|
||||
|
||||
const int indentStops = indentSpaces / tabSize;
|
||||
for (int stop = 1; stop <= indentStops; ++stop)
|
||||
{
|
||||
const int xPixel = textOriginX + stop * tabPixels - scrollX;
|
||||
if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
painter.drawLine(xPixel,
|
||||
static_cast<int>(blockRect.top()),
|
||||
xPixel,
|
||||
static_cast<int>(blockRect.bottom()));
|
||||
}
|
||||
}
|
||||
block = block.next();
|
||||
}
|
||||
}
|
||||
|
||||
// Farbvorschau-Quadrate zeichnen
|
||||
m_colorIndicator->paint(painter, event);
|
||||
}
|
||||
}
|
||||
|
||||
void CodeEditor::setFunctionIndex(FunctionIndex *index)
|
||||
{
|
||||
m_functionIndex = index;
|
||||
}
|
||||
|
||||
void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
// Zuerst normales Verhalten — markiert das Wort unter dem Cursor
|
||||
QPlainTextEdit::mouseDoubleClickEvent(event);
|
||||
|
||||
if (!m_functionIndex || !m_functionIndex->isReady())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Markiertes Wort auslesen
|
||||
const QString word = textCursor().selectedText().trimmed();
|
||||
if (word.isEmpty() || word.contains(' '))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Im Funktionsindex nachschlagen
|
||||
const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word);
|
||||
if (info.filePath.isEmpty())
|
||||
{
|
||||
return; // Nicht gefunden — normales Verhalten bleibt
|
||||
}
|
||||
|
||||
// Nicht zur eigenen Definition springen wenn wir bereits dort sind
|
||||
if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
emit navigateToRequested(info.filePath, info.line);
|
||||
}
|
||||
|
||||
void CodeEditor::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
// Zuerst prüfen ob ein Farbquadrat geklickt wurde
|
||||
if (m_colorIndicator->handleMousePress(event))
|
||||
{
|
||||
return;
|
||||
}
|
||||
QPlainTextEdit::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void CodeEditor::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QPlainTextEdit::resizeEvent(event);
|
||||
|
||||
const QRect cr = contentsRect();
|
||||
m_lineNumberArea->setGeometry(
|
||||
QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())
|
||||
);
|
||||
}
|
||||
|
||||
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPainter painter(m_lineNumberArea);
|
||||
|
||||
// Background
|
||||
const QColor bgColor = palette().color(QPalette::Window).darker(110);
|
||||
painter.fillRect(event->rect(), bgColor);
|
||||
|
||||
const QColor lineNumColor = palette().color(QPalette::Mid);
|
||||
const QColor activeColor = palette().color(QPalette::Text);
|
||||
|
||||
const int currentLine = textCursor().blockNumber();
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
int blockNumber = block.blockNumber();
|
||||
int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
|
||||
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||||
|
||||
while (block.isValid() && top <= event->rect().bottom())
|
||||
{
|
||||
if (block.isVisible() && bottom >= event->rect().top())
|
||||
{
|
||||
const QString number = QString::number(blockNumber + 1);
|
||||
painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor);
|
||||
painter.drawText(
|
||||
0,
|
||||
top,
|
||||
m_lineNumberArea->width() - 4,
|
||||
fontMetrics().height(),
|
||||
Qt::AlignRight,
|
||||
number
|
||||
);
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
top = bottom;
|
||||
bottom = top + static_cast<int>(blockBoundingRect(block).height());
|
||||
++blockNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Current line highlight + Klammerzugehörigkeit
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::highlightCurrentLine()
|
||||
{
|
||||
QList<QTextEdit::ExtraSelection> extraSelections;
|
||||
|
||||
if (!isReadOnly())
|
||||
{
|
||||
// Aktuelle Zeile hervorheben
|
||||
QTextEdit::ExtraSelection lineSelection;
|
||||
lineSelection.format.setBackground(palette().color(QPalette::AlternateBase));
|
||||
lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||||
lineSelection.cursor = textCursor();
|
||||
lineSelection.cursor.clearSelection();
|
||||
extraSelections.append(lineSelection);
|
||||
|
||||
// Klammerzugehörigkeit
|
||||
matchBrackets(extraSelections);
|
||||
}
|
||||
|
||||
setExtraSelections(extraSelections);
|
||||
}
|
||||
|
||||
void CodeEditor::matchBrackets(QList<QTextEdit::ExtraSelection> &selections)
|
||||
{
|
||||
static const QString openBrackets = "({[";
|
||||
static const QString closeBrackets = ")}]";
|
||||
|
||||
QTextCursor cursor = textCursor();
|
||||
const QString blockText = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
// Zeichen unter oder links vom Cursor prüfen
|
||||
QChar ch;
|
||||
int charPos = -1;
|
||||
|
||||
// Zuerst Zeichen unter dem Cursor
|
||||
if (col < blockText.length())
|
||||
{
|
||||
ch = blockText[col];
|
||||
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
|
||||
{
|
||||
charPos = col;
|
||||
}
|
||||
}
|
||||
|
||||
// Dann Zeichen links vom Cursor
|
||||
if (charPos == -1 && col > 0)
|
||||
{
|
||||
ch = blockText[col - 1];
|
||||
if (openBrackets.contains(ch) || closeBrackets.contains(ch))
|
||||
{
|
||||
charPos = col - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (charPos == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Passende Klammer suchen
|
||||
const bool isOpen = openBrackets.contains(ch);
|
||||
const int bracketIndex = isOpen
|
||||
? openBrackets.indexOf(ch)
|
||||
: closeBrackets.indexOf(ch);
|
||||
const QChar matchChar = isOpen
|
||||
? closeBrackets[bracketIndex]
|
||||
: openBrackets[bracketIndex];
|
||||
|
||||
// Dokumentposition der gefundenen Klammer
|
||||
const int startPos = cursor.block().position() + charPos;
|
||||
|
||||
// Passende Klammer suchen — vorwärts oder rückwärts
|
||||
int depth = 0;
|
||||
int matchPos = -1;
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
// Vorwärts suchen
|
||||
QTextCursor search(document());
|
||||
search.setPosition(startPos);
|
||||
|
||||
while (!search.atEnd())
|
||||
{
|
||||
search.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
const QChar c = search.selectedText()[0];
|
||||
search.clearSelection();
|
||||
|
||||
if (c == ch) { ++depth; }
|
||||
else if (c == matchChar)
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
matchPos = search.position() - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rückwärts suchen
|
||||
QTextCursor search(document());
|
||||
search.setPosition(startPos + 1);
|
||||
|
||||
while (search.position() > 0)
|
||||
{
|
||||
search.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
|
||||
const QChar c = search.selectedText()[0];
|
||||
search.clearSelection();
|
||||
|
||||
if (c == ch) { ++depth; }
|
||||
else if (c == matchChar)
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
matchPos = search.position();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formatierung
|
||||
QTextCharFormat matchFormat;
|
||||
if (matchPos >= 0)
|
||||
{
|
||||
// Gefunden — beide Klammern grün hervorheben
|
||||
matchFormat.setBackground(QColor("#1a5a1a"));
|
||||
matchFormat.setForeground(QColor("#88ff88"));
|
||||
matchFormat.setFontWeight(QFont::Bold);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Kein Match — rot markieren
|
||||
matchFormat.setBackground(QColor("#5a1a1a"));
|
||||
matchFormat.setForeground(QColor("#ff8888"));
|
||||
matchFormat.setFontWeight(QFont::Bold);
|
||||
}
|
||||
|
||||
// Öffnende / schließende Klammer markieren
|
||||
QTextEdit::ExtraSelection sel1;
|
||||
sel1.cursor = QTextCursor(document());
|
||||
sel1.cursor.setPosition(startPos);
|
||||
sel1.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
sel1.format = matchFormat;
|
||||
selections.append(sel1);
|
||||
|
||||
// Passende Klammer markieren (nur wenn gefunden)
|
||||
if (matchPos >= 0)
|
||||
{
|
||||
QTextEdit::ExtraSelection sel2;
|
||||
sel2.cursor = QTextCursor(document());
|
||||
sel2.cursor.setPosition(matchPos);
|
||||
sel2.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
|
||||
sel2.format = matchFormat;
|
||||
selections.append(sel2);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
const int tabSize = m_settings->tabSize();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shift+Tab: Einrückung zurückziehen
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Backtab ||
|
||||
(event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier))
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||
|
||||
// Wenn die Selektion genau am Anfang des letzten Blocks endet,
|
||||
// diesen Block nicht mit einbeziehen — der Cursor steht dort nur
|
||||
// mit Position 0, der Nutzer hat die Zeile nicht markiert
|
||||
if (cursor.hasSelection() &&
|
||||
cursor.selectionEnd() == endBlock.position() &&
|
||||
endBlock != startBlock)
|
||||
{
|
||||
endBlock = endBlock.previous();
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
const QString lineText = b.text();
|
||||
int toRemove = 0;
|
||||
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
|
||||
{
|
||||
if (lineText[i] == ' ') { ++toRemove; }
|
||||
else { break; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!lineText.isEmpty() && lineText[0] == '\t')
|
||||
{
|
||||
toRemove = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove > 0)
|
||||
{
|
||||
QTextCursor lineCursor(b);
|
||||
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
lineCursor.movePosition(QTextCursor::Right,
|
||||
QTextCursor::KeepAnchor,
|
||||
toRemove);
|
||||
lineCursor.removeSelectedText();
|
||||
}
|
||||
}
|
||||
cursor.endEditBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Tab: Einrücken (Leerzeichen oder echter Tab)
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Tab)
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
|
||||
if (cursor.hasSelection())
|
||||
{
|
||||
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
|
||||
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
|
||||
|
||||
// Gleiche Korrektur: Cursor am Zeilenanfang → Zeile nicht einrücken
|
||||
if (cursor.selectionEnd() == endBlock.position() &&
|
||||
endBlock != startBlock)
|
||||
{
|
||||
endBlock = endBlock.previous();
|
||||
}
|
||||
|
||||
cursor.beginEditBlock();
|
||||
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
|
||||
{
|
||||
QTextCursor lineCursor(b);
|
||||
lineCursor.movePosition(QTextCursor::StartOfBlock);
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
lineCursor.insertText(QString(tabSize, ' '));
|
||||
}
|
||||
else
|
||||
{
|
||||
lineCursor.insertText("\t");
|
||||
}
|
||||
}
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_settings->useSpacesForTabs())
|
||||
{
|
||||
// Zum nächsten Tab-Stop auffüllen
|
||||
const int col = cursor.columnNumber();
|
||||
const int spacesNeeded = tabSize - (col % tabSize);
|
||||
cursor.insertText(QString(spacesNeeded, ' '));
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor.insertText("\t");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Smart Backspace: springt zur vorherigen Einrückungsstufe
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Backspace
|
||||
&& !textCursor().hasSelection()
|
||||
&& m_settings->useSpacesForTabs())
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
if (col > 0)
|
||||
{
|
||||
// Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen
|
||||
const QString lineText = cursor.block().text();
|
||||
const QString leftOfCursor = lineText.left(col);
|
||||
const bool onlySpaces = leftOfCursor.trimmed().isEmpty();
|
||||
|
||||
if (onlySpaces && col > 0)
|
||||
{
|
||||
// Zur vorherigen Tab-Stop-Position springen
|
||||
const int targetCol = ((col - 1) / tabSize) * tabSize;
|
||||
const int toDelete = col - targetCol;
|
||||
|
||||
cursor.movePosition(QTextCursor::Left,
|
||||
QTextCursor::KeepAnchor,
|
||||
toDelete);
|
||||
cursor.removeSelectedText();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Enter / Return: Auto-Indent
|
||||
// -----------------------------------------------------------------------
|
||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
const QString currentLine = cursor.block().text();
|
||||
|
||||
// Führende Leerzeichen der aktuellen Zeile zählen
|
||||
int leadingSpaces = 0;
|
||||
for (const QChar &ch : currentLine)
|
||||
{
|
||||
if (ch == ' ')
|
||||
{
|
||||
++leadingSpaces;
|
||||
}
|
||||
else if (ch == '\t')
|
||||
{
|
||||
leadingSpaces += tabSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
|
||||
if (leadingSpaces > 0)
|
||||
{
|
||||
const QString indent = m_settings->useSpacesForTabs()
|
||||
? QString(leadingSpaces, ' ')
|
||||
: QString(leadingSpaces / tabSize, '\t');
|
||||
textCursor().insertText(indent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QPlainTextEdit::keyPressEvent(event);
|
||||
|
||||
// Variablen-Popup nach jedem Tastendruck aktualisieren
|
||||
m_varCompleter->handleKeyPress(event);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fokus verloren — z. B. beim Wechsel zu einem anderen Tab.
|
||||
// Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst
|
||||
// bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen
|
||||
// Dateien den Fokus blockieren.
|
||||
// ---------------------------------------------------------------------------
|
||||
void CodeEditor::focusOutEvent(QFocusEvent *event)
|
||||
{
|
||||
QPlainTextEdit::focusOutEvent(event);
|
||||
m_varCompleter->notifyFocusLost();
|
||||
}
|
||||
|
||||
86
barecode/src/editor/CodeEditor.h
Normal file
86
barecode/src/editor/CodeEditor.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QFont>
|
||||
#include <QString>
|
||||
#include <QTextBlock>
|
||||
|
||||
class LineNumberArea;
|
||||
class Settings;
|
||||
class SyntaxHighlighter;
|
||||
class ColorIndicator;
|
||||
class SignatureHelper;
|
||||
class FunctionIndex;
|
||||
class VariableCompleter;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CodeEditor – Core editing widget.
|
||||
// Features:
|
||||
// • Line number gutter
|
||||
// • Current-line highlight
|
||||
// • Auto-indent on Enter
|
||||
// • Tab → spaces (configurable)
|
||||
// • Syntax highlighting (via pluggable SyntaxHighlighter)
|
||||
// ---------------------------------------------------------------------------
|
||||
class CodeEditor : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CodeEditor(Settings *settings, QWidget *parent = nullptr);
|
||||
~CodeEditor() override;
|
||||
|
||||
void loadFile(const QString &filePath);
|
||||
void applySettings();
|
||||
|
||||
// Speichern
|
||||
bool save();
|
||||
bool saveAs();
|
||||
|
||||
// Called by LineNumberArea
|
||||
int lineNumberAreaWidth() const;
|
||||
void lineNumberAreaPaintEvent(QPaintEvent *event);
|
||||
|
||||
QString filePath() const;
|
||||
bool isModified() const;
|
||||
|
||||
// Öffentliche Hilfsmethoden für ColorIndicator
|
||||
// (die Qt-Originale sind protected und von außen nicht erreichbar)
|
||||
QTextBlock firstVisibleBlockPublic() const { return firstVisibleBlock(); }
|
||||
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
|
||||
QPointF contentOffsetPublic() const { return contentOffset(); }
|
||||
|
||||
void setFunctionIndex(FunctionIndex *index);
|
||||
|
||||
signals:
|
||||
void fileSaved(const QString &filePath);
|
||||
void navigateToRequested(const QString &filePath, int line);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
void focusOutEvent(QFocusEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void updateLineNumberAreaWidth(int newBlockCount);
|
||||
void highlightCurrentLine();
|
||||
void updateLineNumberArea(const QRect &rect, int dy);
|
||||
|
||||
private:
|
||||
void setupEditor();
|
||||
void installHighlighter(const QString &filePath);
|
||||
bool writeToFile(const QString &filePath);
|
||||
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
|
||||
|
||||
Settings *m_settings = nullptr;
|
||||
LineNumberArea *m_lineNumberArea = nullptr;
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
ColorIndicator *m_colorIndicator = nullptr;
|
||||
SignatureHelper *m_signatureHelper = nullptr;
|
||||
FunctionIndex *m_functionIndex = nullptr;
|
||||
VariableCompleter *m_varCompleter = nullptr;
|
||||
QString m_filePath;
|
||||
};
|
||||
361
barecode/src/editor/ColorIndicator.cpp
Normal file
361
barecode/src/editor/ColorIndicator.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
#include "ColorIndicator.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QColorDialog>
|
||||
#include <QTextBlock>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
#include <QScrollBar>
|
||||
#include <QHash>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kombinierter Regex — erfasst alle CSS-Farbformate in einer Runde
|
||||
// ---------------------------------------------------------------------------
|
||||
const QRegularExpression ColorIndicator::s_colorRegex(
|
||||
// #rgb / #rrggbb / #rrggbbaa
|
||||
R"(#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?=[^0-9A-Fa-f]|$))"
|
||||
R"(|rgba?\s*\([^)]+\))"
|
||||
R"(|hsla?\s*\([^)]+\))",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
ColorIndicator::ColorIndicator(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Cache invalidieren und Viewport neu zeichnen wenn sich der Text ändert
|
||||
connect(m_editor->document(), &QTextDocument::contentsChanged,
|
||||
this, [this]()
|
||||
{
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
m_editor->viewport()->update();
|
||||
});
|
||||
|
||||
// Auch beim Scrollen neu zeichnen (Cache bleibt gültig, nur Position ändert sich)
|
||||
connect(m_editor->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this]()
|
||||
{
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paint – wird aus CodeEditor::paintEvent aufgerufen
|
||||
// ---------------------------------------------------------------------------
|
||||
void ColorIndicator::paint(QPainter &painter, QPaintEvent *event)
|
||||
{
|
||||
rebuildCache();
|
||||
|
||||
const int squareSize = m_editor->fontMetrics().height() - 4;
|
||||
const int radius = 2;
|
||||
|
||||
for (const ColorMatch &m : m_cache)
|
||||
{
|
||||
if (!event->rect().intersects(m.rect))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rahmen
|
||||
painter.setPen(QColor(0, 0, 0, 80));
|
||||
painter.setBrush(m.color);
|
||||
painter.drawRoundedRect(m.rect, radius, radius);
|
||||
|
||||
// Schachbrettmuster als Hintergrund für transparente Farben
|
||||
if (m.color.alpha() < 255)
|
||||
{
|
||||
const int half = squareSize / 2;
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(180, 180, 180));
|
||||
painter.drawRect(m.rect.x(), m.rect.y(), half, half);
|
||||
painter.drawRect(m.rect.x() + half, m.rect.y() + half, half, half);
|
||||
painter.setBrush(m.color);
|
||||
painter.drawRoundedRect(m.rect, radius, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mouse – wird aus CodeEditor::mousePressEvent aufgerufen
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ColorIndicator::handleMousePress(QMouseEvent *event)
|
||||
{
|
||||
for (const ColorMatch &m : m_cache)
|
||||
{
|
||||
if (!m.rect.contains(event->pos()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Farb-Dialog öffnen
|
||||
QColorDialog dlg(m.color, m_editor);
|
||||
dlg.setOption(QColorDialog::ShowAlphaChannel, true);
|
||||
dlg.setWindowTitle(QObject::tr("Farbe wählen"));
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const QColor newColor = dlg.selectedColor();
|
||||
|
||||
// Ursprünglichen Farbwert im Dokument ersetzen
|
||||
QTextBlock block = m_editor->document()->findBlockByNumber(m.blockNumber);
|
||||
if (!block.isValid())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Neuen Farbwert als Hex-String formatieren
|
||||
QString newValue;
|
||||
if (newColor.alpha() < 255)
|
||||
{
|
||||
newValue = newColor.name(QColor::HexArgb); // #aarrggbb
|
||||
// CSS erwartet #rrggbbaa — Bytes umstellen
|
||||
// Qt liefert #aarrggbb, CSS will #rrggbbaa
|
||||
newValue = QString("#%1%2%3%4")
|
||||
.arg(newColor.red(), 2, 16, QChar('0'))
|
||||
.arg(newColor.green(), 2, 16, QChar('0'))
|
||||
.arg(newColor.blue(), 2, 16, QChar('0'))
|
||||
.arg(newColor.alpha(), 2, 16, QChar('0'));
|
||||
}
|
||||
else
|
||||
{
|
||||
newValue = newColor.name(QColor::HexRgb); // #rrggbb
|
||||
}
|
||||
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(block.position() + m.posInBlock);
|
||||
cursor.setPosition(block.position() + m.posInBlock + m.length,
|
||||
QTextCursor::KeepAnchor);
|
||||
cursor.insertText(newValue);
|
||||
|
||||
// Cache invalidieren
|
||||
m_cache.clear();
|
||||
m_cacheFirstBlock = -1;
|
||||
m_cacheLastBlock = -1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache aufbauen – nur für sichtbare Blöcke
|
||||
// ---------------------------------------------------------------------------
|
||||
void ColorIndicator::rebuildCache()
|
||||
{
|
||||
QTextBlock firstVisible = m_editor->firstVisibleBlockPublic();
|
||||
const int firstNum = firstVisible.blockNumber();
|
||||
|
||||
// Letzten sichtbaren Block bestimmen
|
||||
int lastNum = firstNum;
|
||||
{
|
||||
QTextBlock b = firstVisible;
|
||||
const int bot = m_editor->viewport()->height();
|
||||
while (b.isValid())
|
||||
{
|
||||
const QRectF r = m_editor->blockBoundingGeometryPublic(b)
|
||||
.translated(m_editor->contentOffsetPublic());
|
||||
if (r.top() > bot)
|
||||
{
|
||||
break;
|
||||
}
|
||||
lastNum = b.blockNumber();
|
||||
b = b.next();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache noch aktuell?
|
||||
if (firstNum == m_cacheFirstBlock && lastNum == m_cacheLastBlock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_cache.clear();
|
||||
m_cacheFirstBlock = firstNum;
|
||||
m_cacheLastBlock = lastNum;
|
||||
|
||||
const int squareSize = m_editor->fontMetrics().height() - 4;
|
||||
const int scrollX = m_editor->horizontalScrollBar()->value();
|
||||
|
||||
QTextBlock block = firstVisible;
|
||||
while (block.isValid() && block.blockNumber() <= lastNum)
|
||||
{
|
||||
const QRectF blockRect = m_editor->blockBoundingGeometryPublic(block)
|
||||
.translated(m_editor->contentOffsetPublic());
|
||||
|
||||
const QList<ColorMatch> found = findColorsInBlock(block.text(),
|
||||
block.blockNumber());
|
||||
for (ColorMatch m : found)
|
||||
{
|
||||
// X-Position des Farbwerts im Viewport berechnen
|
||||
const QTextLayout *layout = block.layout();
|
||||
if (!layout || layout->lineCount() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const QTextLine line = layout->lineAt(0);
|
||||
// Position nach dem Ende des Farbwerts
|
||||
const qreal endCharX = line.cursorToX(m.posInBlock + m.length);
|
||||
const int x = static_cast<int>(blockRect.left() + endCharX)
|
||||
- scrollX + 3;
|
||||
|
||||
if (x + squareSize > m_editor->viewport()->width())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int y = static_cast<int>(blockRect.top())
|
||||
+ (static_cast<int>(blockRect.height()) - squareSize) / 2;
|
||||
|
||||
m.rect = QRect(x, y, squareSize, squareSize);
|
||||
m_cache.append(m);
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Farbwerte in einer Zeile suchen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<ColorIndicator::ColorMatch> ColorIndicator::findColorsInBlock(
|
||||
const QString &text, int blockNumber) const
|
||||
{
|
||||
QList<ColorMatch> result;
|
||||
|
||||
QRegularExpressionMatchIterator it = s_colorRegex.globalMatch(text);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
const QString token = match.captured(0);
|
||||
const QColor color = parseColor(token);
|
||||
|
||||
if (!color.isValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ColorMatch m;
|
||||
m.blockNumber = blockNumber;
|
||||
m.posInBlock = static_cast<int>(match.capturedStart());
|
||||
m.length = static_cast<int>(match.capturedLength());
|
||||
m.color = color;
|
||||
result.append(m);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Farb-Parser
|
||||
// ---------------------------------------------------------------------------
|
||||
QColor ColorIndicator::parseColor(const QString &token)
|
||||
{
|
||||
const QString t = token.trimmed();
|
||||
|
||||
if (t.startsWith('#')) { return parseHex(t); }
|
||||
if (t.startsWith("rgba", Qt::CaseInsensitive)) { return parseRgba(t); }
|
||||
if (t.startsWith("rgb", Qt::CaseInsensitive)) { return parseRgb(t); }
|
||||
if (t.startsWith("hsla", Qt::CaseInsensitive)) { return parseHsla(t); }
|
||||
if (t.startsWith("hsl", Qt::CaseInsensitive)) { return parseHsl(t); }
|
||||
|
||||
return QColor();
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHex(const QString &s)
|
||||
{
|
||||
// #rgb → #rrggbb
|
||||
if (s.length() == 4)
|
||||
{
|
||||
return QColor(QString("#%1%1%2%2%3%3")
|
||||
.arg(s[1]).arg(s[2]).arg(s[3]));
|
||||
}
|
||||
// #rrggbb
|
||||
if (s.length() == 7)
|
||||
{
|
||||
return QColor(s);
|
||||
}
|
||||
// #rrggbbaa (CSS) → Qt braucht #aarrggbb
|
||||
if (s.length() == 9)
|
||||
{
|
||||
const QString rr = s.mid(1, 2);
|
||||
const QString gg = s.mid(3, 2);
|
||||
const QString bb = s.mid(5, 2);
|
||||
const QString aa = s.mid(7, 2);
|
||||
return QColor(QString("#%1%2%3%4").arg(aa, rr, gg, bb));
|
||||
}
|
||||
return QColor();
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseRgb(const QString &s)
|
||||
{
|
||||
// rgb(r, g, b)
|
||||
static const QRegularExpression re(
|
||||
R"(rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
return QColor(m.captured(1).toInt(),
|
||||
m.captured(2).toInt(),
|
||||
m.captured(3).toInt());
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseRgba(const QString &s)
|
||||
{
|
||||
// rgba(r, g, b, a) — a ist 0.0–1.0
|
||||
static const QRegularExpression re(
|
||||
R"(rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9.]+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
return QColor(m.captured(1).toInt(),
|
||||
m.captured(2).toInt(),
|
||||
m.captured(3).toInt(),
|
||||
qRound(m.captured(4).toDouble() * 255.0));
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHsl(const QString &s)
|
||||
{
|
||||
// hsl(h, s%, l%)
|
||||
static const QRegularExpression re(
|
||||
R"(hsl\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
QColor c;
|
||||
c.setHsl(m.captured(1).toInt(),
|
||||
qRound(m.captured(2).toInt() * 2.55),
|
||||
qRound(m.captured(3).toInt() * 2.55));
|
||||
return c;
|
||||
}
|
||||
|
||||
QColor ColorIndicator::parseHsla(const QString &s)
|
||||
{
|
||||
// hsla(h, s%, l%, a)
|
||||
static const QRegularExpression re(
|
||||
R"(hsla\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([0-9.]+)\s*\))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch m = re.match(s);
|
||||
if (!m.hasMatch()) { return QColor(); }
|
||||
QColor c;
|
||||
c.setHsl(m.captured(1).toInt(),
|
||||
qRound(m.captured(2).toInt() * 2.55),
|
||||
qRound(m.captured(3).toInt() * 2.55),
|
||||
qRound(m.captured(4).toDouble() * 255.0));
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
|
||||
67
barecode/src/editor/ColorIndicator.h
Normal file
67
barecode/src/editor/ColorIndicator.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QColor>
|
||||
#include <QRect>
|
||||
#include <QList>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
class CodeEditor;
|
||||
class QPainter;
|
||||
class QPaintEvent;
|
||||
class QMouseEvent;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ColorIndicator – Zeichnet kleine Farbquadrate neben CSS-Farbwerten und
|
||||
// öffnet einen QColorDialog wenn der Nutzer darauf klickt.
|
||||
//
|
||||
// Unterstützte Formate:
|
||||
// #rgb #rrggbb #rrggbbaa
|
||||
// rgb(r, g, b) rgba(r, g, b, a)
|
||||
// hsl(h, s%, l%) hsla(h, s%, l%, a)
|
||||
// 140 benannte CSS-Farben (red, blue, cornflowerblue, ...)
|
||||
// ---------------------------------------------------------------------------
|
||||
class ColorIndicator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ColorIndicator(CodeEditor *editor);
|
||||
|
||||
// Wird aus CodeEditor::paintEvent aufgerufen
|
||||
void paint(QPainter &painter, QPaintEvent *event);
|
||||
|
||||
// Wird aus CodeEditor::mousePressEvent aufgerufen
|
||||
// Gibt true zurück wenn der Klick auf einem Farbquadrat war
|
||||
bool handleMousePress(QMouseEvent *event);
|
||||
|
||||
private:
|
||||
struct ColorMatch
|
||||
{
|
||||
QRect rect; // Position des Quadrats im Viewport
|
||||
QColor color; // Erkannte Farbe
|
||||
int blockNumber;
|
||||
int posInBlock; // Zeichenposition des Farbwerts im Block
|
||||
int length; // Länge des Farbwerts im Text
|
||||
};
|
||||
|
||||
void rebuildCache();
|
||||
QList<ColorMatch> findColorsInBlock(const QString &text,
|
||||
int blockNumber) const;
|
||||
|
||||
static QColor parseColor(const QString &token);
|
||||
static QColor parseHex(const QString &s);
|
||||
static QColor parseRgb(const QString &s);
|
||||
static QColor parseRgba(const QString &s);
|
||||
static QColor parseHsl(const QString &s);
|
||||
static QColor parseHsla(const QString &s);
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
QList<ColorMatch> m_cache;
|
||||
int m_cacheFirstBlock = -1;
|
||||
int m_cacheLastBlock = -1;
|
||||
|
||||
// Kombinierter Regex für alle Farbformate
|
||||
static const QRegularExpression s_colorRegex;
|
||||
};
|
||||
181
barecode/src/editor/DeadCodeAnalyzer.cpp
Normal file
181
barecode/src/editor/DeadCodeAnalyzer.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "DeadCodeAnalyzer.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QFileInfo>
|
||||
|
||||
DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
}
|
||||
|
||||
QList<DeadCodeAnalyzer::DeadFunction> DeadCodeAnalyzer::analyze(
|
||||
const QString &projectRoot,
|
||||
const QStringList &extensions,
|
||||
const QStringList &excludeDirs) const
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 1: Alle Dateien sammeln (mit Exclude-Filter)
|
||||
// -----------------------------------------------------------------------
|
||||
QStringList allFiles;
|
||||
QDirIterator it(projectRoot, extensions, QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString path = it.next();
|
||||
|
||||
// Exclude-Verzeichnisse prüfen
|
||||
bool excluded = false;
|
||||
if (!excludeDirs.isEmpty())
|
||||
{
|
||||
const QStringList parts = path.split('/');
|
||||
for (const QString &excl : excludeDirs)
|
||||
{
|
||||
if (parts.contains(excl.trimmed(), Qt::CaseInsensitive))
|
||||
{
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!excluded)
|
||||
{
|
||||
allFiles.append(path);
|
||||
}
|
||||
}
|
||||
|
||||
const int totalFiles = allFiles.size();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 2: Alle Funktionsdefinitionen aus nicht-excluded Dateien
|
||||
// -----------------------------------------------------------------------
|
||||
QHash<QString, FunctionScanner::FunctionInfo> definitions;
|
||||
|
||||
for (const QString &path : allFiles)
|
||||
{
|
||||
const QList<FunctionScanner::FunctionInfo> funcs = m_scanner->scanFile(path);
|
||||
for (const FunctionScanner::FunctionInfo &func : funcs)
|
||||
{
|
||||
if (func.name.startsWith("__"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const QString key = func.name.toLower();
|
||||
if (!definitions.contains(key))
|
||||
{
|
||||
definitions.insert(key, func);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (definitions.isEmpty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 3: Alle Dateien EINMAL lesen, alle Aufrufe sammeln
|
||||
// -----------------------------------------------------------------------
|
||||
QSet<QString> calledFunctions;
|
||||
|
||||
static const QRegularExpression identifierRegex(
|
||||
R"((?:->|::|\b)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()"
|
||||
R"(|['"]([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)['"])"
|
||||
);
|
||||
|
||||
static const QRegularExpression defLineRegex(
|
||||
R"(\bfunction\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))"
|
||||
);
|
||||
|
||||
int filesScanned = 0;
|
||||
|
||||
for (const QString &filePath : allFiles)
|
||||
{
|
||||
++filesScanned;
|
||||
|
||||
// Fortschritt signalisieren
|
||||
emit progressUpdate(filesScanned, totalFiles,
|
||||
QFileInfo(filePath).fileName());
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
const QString line = stream.readLine();
|
||||
const QString trimmed = line.trimmed();
|
||||
|
||||
if (trimmed.startsWith("//") ||
|
||||
trimmed.startsWith("*") ||
|
||||
trimmed.startsWith("#") ||
|
||||
trimmed.startsWith("/*"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Definitionen auf dieser Zeile nicht als Aufruf werten
|
||||
QSet<QString> definedOnThisLine;
|
||||
{
|
||||
QRegularExpressionMatchIterator dit = defLineRegex.globalMatch(line);
|
||||
while (dit.hasNext())
|
||||
{
|
||||
definedOnThisLine.insert(dit.next().captured(1).toLower());
|
||||
}
|
||||
}
|
||||
|
||||
QRegularExpressionMatchIterator mit = identifierRegex.globalMatch(line);
|
||||
while (mit.hasNext())
|
||||
{
|
||||
const QRegularExpressionMatch match = mit.next();
|
||||
const QString name = match.captured(1).isEmpty()
|
||||
? match.captured(2).toLower()
|
||||
: match.captured(1).toLower();
|
||||
|
||||
if (name.isEmpty() || definedOnThisLine.contains(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (definitions.contains(name))
|
||||
{
|
||||
calledFunctions.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Schritt 4: Nicht aufgerufene Funktionen zurückgeben
|
||||
// -----------------------------------------------------------------------
|
||||
QList<DeadFunction> dead;
|
||||
|
||||
for (auto it2 = definitions.begin(); it2 != definitions.end(); ++it2)
|
||||
{
|
||||
if (!calledFunctions.contains(it2.key()))
|
||||
{
|
||||
const FunctionScanner::FunctionInfo &func = it2.value();
|
||||
DeadFunction d;
|
||||
d.info = func;
|
||||
d.reason = func.className.isEmpty()
|
||||
? QObject::tr("Globale Funktion — kein Aufruf gefunden")
|
||||
: QObject::tr("Methode von %1 — kein Aufruf gefunden")
|
||||
.arg(func.className);
|
||||
dead.append(d);
|
||||
}
|
||||
}
|
||||
|
||||
return dead;
|
||||
}
|
||||
39
barecode/src/editor/DeadCodeAnalyzer.h
Normal file
39
barecode/src/editor/DeadCodeAnalyzer.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeadCodeAnalyzer – Findet Funktionen die definiert aber nie aufgerufen werden.
|
||||
// ---------------------------------------------------------------------------
|
||||
class DeadCodeAnalyzer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct DeadFunction
|
||||
{
|
||||
FunctionScanner::FunctionInfo info;
|
||||
QString reason;
|
||||
};
|
||||
|
||||
explicit DeadCodeAnalyzer(QObject *parent = nullptr);
|
||||
|
||||
// excludeDirs: Verzeichnisnamen die komplett übersprungen werden
|
||||
// z.B. {"vendor", "lib", "node_modules"}
|
||||
QList<DeadFunction> analyze(const QString &projectRoot,
|
||||
const QStringList &extensions = {"*.php"},
|
||||
const QStringList &excludeDirs = {}) const;
|
||||
|
||||
signals:
|
||||
// Fortschritt während der Analyse (läuft im Thread)
|
||||
void progressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤tFile) const;
|
||||
|
||||
private:
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
};
|
||||
461
barecode/src/editor/DeadCodeDialog.cpp
Normal file
461
barecode/src/editor/DeadCodeDialog.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
#include "DeadCodeDialog.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFont>
|
||||
#include <QSettings>
|
||||
#include <QCloseEvent>
|
||||
#include <QFileDialog>
|
||||
#include <QTextStream>
|
||||
#include <QMessageBox>
|
||||
#include <QHeaderView>
|
||||
#include <QDateTime>
|
||||
#include <QMetaObject>
|
||||
|
||||
DeadCodeDialog::DeadCodeDialog(QWidget *parent)
|
||||
: QDialog(parent, Qt::Window)
|
||||
{
|
||||
setWindowTitle(tr("Tote Funktionen – BareCode"));
|
||||
setMinimumSize(650, 500);
|
||||
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
if (s.contains("deadcode/geometry"))
|
||||
{
|
||||
restoreGeometry(s.value("deadcode/geometry").toByteArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
resize(780, 620);
|
||||
}
|
||||
|
||||
m_analyzer = new DeadCodeAnalyzer(this);
|
||||
m_watcher = new QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>::finished,
|
||||
this, &DeadCodeDialog::onAnalysisFinished);
|
||||
|
||||
// Fortschritts-Signal aus dem Analyzer-Thread sicher in den UI-Thread leiten
|
||||
connect(m_analyzer, &DeadCodeAnalyzer::progressUpdate,
|
||||
this, &DeadCodeDialog::onProgressUpdate,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setSpacing(8);
|
||||
root->setContentsMargins(12, 12, 12, 12);
|
||||
|
||||
// ---- Warnbox ----
|
||||
QFrame *warningBox = new QFrame(this);
|
||||
warningBox->setStyleSheet(
|
||||
"QFrame {"
|
||||
" background: #3a2a00;"
|
||||
" border: 1px solid #7a5a00;"
|
||||
" border-radius: 4px;"
|
||||
"}"
|
||||
);
|
||||
QHBoxLayout *warnLayout = new QHBoxLayout(warningBox);
|
||||
warnLayout->setContentsMargins(8, 6, 8, 6);
|
||||
|
||||
QLabel *warnIcon = new QLabel(warningBox);
|
||||
warnIcon->setFixedSize(20, 20);
|
||||
warnIcon->setStyleSheet(
|
||||
"background: #ffcc00;"
|
||||
"color: #1a1a00;"
|
||||
"font-weight: bold;"
|
||||
"font-size: 13px;"
|
||||
"border-radius: 10px;"
|
||||
);
|
||||
warnIcon->setText("!");
|
||||
warnIcon->setAlignment(Qt::AlignCenter);
|
||||
warnLayout->addWidget(warnIcon);
|
||||
|
||||
QLabel *warnText = new QLabel(
|
||||
tr("Kandidatenliste — kein Aufruf per Regex gefunden. "
|
||||
"Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. "
|
||||
"Bitte vor dem Löschen manuell prüfen."),
|
||||
warningBox
|
||||
);
|
||||
warnText->setWordWrap(true);
|
||||
warnText->setStyleSheet("color: #ffcc88;");
|
||||
warnLayout->addWidget(warnText, 1);
|
||||
root->addWidget(warningBox);
|
||||
|
||||
// ---- Exclude-Verzeichnisse ----
|
||||
QHBoxLayout *excludeRow = new QHBoxLayout();
|
||||
excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this));
|
||||
|
||||
m_excludeEdit = new QLineEdit(this);
|
||||
m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)"));
|
||||
m_excludeEdit->setToolTip(tr(
|
||||
"Verzeichnisnamen die bei der Analyse übersprungen werden.\n"
|
||||
"Leerzeichen-getrennt, z.B.: vendor lib node_modules cache"
|
||||
));
|
||||
|
||||
// Letzte Einstellung wiederherstellen
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
m_excludeEdit->setText(s.value("deadcode/excludeDirs", "vendor lib node_modules").toString());
|
||||
|
||||
excludeRow->addWidget(m_excludeEdit, 1);
|
||||
|
||||
// Verzeichnis-Auswahl Button
|
||||
QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this);
|
||||
btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen"));
|
||||
connect(btnBrowse, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
const QString startPath = m_projectRoot.isEmpty()
|
||||
? QDir::homePath()
|
||||
: m_projectRoot;
|
||||
|
||||
const QString dir = QFileDialog::getExistingDirectory(
|
||||
this,
|
||||
tr("Verzeichnis ausschließen"),
|
||||
startPath,
|
||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
|
||||
);
|
||||
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad
|
||||
// So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse
|
||||
const QString dirName = QFileInfo(dir).fileName();
|
||||
|
||||
// Prüfen ob bereits in der Liste
|
||||
const QStringList current = m_excludeEdit->text()
|
||||
.simplified()
|
||||
.split(' ', Qt::SkipEmptyParts);
|
||||
if (current.contains(dirName, Qt::CaseInsensitive))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Anhängen
|
||||
QString newText = m_excludeEdit->text().trimmed();
|
||||
if (!newText.isEmpty())
|
||||
{
|
||||
newText += ' ';
|
||||
}
|
||||
newText += dirName;
|
||||
m_excludeEdit->setText(newText);
|
||||
});
|
||||
excludeRow->addWidget(btnBrowse);
|
||||
|
||||
// Löschen-Button — ganzes Feld leeren
|
||||
QPushButton *btnClear = new QPushButton(tr("✕"), this);
|
||||
btnClear->setFixedWidth(28);
|
||||
btnClear->setFlat(true);
|
||||
btnClear->setToolTip(tr("Ausschlussliste leeren"));
|
||||
connect(btnClear, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
m_excludeEdit->clear();
|
||||
});
|
||||
excludeRow->addWidget(btnClear);
|
||||
|
||||
root->addLayout(excludeRow);
|
||||
|
||||
// ---- Steuerleiste ----
|
||||
QHBoxLayout *ctrlRow = new QHBoxLayout();
|
||||
|
||||
m_btnAnalyze = new QPushButton(tr("Analyse starten"), this);
|
||||
ctrlRow->addWidget(m_btnAnalyze);
|
||||
|
||||
m_btnExport = new QPushButton(tr("Als TXT exportieren"), this);
|
||||
m_btnExport->setEnabled(false);
|
||||
ctrlRow->addWidget(m_btnExport);
|
||||
|
||||
ctrlRow->addStretch();
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
ctrlRow->addWidget(m_statusLabel);
|
||||
|
||||
root->addLayout(ctrlRow);
|
||||
|
||||
// ---- Fortschrittsbereich ----
|
||||
m_progress = new QProgressBar(this);
|
||||
m_progress->setRange(0, 100);
|
||||
m_progress->setTextVisible(true);
|
||||
m_progress->setFormat("%v / %m Dateien");
|
||||
m_progress->hide();
|
||||
root->addWidget(m_progress);
|
||||
|
||||
m_progressLabel = new QLabel(this);
|
||||
m_progressLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
|
||||
m_progressLabel->hide();
|
||||
root->addWidget(m_progressLabel);
|
||||
|
||||
// ---- Ergebnistabelle ----
|
||||
m_results = new QTreeWidget(this);
|
||||
m_results->setColumnCount(4);
|
||||
m_results->setHeaderLabels({
|
||||
tr("Funktion"),
|
||||
tr("Klasse"),
|
||||
tr("Datei"),
|
||||
tr("Zeile")
|
||||
});
|
||||
m_results->setRootIsDecorated(false);
|
||||
m_results->setAlternatingRowColors(true);
|
||||
m_results->setSortingEnabled(true);
|
||||
m_results->sortByColumn(0, Qt::AscendingOrder);
|
||||
m_results->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
|
||||
m_results->header()->setSectionResizeMode(0, QHeaderView::Interactive);
|
||||
m_results->header()->setSectionResizeMode(1, QHeaderView::Interactive);
|
||||
m_results->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||
m_results->header()->setSectionResizeMode(3, QHeaderView::Fixed);
|
||||
m_results->header()->resizeSection(0, 200);
|
||||
m_results->header()->resizeSection(1, 120);
|
||||
m_results->header()->resizeSection(3, 60);
|
||||
|
||||
QFont mono("Monospace");
|
||||
mono.setStyleHint(QFont::Monospace);
|
||||
m_results->setFont(mono);
|
||||
|
||||
root->addWidget(m_results, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnAnalyze, &QPushButton::clicked, this, &DeadCodeDialog::onAnalyze);
|
||||
connect(m_btnExport, &QPushButton::clicked, this, &DeadCodeDialog::onExportClicked);
|
||||
connect(m_results, &QTreeWidget::itemActivated,
|
||||
this, &DeadCodeDialog::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::setProjectRoot(const QString &path)
|
||||
{
|
||||
// Alte Exclude-Liste für das vorherige Projekt speichern
|
||||
if (!m_projectRoot.isEmpty())
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString oldKey = "deadcode/exclude/" +
|
||||
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
|
||||
s.setValue(oldKey, m_excludeEdit->text());
|
||||
}
|
||||
|
||||
m_projectRoot = path;
|
||||
m_results->clear();
|
||||
m_statusLabel->setText(QString());
|
||||
m_btnExport->setEnabled(false);
|
||||
|
||||
// Exclude-Liste für das neue Projekt laden
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString key = "deadcode/exclude/" +
|
||||
QString(path).replace('/', '_').replace('\\', '_');
|
||||
|
||||
// Projektspezifisch vorhanden? Sonst globalen Fallback nehmen
|
||||
const QString saved = s.value(key,
|
||||
s.value("deadcode/excludeDirs", "vendor lib node_modules").toString()
|
||||
).toString();
|
||||
|
||||
m_excludeEdit->setText(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analyse starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onAnalyze()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
QMessageBox::information(this, tr("Analyse"),
|
||||
tr("Bitte zuerst ein Projekt öffnen."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
// Exclude-Liste projektspezifisch speichern
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
const QString key = "deadcode/exclude/" +
|
||||
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
|
||||
s.setValue(key, m_excludeEdit->text());
|
||||
s.setValue("deadcode/excludeDirs", m_excludeEdit->text()); // globaler Fallback
|
||||
|
||||
const QStringList excludeDirs = m_excludeEdit->text()
|
||||
.simplified()
|
||||
.split(' ', Qt::SkipEmptyParts);
|
||||
|
||||
m_results->clear();
|
||||
m_btnAnalyze->setEnabled(false);
|
||||
m_btnExport->setEnabled(false);
|
||||
m_progress->setValue(0);
|
||||
m_progress->setMaximum(0); // Unbestimmter Modus bis wir die Dateizahl kennen
|
||||
m_progress->show();
|
||||
m_progressLabel->setText(tr("Dateien werden gezählt…"));
|
||||
m_progressLabel->show();
|
||||
m_statusLabel->setText(tr("Analysiere…"));
|
||||
|
||||
const QString root = m_projectRoot;
|
||||
DeadCodeAnalyzer *analyzer = m_analyzer;
|
||||
|
||||
QFuture<QList<DeadCodeAnalyzer::DeadFunction>> future =
|
||||
QtConcurrent::run([analyzer, root, excludeDirs]()
|
||||
{
|
||||
return analyzer->analyze(root, {"*.php"}, excludeDirs);
|
||||
});
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fortschritt aktualisieren (QueuedConnection — Thread-sicher)
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onProgressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤tFile)
|
||||
{
|
||||
if (m_progress->maximum() != filesTotal)
|
||||
{
|
||||
m_progress->setMaximum(filesTotal);
|
||||
}
|
||||
m_progress->setValue(filesScanned);
|
||||
m_progressLabel->setText(tr("(%1 / %2) %3")
|
||||
.arg(filesScanned)
|
||||
.arg(filesTotal)
|
||||
.arg(currentFile));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analyse abgeschlossen
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onAnalysisFinished()
|
||||
{
|
||||
m_progress->hide();
|
||||
m_progressLabel->hide();
|
||||
m_btnAnalyze->setEnabled(true);
|
||||
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<DeadCodeAnalyzer::DeadFunction> dead = m_watcher->result();
|
||||
|
||||
m_results->setSortingEnabled(false);
|
||||
|
||||
for (const DeadCodeAnalyzer::DeadFunction &d : dead)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(m_results);
|
||||
item->setText(0, d.info.signature);
|
||||
item->setText(1, d.info.className.isEmpty() ? tr("(global)") : d.info.className);
|
||||
item->setText(2, QFileInfo(d.info.filePath).fileName());
|
||||
item->setText(3, QString::number(d.info.line));
|
||||
item->setToolTip(0, d.reason);
|
||||
item->setToolTip(2, d.info.filePath);
|
||||
item->setData(0, Qt::UserRole, d.info.filePath);
|
||||
item->setData(0, Qt::UserRole + 1, d.info.line);
|
||||
|
||||
if (d.info.className.isEmpty())
|
||||
{
|
||||
item->setForeground(1, QColor("#888888"));
|
||||
}
|
||||
}
|
||||
|
||||
m_results->setSortingEnabled(true);
|
||||
m_results->sortByColumn(0, Qt::AscendingOrder);
|
||||
|
||||
if (dead.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("✓ Keine ungenutzten Funktionen gefunden."));
|
||||
m_statusLabel->setStyleSheet("color: #44bb44;");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Kandidaten gefunden").arg(dead.size()));
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
m_btnExport->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick auf Treffer
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onItemActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
if (!path.isEmpty() && line > 0)
|
||||
{
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::onExportClicked()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Ergebnis exportieren"),
|
||||
QString("barecode_deadcode_%1.txt")
|
||||
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmm")),
|
||||
tr("Textdateien (*.txt)")
|
||||
);
|
||||
if (path.isEmpty()) { return; }
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Export"),
|
||||
tr("Datei konnte nicht geschrieben werden:\n%1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
out.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
out << "BareCode – Analyse ungenutzter Funktionen\n";
|
||||
out << "Projekt: " << m_projectRoot << "\n";
|
||||
out << "Ausgeschlossen: " << m_excludeEdit->text() << "\n";
|
||||
out << "Datum: " << QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm") << "\n";
|
||||
out << QString("─").repeated(70) << "\n\n";
|
||||
|
||||
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
||||
{
|
||||
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
||||
out << item->text(0) << "\n";
|
||||
out << " Klasse: " << item->text(1) << "\n";
|
||||
out << " Datei: " << item->toolTip(2) << "\n";
|
||||
out << " Zeile: " << item->text(3) << "\n\n";
|
||||
}
|
||||
|
||||
out << QString("─").repeated(70) << "\n";
|
||||
out << m_results->topLevelItemCount() << " Kandidaten\n";
|
||||
out << "Hinweis: Dynamische Aufrufe werden nicht erkannt.\n";
|
||||
|
||||
QMessageBox::information(this, tr("Export"),
|
||||
tr("Exportiert nach:\n%1").arg(path));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fenstergeometrie speichern
|
||||
// ---------------------------------------------------------------------------
|
||||
void DeadCodeDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
}
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("deadcode/geometry", saveGeometry());
|
||||
event->accept();
|
||||
}
|
||||
62
barecode/src/editor/DeadCodeDialog.h
Normal file
62
barecode/src/editor/DeadCodeDialog.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
|
||||
#include "DeadCodeAnalyzer.h"
|
||||
|
||||
class DeadCodeDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeadCodeDialog(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onAnalyze();
|
||||
void onAnalysisFinished();
|
||||
void onProgressUpdate(int filesScanned, int filesTotal,
|
||||
const QString ¤tFile);
|
||||
void onItemActivated(QTreeWidgetItem *item, int column);
|
||||
void onExportClicked();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
QString m_projectRoot;
|
||||
|
||||
// Einstellungen
|
||||
QLineEdit *m_excludeEdit = nullptr;
|
||||
|
||||
// Steuerung
|
||||
QPushButton *m_btnAnalyze = nullptr;
|
||||
QPushButton *m_btnExport = nullptr;
|
||||
|
||||
// Fortschritt
|
||||
QProgressBar *m_progress = nullptr;
|
||||
QLabel *m_progressLabel = nullptr;
|
||||
|
||||
// Status + Ergebnisse
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QTreeWidget *m_results = nullptr;
|
||||
|
||||
DeadCodeAnalyzer *m_analyzer = nullptr;
|
||||
QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>> *m_watcher = nullptr;
|
||||
};
|
||||
301
barecode/src/editor/EditorPanel.cpp
Normal file
301
barecode/src/editor/EditorPanel.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "EditorPanel.h"
|
||||
#include "EditorTab.h"
|
||||
#include "CodeEditor.h"
|
||||
#include "SearchPanel.h"
|
||||
#include "FileSearchPanel.h"
|
||||
#include "FunctionListDialog.h"
|
||||
#include "DeadCodeDialog.h"
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
#include <QTextDocument>
|
||||
#include <QTextBlock>
|
||||
|
||||
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_settings(settings)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::setupUi()
|
||||
{
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(0);
|
||||
|
||||
m_tabWidget = new QTabWidget(this);
|
||||
m_tabWidget->setTabsClosable(true);
|
||||
m_tabWidget->setMovable(true);
|
||||
m_tabWidget->setDocumentMode(true);
|
||||
|
||||
m_searchPanel = new SearchPanel(this);
|
||||
m_fileSearch = new FileSearchPanel(this);
|
||||
m_funcDialog = new FunctionListDialog(window());
|
||||
m_deadCode = new DeadCodeDialog(window());
|
||||
m_funcIndex = new FunctionIndex(this);
|
||||
|
||||
m_layout->addWidget(m_tabWidget, 1);
|
||||
m_layout->addWidget(m_searchPanel, 0);
|
||||
m_layout->addWidget(m_fileSearch, 0);
|
||||
|
||||
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
|
||||
this, &EditorPanel::onTabCloseRequested);
|
||||
|
||||
connect(m_tabWidget, &QTabWidget::currentChanged,
|
||||
this, &EditorPanel::onCurrentTabChanged);
|
||||
|
||||
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_funcDialog, &FunctionListDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_deadCode, &DeadCodeDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hilfsmethoden
|
||||
// ---------------------------------------------------------------------------
|
||||
EditorTab *EditorPanel::currentTab() const
|
||||
{
|
||||
return qobject_cast<EditorTab *>(m_tabWidget->currentWidget());
|
||||
}
|
||||
|
||||
int EditorPanel::findTabForFile(const QString &filePath) const
|
||||
{
|
||||
EditorTab *tab = m_openTabs.value(filePath, nullptr);
|
||||
return tab ? m_tabWidget->indexOf(tab) : -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::openFile(const QString &filePath)
|
||||
{
|
||||
const int existing = findTabForFile(filePath);
|
||||
if (existing != -1)
|
||||
{
|
||||
m_tabWidget->setCurrentIndex(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget);
|
||||
const int index = m_tabWidget->addTab(tab, tab->fileName());
|
||||
m_tabWidget->setCurrentIndex(index);
|
||||
m_tabWidget->setTabToolTip(index, filePath);
|
||||
m_openTabs.insert(filePath, tab);
|
||||
|
||||
// FunctionIndex dem Editor mitgeben für Doppelklick-Navigation
|
||||
tab->editor()->setFunctionIndex(m_funcIndex);
|
||||
|
||||
// Doppelklick auf Funktion → zur Definition springen
|
||||
connect(tab->editor(), &CodeEditor::navigateToRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
// Änderungsindikator im Tab-Titel (● = ungespeichert)
|
||||
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
|
||||
this, [this, tab](bool modified)
|
||||
{
|
||||
const int idx = m_tabWidget->indexOf(tab);
|
||||
if (idx == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const QString name = QFileInfo(tab->filePath()).fileName();
|
||||
m_tabWidget->setTabText(idx, modified ? "● " + name : name);
|
||||
});
|
||||
|
||||
// Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt)
|
||||
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
|
||||
{
|
||||
const int idx = m_tabWidget->indexOf(tab);
|
||||
if (idx != -1)
|
||||
{
|
||||
m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName());
|
||||
m_tabWidget->setTabToolTip(idx, savedPath);
|
||||
}
|
||||
emit currentFileSaved(savedPath);
|
||||
|
||||
// Index und Funktionsliste aktualisieren
|
||||
m_funcIndex->refresh();
|
||||
if (m_funcDialog->isVisible())
|
||||
{
|
||||
m_funcDialog->refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EditorPanel::saveCurrentFile()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->save();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::saveCurrentFileAs()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->saveAs();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::saveAllFiles()
|
||||
{
|
||||
for (int i = 0; i < m_tabWidget->count(); ++i)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
|
||||
if (tab && tab->isModified())
|
||||
{
|
||||
tab->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::showSearchPanel()
|
||||
{
|
||||
m_fileSearch->hide();
|
||||
m_searchPanel->activate();
|
||||
}
|
||||
|
||||
void EditorPanel::showFileSearchPanel()
|
||||
{
|
||||
m_searchPanel->hide();
|
||||
m_fileSearch->activate();
|
||||
}
|
||||
|
||||
void EditorPanel::showFunctionList()
|
||||
{
|
||||
m_funcDialog->show();
|
||||
m_funcDialog->raise();
|
||||
m_funcDialog->activateWindow();
|
||||
}
|
||||
|
||||
void EditorPanel::showDeadCode()
|
||||
{
|
||||
m_deadCode->show();
|
||||
m_deadCode->raise();
|
||||
m_deadCode->activateWindow();
|
||||
}
|
||||
|
||||
void EditorPanel::setSearchRoot(const QString &path)
|
||||
{
|
||||
m_fileSearch->setSearchRoot(path);
|
||||
m_funcDialog->setProjectRoot(path);
|
||||
m_deadCode->setProjectRoot(path);
|
||||
m_funcIndex->setProjectRoot(path);
|
||||
}
|
||||
|
||||
void EditorPanel::goToLine(const QString &filePath, int line)
|
||||
{
|
||||
// Datei öffnen falls noch nicht geöffnet
|
||||
openFile(filePath);
|
||||
|
||||
EditorTab *tab = m_openTabs.value(filePath, nullptr);
|
||||
if (!tab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_tabWidget->setCurrentWidget(tab);
|
||||
|
||||
// Zur gewünschten Zeile springen
|
||||
CodeEditor *editor = tab->editor();
|
||||
QTextBlock block = editor->document()->findBlockByLineNumber(line - 1);
|
||||
if (block.isValid())
|
||||
{
|
||||
QTextCursor cursor(block);
|
||||
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||
editor->setTextCursor(cursor);
|
||||
editor->centerCursor();
|
||||
editor->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList EditorPanel::openFilePaths() const
|
||||
{
|
||||
QStringList paths;
|
||||
for (int i = 0; i < m_tabWidget->count(); ++i)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
|
||||
if (tab)
|
||||
{
|
||||
paths.append(tab->filePath());
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
QString EditorPanel::activeFilePath() const
|
||||
{
|
||||
EditorTab *tab = currentTab();
|
||||
return tab ? tab->filePath() : QString();
|
||||
}
|
||||
|
||||
void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile)
|
||||
{
|
||||
for (const QString &path : files)
|
||||
{
|
||||
if (QFile::exists(path))
|
||||
{
|
||||
openFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Aktiven Tab wiederherstellen
|
||||
if (!activeFile.isEmpty())
|
||||
{
|
||||
const int idx = findTabForFile(activeFile);
|
||||
if (idx != -1)
|
||||
{
|
||||
m_tabWidget->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::undo()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->editor()->undo();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorPanel::redo()
|
||||
{
|
||||
if (EditorTab *tab = currentTab())
|
||||
{
|
||||
tab->editor()->redo();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void EditorPanel::onTabCloseRequested(int index)
|
||||
{
|
||||
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(index));
|
||||
if (!tab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_openTabs.remove(tab->filePath());
|
||||
m_tabWidget->removeTab(index);
|
||||
tab->deleteLater();
|
||||
|
||||
m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr);
|
||||
}
|
||||
|
||||
void EditorPanel::onCurrentTabChanged(int /*index*/)
|
||||
{
|
||||
EditorTab *tab = currentTab();
|
||||
m_searchPanel->setEditor(tab ? tab->editor() : nullptr);
|
||||
}
|
||||
68
barecode/src/editor/EditorPanel.h
Normal file
68
barecode/src/editor/EditorPanel.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
class EditorTab;
|
||||
class Settings;
|
||||
class SearchPanel;
|
||||
class FileSearchPanel;
|
||||
class FunctionListDialog;
|
||||
class DeadCodeDialog;
|
||||
class FunctionIndex;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
||||
// ---------------------------------------------------------------------------
|
||||
class EditorPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EditorPanel(Settings *settings, QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void openFile(const QString &filePath);
|
||||
void saveCurrentFile();
|
||||
void saveCurrentFileAs();
|
||||
void saveAllFiles();
|
||||
void setSearchRoot(const QString &path);
|
||||
void showSearchPanel();
|
||||
void showFileSearchPanel();
|
||||
void showFunctionList();
|
||||
void showDeadCode();
|
||||
void goToLine(const QString &filePath, int line);
|
||||
|
||||
// Session
|
||||
QStringList openFilePaths() const;
|
||||
QString activeFilePath() const;
|
||||
void restoreSession(const QStringList &files, const QString &activeFile);
|
||||
void undo();
|
||||
void redo();
|
||||
|
||||
signals:
|
||||
void currentFileSaved(const QString &filePath);
|
||||
|
||||
private slots:
|
||||
void onTabCloseRequested(int index);
|
||||
void onCurrentTabChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
int findTabForFile(const QString &filePath) const;
|
||||
EditorTab *currentTab() const;
|
||||
|
||||
Settings *m_settings = nullptr;
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QTabWidget *m_tabWidget = nullptr;
|
||||
SearchPanel *m_searchPanel = nullptr;
|
||||
FileSearchPanel *m_fileSearch = nullptr;
|
||||
FunctionListDialog *m_funcDialog = nullptr;
|
||||
DeadCodeDialog *m_deadCode = nullptr;
|
||||
FunctionIndex *m_funcIndex = nullptr;
|
||||
|
||||
QHash<QString, EditorTab *> m_openTabs;
|
||||
};
|
||||
53
barecode/src/editor/EditorTab.cpp
Normal file
53
barecode/src/editor/EditorTab.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "EditorTab.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_filePath(filePath)
|
||||
{
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(0);
|
||||
|
||||
m_editor = new CodeEditor(settings, this);
|
||||
m_editor->loadFile(filePath);
|
||||
|
||||
m_layout->addWidget(m_editor);
|
||||
}
|
||||
|
||||
QString EditorTab::filePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
QString EditorTab::fileName() const
|
||||
{
|
||||
return QFileInfo(m_filePath).fileName();
|
||||
}
|
||||
|
||||
CodeEditor *EditorTab::editor() const
|
||||
{
|
||||
return m_editor;
|
||||
}
|
||||
|
||||
bool EditorTab::isModified() const
|
||||
{
|
||||
return m_editor->isModified();
|
||||
}
|
||||
|
||||
bool EditorTab::save()
|
||||
{
|
||||
const bool ok = m_editor->save();
|
||||
// Path may have changed if this was an untitled buffer saved for the first time
|
||||
m_filePath = m_editor->filePath();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool EditorTab::saveAs()
|
||||
{
|
||||
const bool ok = m_editor->saveAs();
|
||||
m_filePath = m_editor->filePath();
|
||||
return ok;
|
||||
}
|
||||
33
barecode/src/editor/EditorTab.h
Normal file
33
barecode/src/editor/EditorTab.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class CodeEditor;
|
||||
class Settings;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorTab – Widget placed inside each tab of the tab bar.
|
||||
// Owns a CodeEditor for a single file.
|
||||
// ---------------------------------------------------------------------------
|
||||
class EditorTab : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr);
|
||||
|
||||
QString filePath() const;
|
||||
QString fileName() const;
|
||||
CodeEditor *editor() const;
|
||||
bool isModified() const;
|
||||
|
||||
bool save();
|
||||
bool saveAs();
|
||||
|
||||
private:
|
||||
QString m_filePath;
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
CodeEditor *m_editor = nullptr;
|
||||
};
|
||||
330
barecode/src/editor/FileSearchPanel.cpp
Normal file
330
barecode/src/editor/FileSearchPanel.cpp
Normal file
@@ -0,0 +1,330 @@
|
||||
#include "FileSearchPanel.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QKeyEvent>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFuture>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// ---------------------------------------------------------------------------
|
||||
FileSearchPanel::FileSearchPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
hide();
|
||||
|
||||
m_watcher = new QFutureWatcher<QList<Match>>(this);
|
||||
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
|
||||
this, &FileSearchPanel::onSearchFinished);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(6, 4, 6, 4);
|
||||
root->setSpacing(4);
|
||||
|
||||
// ---- Zeile 1: Suchbegriff ----
|
||||
QHBoxLayout *row1 = new QHBoxLayout();
|
||||
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
|
||||
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
|
||||
m_searchEdit->setClearButtonEnabled(true);
|
||||
row1->addWidget(m_searchEdit, 1);
|
||||
|
||||
m_btnSearch = new QPushButton(tr("Suchen"), this);
|
||||
m_btnSearch->setDefault(true);
|
||||
row1->addWidget(m_btnSearch);
|
||||
|
||||
m_btnClose = new QPushButton(tr("✕"), this);
|
||||
m_btnClose->setFixedWidth(24);
|
||||
m_btnClose->setFlat(true);
|
||||
m_btnClose->setToolTip(tr("Schließen"));
|
||||
row1->addWidget(m_btnClose);
|
||||
|
||||
root->addLayout(row1);
|
||||
|
||||
// ---- Zeile 2: Optionen + Filter ----
|
||||
QHBoxLayout *row2 = new QHBoxLayout();
|
||||
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
||||
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
||||
m_chkRegex = new QCheckBox(tr("Regex"), this);
|
||||
|
||||
row2->addWidget(m_chkCase);
|
||||
row2->addWidget(m_chkWord);
|
||||
row2->addWidget(m_chkRegex);
|
||||
row2->addSpacing(12);
|
||||
|
||||
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
|
||||
m_filterEdit = new QLineEdit(this);
|
||||
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
|
||||
m_filterEdit->setFixedWidth(220);
|
||||
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
|
||||
row2->addWidget(m_filterEdit);
|
||||
row2->addStretch();
|
||||
|
||||
root->addLayout(row2);
|
||||
|
||||
// ---- Fortschritt + Status ----
|
||||
m_progress = new QProgressBar(this);
|
||||
m_progress->setRange(0, 0); // Unbestimmter Modus
|
||||
m_progress->setFixedHeight(4);
|
||||
m_progress->hide();
|
||||
root->addWidget(m_progress);
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid);");
|
||||
root->addWidget(m_statusLabel);
|
||||
|
||||
// ---- Ergebnisliste ----
|
||||
m_results = new QTreeWidget(this);
|
||||
m_results->setHeaderHidden(true);
|
||||
m_results->setRootIsDecorated(true);
|
||||
m_results->setIndentation(16);
|
||||
m_results->setUniformRowHeights(true);
|
||||
m_results->setAlternatingRowColors(true);
|
||||
root->addWidget(m_results, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
|
||||
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
|
||||
connect(m_btnClose, &QPushButton::clicked, this, [this]()
|
||||
{
|
||||
hide();
|
||||
});
|
||||
connect(m_results, &QTreeWidget::itemActivated,
|
||||
this, &FileSearchPanel::onResultActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::setSearchRoot(const QString &path)
|
||||
{
|
||||
m_searchRoot = path;
|
||||
}
|
||||
|
||||
void FileSearchPanel::activate()
|
||||
{
|
||||
show();
|
||||
m_searchEdit->setFocus();
|
||||
m_searchEdit->selectAll();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Suche starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onSearch()
|
||||
{
|
||||
const QString needle = m_searchEdit->text().trimmed();
|
||||
if (needle.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_searchRoot.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Laufende Suche abbrechen
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
m_results->clear();
|
||||
m_statusLabel->setText(tr("Suche läuft…"));
|
||||
m_progress->show();
|
||||
m_btnSearch->setEnabled(false);
|
||||
|
||||
const QString root = m_searchRoot;
|
||||
const bool cs = m_chkCase->isChecked();
|
||||
const bool word = m_chkWord->isChecked();
|
||||
const bool regex = m_chkRegex->isChecked();
|
||||
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
|
||||
Qt::SkipEmptyParts);
|
||||
|
||||
QFuture<QList<Match>> future = QtConcurrent::run(
|
||||
[this, root, needle, cs, word, regex, extensions]()
|
||||
{
|
||||
return searchInFiles(root, needle, cs, word, regex, extensions);
|
||||
}
|
||||
);
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Suchergebnisse anzeigen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onSearchFinished()
|
||||
{
|
||||
m_progress->hide();
|
||||
m_btnSearch->setEnabled(true);
|
||||
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Match> matches = m_watcher->result();
|
||||
|
||||
// Ergebnisse gruppiert nach Datei aufbauen
|
||||
QString currentFile;
|
||||
QTreeWidgetItem *fileItem = nullptr;
|
||||
int fileCount = 0;
|
||||
int matchCount = 0;
|
||||
|
||||
for (const Match &m : matches)
|
||||
{
|
||||
if (m.filePath != currentFile)
|
||||
{
|
||||
currentFile = m.filePath;
|
||||
++fileCount;
|
||||
|
||||
fileItem = new QTreeWidgetItem(m_results);
|
||||
fileItem->setText(0, QFileInfo(m.filePath).fileName());
|
||||
fileItem->setToolTip(0, m.filePath);
|
||||
fileItem->setData(0, Qt::UserRole, m.filePath);
|
||||
fileItem->setData(0, Qt::UserRole + 1, -1);
|
||||
|
||||
QFont boldFont = fileItem->font(0);
|
||||
boldFont.setBold(true);
|
||||
fileItem->setFont(0, boldFont);
|
||||
fileItem->setExpanded(true);
|
||||
}
|
||||
|
||||
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
|
||||
lineItem->setText(0, QString(" Zeile %1: %2")
|
||||
.arg(m.line)
|
||||
.arg(m.content.trimmed().left(120)));
|
||||
lineItem->setToolTip(0, m.content.trimmed());
|
||||
lineItem->setData(0, Qt::UserRole, m.filePath);
|
||||
lineItem->setData(0, Qt::UserRole + 1, m.line);
|
||||
|
||||
++matchCount;
|
||||
}
|
||||
|
||||
// Datei-Titelzeilen um Trefferanzahl ergänzen
|
||||
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
|
||||
{
|
||||
QTreeWidgetItem *item = m_results->topLevelItem(i);
|
||||
const int count = item->childCount();
|
||||
item->setText(0, QString("%1 (%2 Treffer)")
|
||||
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
|
||||
.arg(count));
|
||||
}
|
||||
|
||||
if (matchCount == 0)
|
||||
{
|
||||
m_statusLabel->setText(tr("Keine Treffer gefunden."));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
|
||||
.arg(matchCount)
|
||||
.arg(fileCount));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick auf Treffer → Datei + Zeile öffnen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
|
||||
if (path.isEmpty() || line < 0)
|
||||
{
|
||||
// Datei-Titelzeile: nur auf-/zuklappen
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Eigentliche Suchroutine (läuft in Thread-Pool)
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
|
||||
const QString &root,
|
||||
const QString &needle,
|
||||
bool caseSensitive,
|
||||
bool wholeWord,
|
||||
bool useRegex,
|
||||
const QStringList &extensions) const
|
||||
{
|
||||
QList<Match> results;
|
||||
|
||||
// Regulären Ausdruck vorbereiten
|
||||
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
|
||||
if (wholeWord)
|
||||
{
|
||||
pattern = "\\b" + pattern + "\\b";
|
||||
}
|
||||
|
||||
QRegularExpression re(pattern,
|
||||
caseSensitive
|
||||
? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
if (!re.isValid())
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
// Verzeichnis rekursiv durchsuchen
|
||||
QDirIterator it(root,
|
||||
extensions.isEmpty()
|
||||
? QStringList("*")
|
||||
: extensions,
|
||||
QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const QString filePath = it.next();
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
int lineNumber = 0;
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
++lineNumber;
|
||||
const QString line = stream.readLine();
|
||||
|
||||
if (re.match(line).hasMatch())
|
||||
{
|
||||
results.append({ filePath, lineNumber, line });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
77
barecode/src/editor/FileSearchPanel.h
Normal file
77
barecode/src/editor/FileSearchPanel.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FileSearchPanel – Suche in allen Dateien eines Verzeichnisses.
|
||||
//
|
||||
// Ergebnisse werden als aufklappbare Liste angezeigt:
|
||||
// Dateiname (N Treffer)
|
||||
// └ Zeile 12: <Zeileninhalt>
|
||||
// └ Zeile 34: <Zeileninhalt>
|
||||
//
|
||||
// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FileSearchPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileSearchPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setSearchRoot(const QString &path);
|
||||
|
||||
public slots:
|
||||
void activate();
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onSearch();
|
||||
void onResultActivated(QTreeWidgetItem *item, int column);
|
||||
void onSearchFinished();
|
||||
|
||||
private:
|
||||
struct Match
|
||||
{
|
||||
QString filePath;
|
||||
int line;
|
||||
QString content;
|
||||
};
|
||||
|
||||
void setupUi();
|
||||
QList<Match> searchInFiles(const QString &root,
|
||||
const QString &needle,
|
||||
bool caseSensitive,
|
||||
bool wholeWord,
|
||||
bool useRegex,
|
||||
const QStringList &extensions) const;
|
||||
|
||||
QString m_searchRoot;
|
||||
|
||||
QLineEdit *m_searchEdit = nullptr;
|
||||
QCheckBox *m_chkCase = nullptr;
|
||||
QCheckBox *m_chkWord = nullptr;
|
||||
QCheckBox *m_chkRegex = nullptr;
|
||||
QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter
|
||||
QPushButton *m_btnSearch = nullptr;
|
||||
QPushButton *m_btnClose = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QTreeWidget *m_results = nullptr;
|
||||
QProgressBar *m_progress = nullptr;
|
||||
|
||||
QFutureWatcher<QList<Match>> *m_watcher = nullptr;
|
||||
};
|
||||
83
barecode/src/editor/FunctionIndex.cpp
Normal file
83
barecode/src/editor/FunctionIndex.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
FunctionIndex::FunctionIndex(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
|
||||
this, &FunctionIndex::onScanFinished);
|
||||
}
|
||||
|
||||
void FunctionIndex::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_projectRoot = path;
|
||||
m_ready = false;
|
||||
m_index.clear();
|
||||
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionIndex::refresh()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
FunctionScanner *scanner = m_scanner;
|
||||
const QString root = m_projectRoot;
|
||||
|
||||
m_watcher->setFuture(
|
||||
QtConcurrent::run([scanner, root]()
|
||||
{
|
||||
return scanner->scanDirectory(root);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
void FunctionIndex::onScanFinished()
|
||||
{
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_index.clear();
|
||||
const QList<FunctionScanner::FunctionInfo> all = m_watcher->result();
|
||||
|
||||
for (const FunctionScanner::FunctionInfo &func : all)
|
||||
{
|
||||
const QString key = func.name.toLower();
|
||||
// Ersten Eintrag bevorzugen (globale Funktionen vor Methoden)
|
||||
if (!m_index.contains(key) || func.className.isEmpty())
|
||||
{
|
||||
m_index.insert(key, func);
|
||||
}
|
||||
}
|
||||
|
||||
m_ready = true;
|
||||
emit indexReady();
|
||||
}
|
||||
|
||||
FunctionScanner::FunctionInfo FunctionIndex::lookup(const QString &name) const
|
||||
{
|
||||
return m_index.value(name.toLower(), FunctionScanner::FunctionInfo{});
|
||||
}
|
||||
|
||||
bool FunctionIndex::isReady() const
|
||||
{
|
||||
return m_ready;
|
||||
}
|
||||
47
barecode/src/editor/FunctionIndex.h
Normal file
47
barecode/src/editor/FunctionIndex.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionIndex – Singleton-artiger Index aller Projektfunktionen.
|
||||
// Wird von CodeEditor (Doppelklick-Navigation) und SignatureHelper genutzt.
|
||||
// Aktualisiert sich asynchron wenn setProjectRoot() aufgerufen wird.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionIndex : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionIndex(QObject *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
void refresh();
|
||||
|
||||
// Sucht eine Funktion nach Name (Groß-/Kleinschreibung ignoriert)
|
||||
// Gibt ungültige FunctionInfo zurück wenn nicht gefunden (filePath ist leer)
|
||||
FunctionScanner::FunctionInfo lookup(const QString &name) const;
|
||||
|
||||
bool isReady() const;
|
||||
|
||||
signals:
|
||||
void indexReady();
|
||||
|
||||
private slots:
|
||||
void onScanFinished();
|
||||
|
||||
private:
|
||||
QString m_projectRoot;
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
|
||||
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
|
||||
|
||||
// name.toLower() → FunctionInfo
|
||||
QHash<QString, FunctionScanner::FunctionInfo> m_index;
|
||||
bool m_ready = false;
|
||||
};
|
||||
52
barecode/src/editor/FunctionListDialog.cpp
Normal file
52
barecode/src/editor/FunctionListDialog.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "FunctionListDialog.h"
|
||||
#include "FunctionListPanel.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QSettings>
|
||||
#include <QCloseEvent>
|
||||
|
||||
FunctionListDialog::FunctionListDialog(QWidget *parent)
|
||||
: QDialog(parent, Qt::Window)
|
||||
{
|
||||
setWindowTitle(tr("Projektfunktionen – BareCode"));
|
||||
setMinimumSize(500, 400);
|
||||
|
||||
// Fenstergröße und -position wiederherstellen
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
if (s.contains("funclist/geometry"))
|
||||
{
|
||||
restoreGeometry(s.value("funclist/geometry").toByteArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
resize(600, 700);
|
||||
}
|
||||
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_panel = new FunctionListPanel(this);
|
||||
m_panel->show(); // Immer sichtbar — Panel ist jetzt der gesamte Dialog-Inhalt
|
||||
layout->addWidget(m_panel);
|
||||
|
||||
connect(m_panel, &FunctionListPanel::fileLineRequested,
|
||||
this, &FunctionListDialog::fileLineRequested);
|
||||
}
|
||||
|
||||
void FunctionListDialog::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_panel->setProjectRoot(path);
|
||||
}
|
||||
|
||||
void FunctionListDialog::refresh()
|
||||
{
|
||||
m_panel->refresh();
|
||||
}
|
||||
|
||||
void FunctionListDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
|
||||
s.setValue("funclist/geometry", saveGeometry());
|
||||
event->accept();
|
||||
}
|
||||
30
barecode/src/editor/FunctionListDialog.h
Normal file
30
barecode/src/editor/FunctionListDialog.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class FunctionListPanel;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionListDialog – Eigenständiges Fenster für die Projektfunktionen.
|
||||
// Bleibt offen während man im Editor arbeitet (non-modal).
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionListDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionListDialog(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
void refresh();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private:
|
||||
FunctionListPanel *m_panel = nullptr;
|
||||
};
|
||||
332
barecode/src/editor/FunctionListPanel.cpp
Normal file
332
barecode/src/editor/FunctionListPanel.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
#include "FunctionListPanel.h"
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QProgressBar>
|
||||
|
||||
FunctionListPanel::FunctionListPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_scanner = new FunctionScanner(this);
|
||||
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
|
||||
|
||||
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
|
||||
this, &FunctionListPanel::onScanFinished);
|
||||
|
||||
setupUi();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::setupUi()
|
||||
{
|
||||
QVBoxLayout *root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(0, 0, 0, 0);
|
||||
root->setSpacing(0);
|
||||
|
||||
// ---- Kopfzeile ----
|
||||
QWidget *header = new QWidget(this);
|
||||
header->setStyleSheet("background: palette(mid);");
|
||||
QHBoxLayout *headerLayout = new QHBoxLayout(header);
|
||||
headerLayout->setContentsMargins(6, 4, 6, 4);
|
||||
headerLayout->setSpacing(4);
|
||||
|
||||
QLabel *title = new QLabel(tr("Projektfunktionen"), header);
|
||||
QFont boldFont = title->font();
|
||||
boldFont.setBold(true);
|
||||
title->setFont(boldFont);
|
||||
headerLayout->addWidget(title);
|
||||
headerLayout->addStretch();
|
||||
|
||||
m_btnRefresh = new QPushButton(tr("↻"), header);
|
||||
m_btnRefresh->setFixedSize(24, 24);
|
||||
m_btnRefresh->setFlat(true);
|
||||
m_btnRefresh->setToolTip(tr("Neu scannen"));
|
||||
headerLayout->addWidget(m_btnRefresh);
|
||||
|
||||
root->addWidget(header);
|
||||
|
||||
// ---- Filter + Gruppierung ----
|
||||
QHBoxLayout *toolRow = new QHBoxLayout();
|
||||
toolRow->setContentsMargins(6, 4, 6, 4);
|
||||
toolRow->setSpacing(4);
|
||||
|
||||
m_filterEdit = new QLineEdit(this);
|
||||
m_filterEdit->setPlaceholderText(tr("Funktion suchen…"));
|
||||
m_filterEdit->setClearButtonEnabled(true);
|
||||
toolRow->addWidget(m_filterEdit, 1);
|
||||
|
||||
m_groupCombo = new QComboBox(this);
|
||||
m_groupCombo->addItem(tr("Nach Datei"), "file");
|
||||
m_groupCombo->addItem(tr("Nach Klasse"), "class");
|
||||
m_groupCombo->addItem(tr("Alphabetisch"), "alpha");
|
||||
m_groupCombo->setFixedWidth(120);
|
||||
toolRow->addWidget(m_groupCombo);
|
||||
|
||||
root->addLayout(toolRow);
|
||||
|
||||
// ---- Status ----
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_statusLabel->setContentsMargins(6, 0, 6, 2);
|
||||
m_statusLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
|
||||
root->addWidget(m_statusLabel);
|
||||
|
||||
// ---- Ergebnisbaum ----
|
||||
m_tree = new QTreeWidget(this);
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setIndentation(14);
|
||||
m_tree->setAlternatingRowColors(false);
|
||||
m_tree->setAnimated(true);
|
||||
|
||||
// Monospace-Font für Parameter
|
||||
QFont monoFont("Monospace", m_tree->font().pointSize());
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
m_tree->setFont(monoFont);
|
||||
|
||||
root->addWidget(m_tree, 1);
|
||||
|
||||
// ---- Verbindungen ----
|
||||
connect(m_btnRefresh, &QPushButton::clicked,
|
||||
this, &FunctionListPanel::refresh);
|
||||
|
||||
connect(m_filterEdit, &QLineEdit::textChanged,
|
||||
this, &FunctionListPanel::onFilterChanged);
|
||||
|
||||
connect(m_groupCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &FunctionListPanel::onGroupingChanged);
|
||||
|
||||
connect(m_tree, &QTreeWidget::itemActivated,
|
||||
this, &FunctionListPanel::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche Schnittstelle
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::setProjectRoot(const QString &path)
|
||||
{
|
||||
m_projectRoot = path;
|
||||
if (!path.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_tree->clear();
|
||||
m_statusLabel->setText(QString());
|
||||
m_lastResult.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::activate()
|
||||
{
|
||||
m_filterEdit->setFocus();
|
||||
m_filterEdit->selectAll();
|
||||
if (m_lastResult.isEmpty() && !m_projectRoot.isEmpty())
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan starten
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::refresh()
|
||||
{
|
||||
if (m_projectRoot.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("Kein Projekt geöffnet."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_watcher->isRunning())
|
||||
{
|
||||
m_watcher->cancel();
|
||||
m_watcher->waitForFinished();
|
||||
}
|
||||
|
||||
m_statusLabel->setText(tr("Scanne…"));
|
||||
m_btnRefresh->setEnabled(false);
|
||||
|
||||
const QString root = m_projectRoot;
|
||||
FunctionScanner *scanner = m_scanner;
|
||||
|
||||
QFuture<QList<FunctionScanner::FunctionInfo>> future =
|
||||
QtConcurrent::run([scanner, root]()
|
||||
{
|
||||
return scanner->scanDirectory(root);
|
||||
});
|
||||
|
||||
m_watcher->setFuture(future);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan abgeschlossen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::onScanFinished()
|
||||
{
|
||||
m_btnRefresh->setEnabled(true);
|
||||
|
||||
if (m_watcher->isCanceled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastResult = m_watcher->result();
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Baum befüllen
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::populateTree(const QList<FunctionScanner::FunctionInfo> &functions)
|
||||
{
|
||||
m_tree->clear();
|
||||
|
||||
const QString groupBy = m_groupCombo->currentData().toString();
|
||||
const QString filter = m_filterEdit->text().trimmed().toLower();
|
||||
|
||||
// Funktionen filtern
|
||||
QList<FunctionScanner::FunctionInfo> filtered;
|
||||
for (const auto &f : functions)
|
||||
{
|
||||
if (filter.isEmpty() ||
|
||||
f.name.toLower().contains(filter) ||
|
||||
f.parameters.toLower().contains(filter) ||
|
||||
f.className.toLower().contains(filter))
|
||||
{
|
||||
filtered.append(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBy == "alpha")
|
||||
{
|
||||
// Alphabetisch — keine Gruppen
|
||||
std::sort(filtered.begin(), filtered.end(),
|
||||
[](const FunctionScanner::FunctionInfo &a,
|
||||
const FunctionScanner::FunctionInfo &b)
|
||||
{
|
||||
return a.name.toLower() < b.name.toLower();
|
||||
});
|
||||
|
||||
for (const auto &f : filtered)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(m_tree);
|
||||
formatFunctionItem(item, f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gruppiert nach Datei oder Klasse
|
||||
QMap<QString, QList<FunctionScanner::FunctionInfo>> groups;
|
||||
|
||||
for (const auto &f : filtered)
|
||||
{
|
||||
QString key;
|
||||
if (groupBy == "class")
|
||||
{
|
||||
key = f.className.isEmpty() ? tr("(global)") : f.className;
|
||||
}
|
||||
else
|
||||
{
|
||||
key = QFileInfo(f.filePath).fileName();
|
||||
}
|
||||
groups[key].append(f);
|
||||
}
|
||||
|
||||
for (auto it = groups.begin(); it != groups.end(); ++it)
|
||||
{
|
||||
QTreeWidgetItem *groupItem = new QTreeWidgetItem(m_tree);
|
||||
groupItem->setText(0, QString("%1 (%2)")
|
||||
.arg(it.key())
|
||||
.arg(it.value().size()));
|
||||
groupItem->setData(0, Qt::UserRole, QString());
|
||||
groupItem->setData(0, Qt::UserRole + 1, -1);
|
||||
|
||||
QFont boldFont = groupItem->font(0);
|
||||
boldFont.setBold(true);
|
||||
groupItem->setFont(0, boldFont);
|
||||
groupItem->setExpanded(true);
|
||||
|
||||
// Funktionen innerhalb der Gruppe alphabetisch
|
||||
auto &list = it.value();
|
||||
std::sort(list.begin(), list.end(),
|
||||
[](const FunctionScanner::FunctionInfo &a,
|
||||
const FunctionScanner::FunctionInfo &b)
|
||||
{
|
||||
return a.name.toLower() < b.name.toLower();
|
||||
});
|
||||
|
||||
for (const auto &f : list)
|
||||
{
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(groupItem);
|
||||
formatFunctionItem(item, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Statuszeile
|
||||
const int total = functions.size();
|
||||
const int shown = filtered.size();
|
||||
if (filter.isEmpty())
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 Funktionen gefunden").arg(total));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_statusLabel->setText(tr("%1 von %2 Funktionen").arg(shown).arg(total));
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::formatFunctionItem(QTreeWidgetItem *item,
|
||||
const FunctionScanner::FunctionInfo &f)
|
||||
{
|
||||
// Anzeige: name(parameter) — Zeilennummer klein dahinter
|
||||
const QString display = QString("%1(%2)")
|
||||
.arg(f.name)
|
||||
.arg(f.parameters);
|
||||
|
||||
item->setText(0, display);
|
||||
item->setToolTip(0, QString("%1\nZeile %2").arg(f.filePath).arg(f.line));
|
||||
item->setData(0, Qt::UserRole, f.filePath);
|
||||
item->setData(0, Qt::UserRole + 1, f.line);
|
||||
|
||||
// Zeilennummer in gedimmter Farbe als zweite Spalte simulieren
|
||||
// (über ToolTip gelöst da wir nur eine Spalte haben)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void FunctionListPanel::onItemActivated(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
const QString path = item->data(0, Qt::UserRole).toString();
|
||||
const int line = item->data(0, Qt::UserRole + 1).toInt();
|
||||
|
||||
if (path.isEmpty() || line < 0)
|
||||
{
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
emit fileLineRequested(path, line);
|
||||
}
|
||||
|
||||
void FunctionListPanel::onFilterChanged(const QString &text)
|
||||
{
|
||||
Q_UNUSED(text)
|
||||
if (!m_lastResult.isEmpty())
|
||||
{
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
}
|
||||
|
||||
void FunctionListPanel::onGroupingChanged(int /*index*/)
|
||||
{
|
||||
if (!m_lastResult.isEmpty())
|
||||
{
|
||||
populateTree(m_lastResult);
|
||||
}
|
||||
}
|
||||
67
barecode/src/editor/FunctionListPanel.h
Normal file
67
barecode/src/editor/FunctionListPanel.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QFutureWatcher>
|
||||
#include <QString>
|
||||
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionListPanel – Zeigt alle im Projekt definierten Funktionen
|
||||
// als durchsuchbare, sortierbare Liste.
|
||||
//
|
||||
// Gruppierung: nach Datei oder nach Klasse
|
||||
// Klick: öffnet Datei und springt zur Definition
|
||||
// Aktualisierung: automatisch nach jedem Speichern
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionListPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FunctionListPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setProjectRoot(const QString &path);
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
void activate();
|
||||
|
||||
signals:
|
||||
void fileLineRequested(const QString &filePath, int line);
|
||||
|
||||
private slots:
|
||||
void onScanFinished();
|
||||
void onItemActivated(QTreeWidgetItem *item, int column);
|
||||
void onFilterChanged(const QString &text);
|
||||
void onGroupingChanged(int index);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateTree(const QList<FunctionScanner::FunctionInfo> &functions);
|
||||
void applyFilter(const QString &text);
|
||||
static void formatFunctionItem(QTreeWidgetItem *item,
|
||||
const FunctionScanner::FunctionInfo &f);
|
||||
|
||||
QString m_projectRoot;
|
||||
|
||||
QLineEdit *m_filterEdit = nullptr;
|
||||
QComboBox *m_groupCombo = nullptr;
|
||||
QPushButton *m_btnRefresh = nullptr;
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QTreeWidget *m_tree = nullptr;
|
||||
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
|
||||
|
||||
// Letztes Scan-Ergebnis für Filter ohne Neuscan
|
||||
QList<FunctionScanner::FunctionInfo> m_lastResult;
|
||||
};
|
||||
158
barecode/src/editor/FunctionScanner.cpp
Normal file
158
barecode/src/editor/FunctionScanner.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "FunctionScanner.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
|
||||
FunctionScanner::FunctionScanner(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Verzeichnis rekursiv scannen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanDirectory(
|
||||
const QString &rootPath,
|
||||
const QStringList &extensions) const
|
||||
{
|
||||
QList<FunctionInfo> results;
|
||||
|
||||
QDirIterator it(rootPath,
|
||||
extensions,
|
||||
QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString path = it.next();
|
||||
results.append(scanFile(path));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Einzelne Datei scannen
|
||||
// ---------------------------------------------------------------------------
|
||||
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanFile(const QString &filePath) const
|
||||
{
|
||||
QList<FunctionInfo> results;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
|
||||
QStringList lines;
|
||||
while (!stream.atEnd())
|
||||
{
|
||||
lines.append(stream.readLine());
|
||||
}
|
||||
|
||||
// Regex für Funktionsdefinitionen:
|
||||
// optional: public/protected/private/static/abstract/final
|
||||
// gefolgt von: function name(
|
||||
static const QRegularExpression funcRegex(
|
||||
R"((?:(?:public|protected|private|static|abstract|final)\s+)*)"
|
||||
R"(function\s+(&?\s*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i)
|
||||
{
|
||||
const QString &line = lines[i];
|
||||
|
||||
// Zeilen in Kommentaren überspringen
|
||||
const QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QRegularExpressionMatch match = funcRegex.match(line);
|
||||
if (!match.hasMatch())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FunctionInfo info;
|
||||
info.name = match.captured(1).trimmed();
|
||||
info.filePath = filePath;
|
||||
info.line = i + 1;
|
||||
info.className = detectClassContext(lines, i);
|
||||
info.parameters = extractParameters(line, static_cast<int>(match.capturedEnd()) - 1);
|
||||
info.signature = info.name + "(" + info.parameters + ")";
|
||||
|
||||
// Konstruktoren und magische Methoden kennzeichnen
|
||||
// aber trotzdem aufnehmen — sie sind nützlich in der Liste
|
||||
|
||||
results.append(info);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter aus der Funktionssignatur extrahieren
|
||||
// Behandelt auch mehrzeilige Signaturen mit öffnender Klammer am Ende
|
||||
// ---------------------------------------------------------------------------
|
||||
QString FunctionScanner::extractParameters(const QString &line, int parenPos)
|
||||
{
|
||||
// Inhalt zwischen ( und ) extrahieren
|
||||
// Einfache Version: nur die erste Zeile — reicht für 99% aller Fälle
|
||||
const int openParen = parenPos;
|
||||
int depth = 0;
|
||||
int closePos = -1;
|
||||
|
||||
for (int i = openParen; i < line.length(); ++i)
|
||||
{
|
||||
if (line[i] == '(') { ++depth; }
|
||||
else if (line[i] == ')')
|
||||
{
|
||||
--depth;
|
||||
if (depth == 0)
|
||||
{
|
||||
closePos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closePos == -1)
|
||||
{
|
||||
// Klammer geht über Zeilenende — gekürzt anzeigen
|
||||
return line.mid(openParen + 1).trimmed() + "…";
|
||||
}
|
||||
|
||||
return line.mid(openParen + 1, closePos - openParen - 1).trimmed();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klassenkontext erkennen — rückwärts durch den Code suchen
|
||||
// ---------------------------------------------------------------------------
|
||||
QString FunctionScanner::detectClassContext(const QStringList &lines, int functionLineIndex)
|
||||
{
|
||||
static const QRegularExpression classRegex(
|
||||
R"((?:class|interface|trait|enum)\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))",
|
||||
QRegularExpression::CaseInsensitiveOption
|
||||
);
|
||||
|
||||
// Rückwärts suchen — letztes class/interface/trait vor dieser Zeile
|
||||
for (int i = functionLineIndex - 1; i >= 0; --i)
|
||||
{
|
||||
QRegularExpressionMatch match = classRegex.match(lines[i]);
|
||||
if (match.hasMatch())
|
||||
{
|
||||
return match.captured(1);
|
||||
}
|
||||
}
|
||||
|
||||
return QString(); // Globale Funktion
|
||||
}
|
||||
47
barecode/src/editor/FunctionScanner.h
Normal file
47
barecode/src/editor/FunctionScanner.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QList>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FunctionScanner – Scannt PHP-Dateien nach Funktionsdefinitionen.
|
||||
//
|
||||
// Liefert für jede gefundene Funktion:
|
||||
// - Name
|
||||
// - Parameter (vollständige Signatur)
|
||||
// - Dateipfad
|
||||
// - Zeilennummer
|
||||
//
|
||||
// Wird von FunctionListPanel (Anzeige) und DeadCodeAnalyzer (Stufe 1)
|
||||
// gemeinsam genutzt.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FunctionScanner : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct FunctionInfo
|
||||
{
|
||||
QString name; // Funktionsname
|
||||
QString parameters; // Parameter wie definiert, z.B. "$id, $name = null"
|
||||
QString signature; // name(parameters) — fertig formatiert
|
||||
QString filePath; // Absoluter Pfad zur Datei
|
||||
int line = 0; // Zeilennummer der Definition
|
||||
QString className; // Klassen- oder Namespace-Kontext, leer = global
|
||||
};
|
||||
|
||||
explicit FunctionScanner(QObject *parent = nullptr);
|
||||
|
||||
// Synchroner Scan — für direkten Aufruf aus Threads
|
||||
QList<FunctionInfo> scanDirectory(const QString &rootPath,
|
||||
const QStringList &extensions = {"*.php"}) const;
|
||||
|
||||
// Scannt eine einzelne Datei
|
||||
QList<FunctionInfo> scanFile(const QString &filePath) const;
|
||||
|
||||
private:
|
||||
static QString extractParameters(const QString &line, int parenPos);
|
||||
static QString detectClassContext(const QStringList &lines, int functionLineIndex);
|
||||
};
|
||||
18
barecode/src/editor/LineNumberArea.cpp
Normal file
18
barecode/src/editor/LineNumberArea.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "LineNumberArea.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
LineNumberArea::LineNumberArea(CodeEditor *editor)
|
||||
: QWidget(editor)
|
||||
, m_codeEditor(editor)
|
||||
{
|
||||
}
|
||||
|
||||
QSize LineNumberArea::sizeHint() const
|
||||
{
|
||||
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
|
||||
}
|
||||
|
||||
void LineNumberArea::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
m_codeEditor->lineNumberAreaPaintEvent(event);
|
||||
}
|
||||
25
barecode/src/editor/LineNumberArea.h
Normal file
25
barecode/src/editor/LineNumberArea.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LineNumberArea – Thin widget painted on the left side of the CodeEditor.
|
||||
// Painted by CodeEditor::lineNumberAreaPaintEvent().
|
||||
// ---------------------------------------------------------------------------
|
||||
class LineNumberArea : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LineNumberArea(CodeEditor *editor);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
CodeEditor *m_codeEditor;
|
||||
};
|
||||
492
barecode/src/editor/SearchPanel.cpp
Normal file
492
barecode/src/editor/SearchPanel.cpp
Normal file
@@ -0,0 +1,492 @@
|
||||
#include "SearchPanel.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QRegularExpression>
|
||||
#include <QMessageBox>
|
||||
#include <QKeyEvent>
|
||||
#include <QShortcut>
|
||||
|
||||
SearchPanel::SearchPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
hide();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::setupUi()
|
||||
{
|
||||
m_grid = new QGridLayout(this);
|
||||
m_grid->setContentsMargins(6, 4, 6, 4);
|
||||
m_grid->setSpacing(4);
|
||||
|
||||
// ---- Row 0: Suchen ----
|
||||
m_searchEdit = new QLineEdit(this);
|
||||
m_searchEdit->setPlaceholderText(tr("Suchen…"));
|
||||
m_searchEdit->setClearButtonEnabled(true);
|
||||
|
||||
m_btnPrev = new QPushButton(tr("▲"), this);
|
||||
m_btnNext = new QPushButton(tr("▼"), this);
|
||||
m_btnPrev->setFixedWidth(28);
|
||||
m_btnNext->setFixedWidth(28);
|
||||
m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)"));
|
||||
m_btnNext->setToolTip(tr("Nächster Treffer (F3)"));
|
||||
|
||||
m_matchLabel = new QLabel(this);
|
||||
m_matchLabel->setMinimumWidth(80);
|
||||
|
||||
m_btnClose = new QPushButton(tr("✕"), this);
|
||||
m_btnClose->setFixedWidth(24);
|
||||
m_btnClose->setToolTip(tr("Schließen (Esc)"));
|
||||
m_btnClose->setFlat(true);
|
||||
|
||||
QHBoxLayout *searchRow = new QHBoxLayout();
|
||||
searchRow->addWidget(new QLabel(tr("Suchen:"), this));
|
||||
searchRow->addWidget(m_searchEdit, 1);
|
||||
searchRow->addWidget(m_btnPrev);
|
||||
searchRow->addWidget(m_btnNext);
|
||||
searchRow->addWidget(m_matchLabel);
|
||||
searchRow->addWidget(m_btnClose);
|
||||
m_grid->addLayout(searchRow, 0, 0);
|
||||
|
||||
// ---- Row 1: Ersetzen ----
|
||||
m_replaceEdit = new QLineEdit(this);
|
||||
m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…"));
|
||||
m_replaceEdit->setClearButtonEnabled(true);
|
||||
|
||||
m_btnReplace = new QPushButton(tr("Ersetzen"), this);
|
||||
m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this);
|
||||
m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this);
|
||||
|
||||
QHBoxLayout *replaceRow = new QHBoxLayout();
|
||||
replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this));
|
||||
replaceRow->addWidget(m_replaceEdit, 1);
|
||||
replaceRow->addWidget(m_btnReplace);
|
||||
replaceRow->addWidget(m_btnReplaceAll);
|
||||
replaceRow->addWidget(m_btnReplaceSelection);
|
||||
m_grid->addLayout(replaceRow, 1, 0);
|
||||
|
||||
// ---- Row 2: Optionen ----
|
||||
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
|
||||
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
|
||||
m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this);
|
||||
|
||||
QHBoxLayout *optRow = new QHBoxLayout();
|
||||
optRow->addWidget(m_chkCase);
|
||||
optRow->addWidget(m_chkWord);
|
||||
optRow->addWidget(m_chkRegex);
|
||||
optRow->addStretch();
|
||||
m_grid->addLayout(optRow, 2, 0);
|
||||
|
||||
// ---- Connections ----
|
||||
connect(m_searchEdit, &QLineEdit::textChanged,
|
||||
this, &SearchPanel::onSearchTextChanged);
|
||||
|
||||
connect(m_searchEdit, &QLineEdit::returnPressed,
|
||||
this, &SearchPanel::findNext);
|
||||
|
||||
connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext);
|
||||
connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious);
|
||||
|
||||
connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent);
|
||||
connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll);
|
||||
connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection);
|
||||
|
||||
connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked);
|
||||
|
||||
connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::setEditor(CodeEditor *editor)
|
||||
{
|
||||
clearHighlights();
|
||||
m_editor = editor;
|
||||
}
|
||||
|
||||
void SearchPanel::activate()
|
||||
{
|
||||
show();
|
||||
m_searchEdit->setFocus();
|
||||
m_searchEdit->selectAll();
|
||||
|
||||
// Pre-fill with selected text if short enough
|
||||
if (m_editor)
|
||||
{
|
||||
const QString sel = m_editor->textCursor().selectedText();
|
||||
if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200)
|
||||
{
|
||||
m_searchEdit->setText(sel);
|
||||
}
|
||||
}
|
||||
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const
|
||||
{
|
||||
QTextDocument::FindFlags flags;
|
||||
if (backwards) { flags |= QTextDocument::FindBackward; }
|
||||
if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; }
|
||||
if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; }
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool SearchPanel::performFind(bool backwards)
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(backwards);
|
||||
bool found = false;
|
||||
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(m_searchEdit->text());
|
||||
if (m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::NoPatternOption);
|
||||
}
|
||||
else
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
found = m_editor->find(re, flags);
|
||||
|
||||
// Wrap around
|
||||
if (!found)
|
||||
{
|
||||
QTextCursor c = m_editor->textCursor();
|
||||
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
|
||||
m_editor->setTextCursor(c);
|
||||
found = m_editor->find(re, flags);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
found = m_editor->find(m_searchEdit->text(), flags);
|
||||
|
||||
// Wrap around
|
||||
if (!found)
|
||||
{
|
||||
QTextCursor c = m_editor->textCursor();
|
||||
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
|
||||
m_editor->setTextCursor(c);
|
||||
found = m_editor->find(m_searchEdit->text(), flags);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void SearchPanel::highlightAllMatches()
|
||||
{
|
||||
if (!m_editor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QTextEdit::ExtraSelection> extras;
|
||||
|
||||
const QString needle = m_searchEdit->text();
|
||||
if (needle.isEmpty())
|
||||
{
|
||||
m_editor->setExtraSelections(extras);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCharFormat fmt;
|
||||
fmt.setBackground(QColor("#3a3a00"));
|
||||
fmt.setForeground(QColor("#ffff80"));
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
QTextEdit::ExtraSelection sel;
|
||||
sel.cursor = cursor;
|
||||
sel.format = fmt;
|
||||
extras.append(sel);
|
||||
}
|
||||
|
||||
m_editor->setExtraSelections(extras);
|
||||
}
|
||||
|
||||
void SearchPanel::clearHighlights()
|
||||
{
|
||||
if (m_editor)
|
||||
{
|
||||
m_editor->setExtraSelections({});
|
||||
}
|
||||
}
|
||||
|
||||
void SearchPanel::updateMatchLabel()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
m_matchLabel->setText(QString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Count total matches
|
||||
int count = 0;
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
++count;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
m_matchLabel->setText(tr("Kein Treffer"));
|
||||
m_matchLabel->setStyleSheet("color: #cc4444;");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_matchLabel->setText(tr("%1 Treffer").arg(count));
|
||||
m_matchLabel->setStyleSheet(QString());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::findNext()
|
||||
{
|
||||
performFind(false);
|
||||
}
|
||||
|
||||
void SearchPanel::findPrevious()
|
||||
{
|
||||
performFind(true);
|
||||
}
|
||||
|
||||
void SearchPanel::replaceCurrent()
|
||||
{
|
||||
if (!m_editor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = m_editor->textCursor();
|
||||
|
||||
// If current selection matches the search term, replace it
|
||||
// Otherwise just find the next occurrence first
|
||||
const bool hasMatch = !cursor.selectedText().isEmpty();
|
||||
if (!hasMatch)
|
||||
{
|
||||
performFind(false);
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.insertText(m_replaceEdit->text());
|
||||
|
||||
// Move to next match
|
||||
performFind(false);
|
||||
updateMatchLabel();
|
||||
highlightAllMatches();
|
||||
}
|
||||
|
||||
void SearchPanel::replaceAll()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
cursor.beginEditBlock();
|
||||
|
||||
int count = 0;
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
const QString replacement = m_replaceEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.insertText(replacement);
|
||||
++count;
|
||||
}
|
||||
|
||||
cursor.endEditBlock();
|
||||
|
||||
updateMatchLabel();
|
||||
clearHighlights();
|
||||
|
||||
QMessageBox::information(this, tr("Alle ersetzen"),
|
||||
tr("%1 Ersetzung(en) durchgeführt.").arg(count));
|
||||
}
|
||||
|
||||
void SearchPanel::replaceInSelection()
|
||||
{
|
||||
if (!m_editor || m_searchEdit->text().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor selCursor = m_editor->textCursor();
|
||||
if (!selCursor.hasSelection())
|
||||
{
|
||||
QMessageBox::information(this, tr("In Auswahl ersetzen"),
|
||||
tr("Es ist kein Text ausgewählt."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Work only within the selected region
|
||||
const int selStart = selCursor.selectionStart();
|
||||
const int selEnd = selCursor.selectionEnd();
|
||||
|
||||
QTextDocument *doc = m_editor->document();
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(selStart);
|
||||
cursor.beginEditBlock();
|
||||
|
||||
int count = 0;
|
||||
int offset = 0; // Replacement may be longer/shorter than search term
|
||||
const QTextDocument::FindFlags flags = buildFindFlags(false);
|
||||
const QString needle = m_searchEdit->text();
|
||||
const QString replacement = m_replaceEdit->text();
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (m_chkRegex->isChecked())
|
||||
{
|
||||
QRegularExpression re(needle);
|
||||
if (!m_chkCase->isChecked())
|
||||
{
|
||||
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
cursor = doc->find(re, cursor, flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = doc->find(needle, cursor, flags);
|
||||
}
|
||||
|
||||
if (cursor.isNull())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop if we've left the original selection
|
||||
if (cursor.selectionEnd() > selEnd + offset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
offset += replacement.length() - cursor.selectedText().length();
|
||||
cursor.insertText(replacement);
|
||||
++count;
|
||||
}
|
||||
|
||||
cursor.endEditBlock();
|
||||
|
||||
updateMatchLabel();
|
||||
clearHighlights();
|
||||
|
||||
QMessageBox::information(this, tr("In Auswahl ersetzen"),
|
||||
tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private slots
|
||||
// ---------------------------------------------------------------------------
|
||||
void SearchPanel::onSearchTextChanged(const QString &/*text*/)
|
||||
{
|
||||
highlightAllMatches();
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
void SearchPanel::onOptionChanged()
|
||||
{
|
||||
highlightAllMatches();
|
||||
updateMatchLabel();
|
||||
}
|
||||
|
||||
void SearchPanel::onCloseClicked()
|
||||
{
|
||||
clearHighlights();
|
||||
m_matchLabel->setText(QString());
|
||||
hide();
|
||||
if (m_editor)
|
||||
{
|
||||
m_editor->setFocus();
|
||||
}
|
||||
}
|
||||
79
barecode/src/editor/SearchPanel.h
Normal file
79
barecode/src/editor/SearchPanel.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTextDocument>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SearchPanel – Collapsible find/replace bar that operates on a CodeEditor.
|
||||
//
|
||||
// Capabilities:
|
||||
// • Nächsten / Vorherigen Treffer suchen
|
||||
// • Einzeln ersetzen
|
||||
// • Alle ersetzen
|
||||
// • Nur in Auswahl ersetzen
|
||||
// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke
|
||||
// ---------------------------------------------------------------------------
|
||||
class SearchPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SearchPanel(QWidget *parent = nullptr);
|
||||
|
||||
// Must be called whenever the active editor changes
|
||||
void setEditor(CodeEditor *editor);
|
||||
|
||||
// Toggle visibility and focus the search field
|
||||
void activate();
|
||||
|
||||
public slots:
|
||||
void findNext();
|
||||
void findPrevious();
|
||||
void replaceCurrent();
|
||||
void replaceAll();
|
||||
void replaceInSelection();
|
||||
|
||||
private slots:
|
||||
void onSearchTextChanged(const QString &text);
|
||||
void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert)
|
||||
void onCloseClicked();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
QTextDocument::FindFlags buildFindFlags(bool backwards = false) const;
|
||||
bool performFind(bool backwards = false);
|
||||
void highlightAllMatches();
|
||||
void clearHighlights();
|
||||
void updateMatchLabel();
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
|
||||
// Search row
|
||||
QLineEdit *m_searchEdit = nullptr;
|
||||
QPushButton *m_btnPrev = nullptr;
|
||||
QPushButton *m_btnNext = nullptr;
|
||||
QLabel *m_matchLabel = nullptr;
|
||||
QPushButton *m_btnClose = nullptr;
|
||||
|
||||
// Replace row
|
||||
QLineEdit *m_replaceEdit = nullptr;
|
||||
QPushButton *m_btnReplace = nullptr;
|
||||
QPushButton *m_btnReplaceAll = nullptr;
|
||||
QPushButton *m_btnReplaceSelection = nullptr;
|
||||
|
||||
// Options row
|
||||
QCheckBox *m_chkCase = nullptr;
|
||||
QCheckBox *m_chkWord = nullptr;
|
||||
QCheckBox *m_chkRegex = nullptr;
|
||||
|
||||
QGridLayout *m_grid = nullptr;
|
||||
};
|
||||
222
barecode/src/editor/SignatureHelper.cpp
Normal file
222
barecode/src/editor/SignatureHelper.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "SignatureHelper.h"
|
||||
#include "SignatureTooltip.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRect>
|
||||
#include <QRegularExpression>
|
||||
|
||||
SignatureHelper::SignatureHelper(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Tooltip als Kind des Viewports — bleibt im Fenster
|
||||
m_tooltip = new SignatureTooltip(editor->window());
|
||||
|
||||
loadDatabase();
|
||||
|
||||
connect(m_editor, &CodeEditor::cursorPositionChanged,
|
||||
this, &SignatureHelper::onCursorPositionChanged);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Datenbank laden
|
||||
// ---------------------------------------------------------------------------
|
||||
void SignatureHelper::loadDatabase()
|
||||
{
|
||||
QFile f(":/php_functions.json");
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||
if (!doc.isArray())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &val : doc.array())
|
||||
{
|
||||
const QJsonObject obj = val.toObject();
|
||||
const QString name = obj["name"].toString();
|
||||
if (name.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FunctionInfo info;
|
||||
info.signature = obj["signature"].toString();
|
||||
info.description = obj["desc"].toString();
|
||||
m_functions.insert(name.toLower(), info);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cursor-Bewegung auswerten
|
||||
// ---------------------------------------------------------------------------
|
||||
void SignatureHelper::onCursorPositionChanged()
|
||||
{
|
||||
const QTextCursor cursor = m_editor->textCursor();
|
||||
const QString block = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
const QString leftText = block.left(col);
|
||||
|
||||
const QString funcName = extractFunctionName(leftText);
|
||||
|
||||
if (funcName.isEmpty())
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Klammern zählen — wenn alle geschlossen, Tooltip ausblenden
|
||||
if (countOpenParens(leftText) <= 0)
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString key = funcName.toLower();
|
||||
if (!m_functions.contains(key))
|
||||
{
|
||||
m_tooltip->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const FunctionInfo &info = m_functions[key];
|
||||
|
||||
// Position unter dem Cursor berechnen
|
||||
const QRect cursorRect = m_editor->cursorRect(cursor);
|
||||
const QPoint globalPos = m_editor->viewport()->mapToGlobal(
|
||||
QPoint(cursorRect.left(), cursorRect.bottom() + 4)
|
||||
);
|
||||
|
||||
m_tooltip->showSignature(info.signature, info.description, globalPos);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Funktionsnamen links vor der öffnenden Klammer extrahieren
|
||||
// ---------------------------------------------------------------------------
|
||||
QString SignatureHelper::extractFunctionName(const QString &text) const
|
||||
{
|
||||
// Wir suchen das letzte '(' das zu einem Funktionsnamen gehört.
|
||||
// Dabei müssen wir verschachtelte Klammern korrekt behandeln.
|
||||
int depth = 0;
|
||||
int openPos = -1;
|
||||
|
||||
for (int i = text.length() - 1; i >= 0; --i)
|
||||
{
|
||||
const QChar ch = text[i];
|
||||
if (ch == ')')
|
||||
{
|
||||
++depth;
|
||||
}
|
||||
else if (ch == '(')
|
||||
{
|
||||
if (depth == 0)
|
||||
{
|
||||
openPos = i;
|
||||
break;
|
||||
}
|
||||
--depth;
|
||||
}
|
||||
}
|
||||
|
||||
if (openPos <= 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Funktionsnamen direkt links von '(' lesen
|
||||
int end = openPos - 1;
|
||||
|
||||
// Leerzeichen überspringen
|
||||
while (end >= 0 && text[end].isSpace())
|
||||
{
|
||||
--end;
|
||||
}
|
||||
|
||||
if (end < 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Bezeichner-Zeichen sammeln (Buchstaben, Ziffern, _, :, \)
|
||||
int start = end;
|
||||
while (start > 0 &&
|
||||
(text[start - 1].isLetterOrNumber() ||
|
||||
text[start - 1] == '_' ||
|
||||
text[start - 1] == ':' ||
|
||||
text[start - 1] == '\\'))
|
||||
{
|
||||
--start;
|
||||
}
|
||||
|
||||
const QString name = text.mid(start, end - start + 1);
|
||||
|
||||
// Schlüsselwörter und leere Namen ausschließen
|
||||
static const QStringList keywords = {
|
||||
"if", "else", "elseif", "while", "for", "foreach",
|
||||
"switch", "match", "catch", "function", "fn"
|
||||
};
|
||||
|
||||
if (name.isEmpty() || keywords.contains(name.toLower()))
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Nur den letzten Teil nach :: oder -> nehmen
|
||||
const int colonPos = name.lastIndexOf("::");
|
||||
const int arrowPos = name.lastIndexOf("->");
|
||||
const int backslashPos = name.lastIndexOf("\\");
|
||||
const int splitPos = qMax(backslashPos, qMax(colonPos, arrowPos));
|
||||
|
||||
if (splitPos >= 0)
|
||||
{
|
||||
return name.mid(splitPos + (name[splitPos] == ':' ? 2 : (name[splitPos] == '\\' ? 1 : 2)));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offene Klammern zählen
|
||||
// ---------------------------------------------------------------------------
|
||||
int SignatureHelper::countOpenParens(const QString &text) const
|
||||
{
|
||||
int depth = 0;
|
||||
bool inString = false;
|
||||
QChar stringChar;
|
||||
|
||||
for (int i = 0; i < text.length(); ++i)
|
||||
{
|
||||
const QChar ch = text[i];
|
||||
|
||||
// Einfache String-Erkennung (kein vollständiger PHP-Parser)
|
||||
if (!inString && (ch == '\'' || ch == '"'))
|
||||
{
|
||||
inString = true;
|
||||
stringChar = ch;
|
||||
continue;
|
||||
}
|
||||
if (inString)
|
||||
{
|
||||
if (ch == stringChar && (i == 0 || text[i - 1] != '\\'))
|
||||
{
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '(') { ++depth; }
|
||||
else if (ch == ')') { --depth; }
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
51
barecode/src/editor/SignatureHelper.h
Normal file
51
barecode/src/editor/SignatureHelper.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
|
||||
class CodeEditor;
|
||||
class SignatureTooltip;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SignatureHelper – Lädt die PHP-Funktionsdatenbank und zeigt beim Tippen
|
||||
// automatisch die passende Funktionssignatur als Tooltip.
|
||||
//
|
||||
// Logik:
|
||||
// • Bei jedem Tastendruck: Text links vom Cursor analysieren
|
||||
// • Wenn "funktionsname(" erkannt wird → Tooltip anzeigen
|
||||
// • Wenn ")" die öffnende Klammer schließt → Tooltip verstecken
|
||||
// • Wenn Cursor sich weg bewegt → Tooltip verstecken
|
||||
// ---------------------------------------------------------------------------
|
||||
class SignatureHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignatureHelper(CodeEditor *editor);
|
||||
|
||||
private slots:
|
||||
void onCursorPositionChanged();
|
||||
|
||||
private:
|
||||
struct FunctionInfo
|
||||
{
|
||||
QString signature;
|
||||
QString description;
|
||||
};
|
||||
|
||||
void loadDatabase();
|
||||
void loadProjectFunctions();
|
||||
|
||||
// Extrahiert den Funktionsnamen direkt links vor dem letzten '('
|
||||
// Gibt leeren String zurück wenn kein Kontext gefunden
|
||||
QString extractFunctionName(const QString &textUpToCursor) const;
|
||||
|
||||
// Zählt offene Klammern — bei 0 ist der Aufruf abgeschlossen
|
||||
int countOpenParens(const QString &textUpToCursor) const;
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
SignatureTooltip *m_tooltip = nullptr;
|
||||
|
||||
QHash<QString, FunctionInfo> m_functions; // name → info
|
||||
};
|
||||
91
barecode/src/editor/SignatureTooltip.cpp
Normal file
91
barecode/src/editor/SignatureTooltip.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "SignatureTooltip.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
SignatureTooltip::SignatureTooltip(QWidget *parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
setFrameShadow(QFrame::Raised);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
|
||||
// Dezentes Styling passend zu Hell- und Dunkeltheme
|
||||
setStyleSheet(
|
||||
"SignatureTooltip {"
|
||||
" background: palette(toolTipBase);"
|
||||
" border: 1px solid palette(mid);"
|
||||
" border-radius: 4px;"
|
||||
" padding: 4px;"
|
||||
"}"
|
||||
);
|
||||
|
||||
m_layout = new QVBoxLayout(this);
|
||||
m_layout->setContentsMargins(8, 6, 8, 6);
|
||||
m_layout->setSpacing(3);
|
||||
|
||||
// Signatur — Monospace, deutlich hervorgehoben
|
||||
m_sigLabel = new QLabel(this);
|
||||
m_sigLabel->setTextFormat(Qt::PlainText);
|
||||
m_sigLabel->setWordWrap(false);
|
||||
QFont sigFont = m_sigLabel->font();
|
||||
sigFont.setFamily("Monospace");
|
||||
sigFont.setStyleHint(QFont::Monospace);
|
||||
sigFont.setPointSize(sigFont.pointSize());
|
||||
m_sigLabel->setFont(sigFont);
|
||||
m_sigLabel->setStyleSheet("color: palette(toolTipText); font-weight: bold;");
|
||||
m_layout->addWidget(m_sigLabel);
|
||||
|
||||
// Beschreibung — kleiner, gedimmt
|
||||
m_descLabel = new QLabel(this);
|
||||
m_descLabel->setTextFormat(Qt::PlainText);
|
||||
m_descLabel->setWordWrap(false);
|
||||
m_descLabel->setStyleSheet("color: palette(mid);");
|
||||
QFont descFont = m_descLabel->font();
|
||||
descFont.setPointSize(qMax(descFont.pointSize() - 1, 8));
|
||||
m_descLabel->setFont(descFont);
|
||||
m_layout->addWidget(m_descLabel);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void SignatureTooltip::showSignature(const QString &signature,
|
||||
const QString &description,
|
||||
const QPoint &globalPos)
|
||||
{
|
||||
m_sigLabel->setText(signature);
|
||||
|
||||
if (description.isEmpty())
|
||||
{
|
||||
m_descLabel->hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_descLabel->setText(description);
|
||||
m_descLabel->show();
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
|
||||
// Position so wählen dass das Popup nicht aus dem Bildschirm ragt
|
||||
QPoint pos = globalPos;
|
||||
const QRect screen = QApplication::primaryScreen()->availableGeometry();
|
||||
|
||||
if (pos.x() + width() > screen.right())
|
||||
{
|
||||
pos.setX(screen.right() - width() - 4);
|
||||
}
|
||||
if (pos.y() + height() > screen.bottom())
|
||||
{
|
||||
pos.setY(globalPos.y() - height() - 24);
|
||||
}
|
||||
|
||||
move(pos);
|
||||
show();
|
||||
raise();
|
||||
}
|
||||
|
||||
void SignatureTooltip::hide()
|
||||
{
|
||||
QFrame::hide();
|
||||
}
|
||||
29
barecode/src/editor/SignatureTooltip.h
Normal file
29
barecode/src/editor/SignatureTooltip.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SignatureTooltip – Schwebendes Popup das die Signatur einer Funktion zeigt.
|
||||
// Erscheint unter dem Cursor, verschwindet automatisch wenn der Nutzer
|
||||
// die Klammer schließt oder den Kontext verlässt.
|
||||
// ---------------------------------------------------------------------------
|
||||
class SignatureTooltip : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignatureTooltip(QWidget *parent = nullptr);
|
||||
|
||||
void showSignature(const QString &signature,
|
||||
const QString &description,
|
||||
const QPoint &globalPos);
|
||||
void hide();
|
||||
|
||||
private:
|
||||
QVBoxLayout *m_layout = nullptr;
|
||||
QLabel *m_sigLabel = nullptr;
|
||||
QLabel *m_descLabel = nullptr;
|
||||
};
|
||||
256
barecode/src/editor/VariableCompleter.cpp
Normal file
256
barecode/src/editor/VariableCompleter.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
#include "VariableCompleter.h"
|
||||
#include "CodeEditor.h"
|
||||
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
#include <QKeyEvent>
|
||||
#include <QListWidgetItem>
|
||||
#include <QAbstractItemView>
|
||||
#include <QScrollBar>
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
|
||||
VariableCompleter::VariableCompleter(CodeEditor *editor)
|
||||
: QObject(editor)
|
||||
, m_editor(editor)
|
||||
{
|
||||
// Popup als reines Anzeige-Fenster — dieselbe Fensterart wie
|
||||
// SignatureTooltip, die nachweislich nie den Fokus an sich reißt.
|
||||
// Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst
|
||||
// behandelt der Fenstermanager das Popup als eigenständiges Fenster.
|
||||
// Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste
|
||||
// Popups entstehen konnten, die den Fokus übernehmen und die
|
||||
// Texteingabe blockieren.
|
||||
m_popup = new QListWidget(editor->window());
|
||||
m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
|
||||
m_popup->setAttribute(Qt::WA_ShowWithoutActivating);
|
||||
m_popup->setFocusPolicy(Qt::NoFocus);
|
||||
m_popup->setMaximumHeight(200);
|
||||
m_popup->setMinimumWidth(180);
|
||||
m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_popup->setStyleSheet(
|
||||
"QListWidget {"
|
||||
" border: 1px solid palette(mid);"
|
||||
" background: palette(toolTipBase);"
|
||||
" color: palette(toolTipText);"
|
||||
" font-family: Monospace;"
|
||||
"}"
|
||||
"QListWidget::item:selected {"
|
||||
" background: palette(highlight);"
|
||||
" color: palette(highlightedText);"
|
||||
"}"
|
||||
);
|
||||
|
||||
connect(m_popup, &QListWidget::itemActivated,
|
||||
this, &VariableCompleter::onItemActivated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nach jedem Tastendruck aufrufen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::handleKeyPress(QKeyEvent *event)
|
||||
{
|
||||
// Popup-Navigation
|
||||
if (m_popup->isVisible())
|
||||
{
|
||||
if (event->key() == Qt::Key_Escape)
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
{
|
||||
if (m_popup->currentItem())
|
||||
{
|
||||
onItemActivated(m_popup->currentItem());
|
||||
}
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Down)
|
||||
{
|
||||
const int next = qMin(m_popup->currentRow() + 1,
|
||||
m_popup->count() - 1);
|
||||
m_popup->setCurrentRow(next);
|
||||
return;
|
||||
}
|
||||
if (event->key() == Qt::Key_Up)
|
||||
{
|
||||
const int prev = qMax(m_popup->currentRow() - 1, 0);
|
||||
m_popup->setCurrentRow(prev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Nach dem normalen Tastendruck Popup aktualisieren
|
||||
// (der Event wurde bereits an QPlainTextEdit weitergegeben)
|
||||
updatePopup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Popup aktualisieren
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::updatePopup()
|
||||
{
|
||||
const QString prefix = currentPrefix();
|
||||
|
||||
// Nur bei $ und mindestens einem weiteren Zeichen anzeigen
|
||||
if (prefix.length() < 2 || !prefix.startsWith('$'))
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList allVars = collectVariables();
|
||||
const QString filter = prefix.toLower();
|
||||
|
||||
QStringList matches;
|
||||
for (const QString &var : allVars)
|
||||
{
|
||||
if (var.toLower().startsWith(filter) && var != prefix)
|
||||
{
|
||||
matches.append(var);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.isEmpty())
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Popup befüllen
|
||||
m_popup->clear();
|
||||
for (const QString &var : matches)
|
||||
{
|
||||
m_popup->addItem(var);
|
||||
}
|
||||
m_popup->setCurrentRow(0);
|
||||
|
||||
// Größe anpassen
|
||||
const int itemHeight = m_popup->sizeHintForRow(0) + 2;
|
||||
const int height = qMin(matches.size() * itemHeight + 4, 200);
|
||||
m_popup->setFixedHeight(height);
|
||||
|
||||
// Position unter dem Cursor
|
||||
const QRect cursorRect = m_editor->cursorRect();
|
||||
QPoint pos = m_editor->viewport()->mapToGlobal(
|
||||
QPoint(cursorRect.left(), cursorRect.bottom() + 2)
|
||||
);
|
||||
|
||||
// Nicht aus dem Bildschirm herausragen
|
||||
const QRect screen = QApplication::primaryScreen()->availableGeometry();
|
||||
if (pos.x() + m_popup->width() > screen.right())
|
||||
{
|
||||
pos.setX(screen.right() - m_popup->width());
|
||||
}
|
||||
if (pos.y() + height > screen.bottom())
|
||||
{
|
||||
pos.setY(cursorRect.top() - height - 2);
|
||||
}
|
||||
|
||||
m_popup->move(pos);
|
||||
m_popup->show();
|
||||
m_popup->raise();
|
||||
}
|
||||
|
||||
void VariableCompleter::hidePopup()
|
||||
{
|
||||
m_popup->hide();
|
||||
m_popup->clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::notifyFocusLost()
|
||||
{
|
||||
hidePopup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Variablen im aktuellen Dokument sammeln
|
||||
// ---------------------------------------------------------------------------
|
||||
QStringList VariableCompleter::collectVariables() const
|
||||
{
|
||||
static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)");
|
||||
|
||||
QSet<QString> seen;
|
||||
QStringList result;
|
||||
|
||||
const QString text = m_editor->toPlainText();
|
||||
QRegularExpressionMatchIterator it = varRegex.globalMatch(text);
|
||||
|
||||
while (it.hasNext())
|
||||
{
|
||||
const QString var = it.next().captured(0);
|
||||
if (!seen.contains(var))
|
||||
{
|
||||
seen.insert(var);
|
||||
result.append(var);
|
||||
}
|
||||
}
|
||||
|
||||
result.sort(Qt::CaseInsensitive);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Text links vom Cursor — das angefangene $variable
|
||||
// ---------------------------------------------------------------------------
|
||||
QString VariableCompleter::currentPrefix() const
|
||||
{
|
||||
const QTextCursor cursor = m_editor->textCursor();
|
||||
const QString line = cursor.block().text();
|
||||
const int col = cursor.columnNumber();
|
||||
|
||||
if (col == 0)
|
||||
{
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Rückwärts gehen bis zum $ oder einem Nicht-Bezeichner-Zeichen
|
||||
int start = col - 1;
|
||||
while (start > 0)
|
||||
{
|
||||
const QChar c = line[start - 1];
|
||||
if (!c.isLetterOrNumber() && c != '_' && c != '$')
|
||||
{
|
||||
break;
|
||||
}
|
||||
--start;
|
||||
}
|
||||
|
||||
const QString token = line.mid(start, col - start);
|
||||
return token.startsWith('$') ? token : QString();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Klick oder Enter — Variable einfügen
|
||||
// ---------------------------------------------------------------------------
|
||||
void VariableCompleter::onItemActivated(QListWidgetItem *item)
|
||||
{
|
||||
if (!item)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const QString selected = item->text();
|
||||
const QString prefix = currentPrefix();
|
||||
|
||||
if (prefix.isEmpty())
|
||||
{
|
||||
hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefix durch den vollständigen Variablennamen ersetzen
|
||||
QTextCursor cursor = m_editor->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left,
|
||||
QTextCursor::KeepAnchor,
|
||||
prefix.length());
|
||||
cursor.insertText(selected);
|
||||
|
||||
m_editor->setTextCursor(cursor);
|
||||
hidePopup();
|
||||
}
|
||||
43
barecode/src/editor/VariableCompleter.h
Normal file
43
barecode/src/editor/VariableCompleter.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QListWidget>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
|
||||
class CodeEditor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VariableCompleter – Zeigt ein Popup mit passenden Variablennamen
|
||||
// wenn der Nutzer $ tippt und weiter eingibt.
|
||||
//
|
||||
// Kein Autocomplete — nur Anzeige. Klick oder Enter übernimmt.
|
||||
// Escape oder Weiterschreiben ohne Treffer schließt das Popup.
|
||||
// ---------------------------------------------------------------------------
|
||||
class VariableCompleter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VariableCompleter(CodeEditor *editor);
|
||||
|
||||
// Muss nach jedem Tastendruck aufgerufen werden
|
||||
void handleKeyPress(QKeyEvent *event);
|
||||
|
||||
// Muss aufgerufen werden, wenn der Editor den Fokus verliert
|
||||
// (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup,
|
||||
// damit es nicht als "Geisterfenster" stehen bleibt.
|
||||
void notifyFocusLost();
|
||||
|
||||
private slots:
|
||||
void onItemActivated(QListWidgetItem *item);
|
||||
|
||||
private:
|
||||
void updatePopup();
|
||||
void hidePopup();
|
||||
QStringList collectVariables() const;
|
||||
QString currentPrefix() const; // "$me" etc. links vom Cursor
|
||||
|
||||
CodeEditor *m_editor = nullptr;
|
||||
QListWidget *m_popup = nullptr;
|
||||
};
|
||||
Reference in New Issue
Block a user