774 lines
23 KiB
C++
774 lines
23 KiB
C++
#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();
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Escape: Signatur-Tooltip sofort schließen (unabhängig vom Cursor-
|
||
// Kontext). Das Variablen-Popup behandelt Escape bereits selbst weiter
|
||
// unten in m_varCompleter->handleKeyPress().
|
||
// -----------------------------------------------------------------------
|
||
if (event->key() == Qt::Key_Escape)
|
||
{
|
||
m_signatureHelper->dismiss();
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 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();
|
||
m_signatureHelper->notifyFocusLost();
|
||
}
|
||
|