Files
BareCode/src/editor/CodeEditor.cpp

572 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "CodeEditor.h"
#include "LineNumberArea.h"
#include "core/Settings.h"
#include "highlighter/HighlighterFactory.h"
#include <QPainter>
#include <QTextBlock>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#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);
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);
// Danach die Einrück-Führungslinien darüber legen
const int tabSize = m_settings->tabSize();
if (tabSize <= 0)
{
return;
}
QPainter painter(viewport());
// Farbe: subtil, passt zu Hell- und Dunkeltheme
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)
{
return;
}
// X-Startposition des Textes direkt aus dem Layout des ersten Blocks holen.
// Das ist der einzige zuverlässige Weg — Qt berücksichtigt intern Margins,
// Gutter und Frame-Abstände die sich nicht sauber manuell nachrechnen lassen.
int textOriginX = 0;
{
QTextBlock firstBlock = firstVisibleBlock();
if (!firstBlock.isValid())
{
firstBlock = document()->begin();
}
if (firstBlock.isValid())
{
const QRectF blockRect = blockBoundingGeometry(firstBlock)
.translated(contentOffset());
// Position des ersten Zeichens im Layout
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());
}
}
}
// Horizontalen Scroll-Offset berücksichtigen
const int scrollX = horizontalScrollBar()->value();
// Sichtbaren Zeilenbereich bestimmen
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();
// Einrückungstiefe der Zeile zählen (Leerzeichen / Tabs)
int indentSpaces = 0;
for (const QChar &ch : text)
{
if (ch == ' ')
{
++indentSpaces;
}
else if (ch == '\t')
{
// Tab auffüllen auf nächsten Tab-Stop
indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize;
}
else
{
break;
}
}
const int indentStops = indentSpaces / tabSize;
// Für jeden Einrückungslevel eine vertikale Linie zeichnen
for (int stop = 1; stop <= indentStops; ++stop)
{
const int xPixel = textOriginX + stop * tabPixels - scrollX;
// Nur im sichtbaren Bereich zeichnen
if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width())
{
continue;
}
const int y1 = static_cast<int>(blockRect.top());
const int y2 = static_cast<int>(blockRect.bottom());
painter.drawLine(xPixel, y1, xPixel, y2);
}
}
block = block.next();
}
}
void CodeEditor::resizeEvent(QResizeEvent *event)
{
QPlainTextEdit::resizeEvent(event);
const QRect cr = contentsRect();
m_lineNumberArea->setGeometry(
QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())
);
}
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(m_lineNumberArea);
// Background
const QColor bgColor = palette().color(QPalette::Window).darker(110);
painter.fillRect(event->rect(), bgColor);
const QColor lineNumColor = palette().color(QPalette::Mid);
const QColor activeColor = palette().color(QPalette::Text);
const int currentLine = textCursor().blockNumber();
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = static_cast<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + static_cast<int>(blockBoundingRect(block).height());
while (block.isValid() && top <= event->rect().bottom())
{
if (block.isVisible() && bottom >= event->rect().top())
{
const QString number = QString::number(blockNumber + 1);
painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor);
painter.drawText(
0,
top,
m_lineNumberArea->width() - 4,
fontMetrics().height(),
Qt::AlignRight,
number
);
}
block = block.next();
top = bottom;
bottom = top + static_cast<int>(blockBoundingRect(block).height());
++blockNumber;
}
}
// ---------------------------------------------------------------------------
// Current line highlight
// ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly())
{
QTextEdit::ExtraSelection selection;
const QColor lineColor = palette().color(QPalette::AlternateBase);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
// ---------------------------------------------------------------------------
// Key handling Tab/Shift+Tab, Smart-Backspace, Auto-Indent
// ---------------------------------------------------------------------------
void CodeEditor::keyPressEvent(QKeyEvent *event)
{
const int tabSize = m_settings->tabSize();
// -----------------------------------------------------------------------
// Shift+Tab: Einrückung zurückziehen
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Backtab ||
(event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier))
{
QTextCursor cursor = textCursor();
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{
const QString lineText = b.text();
int toRemove = 0;
if (m_settings->useSpacesForTabs())
{
// Bis zu tabSize führende Leerzeichen entfernen
for (int i = 0; i < tabSize && i < lineText.length(); ++i)
{
if (lineText[i] == ' ')
{
++toRemove;
}
else
{
break;
}
}
}
else
{
// Einen führenden Tab entfernen
if (!lineText.isEmpty() && lineText[0] == '\t')
{
toRemove = 1;
}
}
if (toRemove > 0)
{
QTextCursor lineCursor(b);
lineCursor.movePosition(QTextCursor::StartOfBlock);
lineCursor.movePosition(QTextCursor::Right,
QTextCursor::KeepAnchor,
toRemove);
lineCursor.removeSelectedText();
}
}
cursor.endEditBlock();
return;
}
// -----------------------------------------------------------------------
// Tab: Einrücken (Leerzeichen oder echter Tab)
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Tab)
{
QTextCursor cursor = textCursor();
if (cursor.hasSelection())
{
// Mehrere Zeilen einrücken
QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd());
cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{
QTextCursor lineCursor(b);
lineCursor.movePosition(QTextCursor::StartOfBlock);
if (m_settings->useSpacesForTabs())
{
lineCursor.insertText(QString(tabSize, ' '));
}
else
{
lineCursor.insertText("\t");
}
}
cursor.endEditBlock();
}
else
{
if (m_settings->useSpacesForTabs())
{
// Zum nächsten Tab-Stop auffüllen
const int col = cursor.columnNumber();
const int spacesNeeded = tabSize - (col % tabSize);
cursor.insertText(QString(spacesNeeded, ' '));
}
else
{
cursor.insertText("\t");
}
}
return;
}
// -----------------------------------------------------------------------
// Smart Backspace: springt zur vorherigen Einrückungsstufe
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Backspace
&& !textCursor().hasSelection()
&& m_settings->useSpacesForTabs())
{
QTextCursor cursor = textCursor();
const int col = cursor.columnNumber();
if (col > 0)
{
// Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen
const QString lineText = cursor.block().text();
const QString leftOfCursor = lineText.left(col);
const bool onlySpaces = leftOfCursor.trimmed().isEmpty();
if (onlySpaces && col > 0)
{
// Zur vorherigen Tab-Stop-Position springen
const int targetCol = ((col - 1) / tabSize) * tabSize;
const int toDelete = col - targetCol;
cursor.movePosition(QTextCursor::Left,
QTextCursor::KeepAnchor,
toDelete);
cursor.removeSelectedText();
return;
}
}
}
// -----------------------------------------------------------------------
// Enter / Return: Auto-Indent
// -----------------------------------------------------------------------
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
{
QTextCursor cursor = textCursor();
const QString currentLine = cursor.block().text();
// Führende Leerzeichen der aktuellen Zeile zählen
int leadingSpaces = 0;
for (const QChar &ch : currentLine)
{
if (ch == ' ')
{
++leadingSpaces;
}
else if (ch == '\t')
{
leadingSpaces += tabSize;
}
else
{
break;
}
}
QPlainTextEdit::keyPressEvent(event);
if (leadingSpaces > 0)
{
const QString indent = m_settings->useSpacesForTabs()
? QString(leadingSpaces, ' ')
: QString(leadingSpaces / tabSize, '\t');
textCursor().insertText(indent);
}
return;
}
QPlainTextEdit::keyPressEvent(event);
}