#include "CodeEditor.h" #include "LineNumberArea.h" #include "core/Settings.h" #include "highlighter/HighlighterFactory.h" #include #include #include #include #include #include #include #include #include #include #include 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(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(blockRect.left() + layout->lineAt(0).position().x()); } else { textOriginX = static_cast(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(blockRect.top()); const int y2 = static_cast(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(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + static_cast(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(blockBoundingRect(block).height()); ++blockNumber; } } // --------------------------------------------------------------------------- // Current line highlight // --------------------------------------------------------------------------- void CodeEditor::highlightCurrentLine() { QList 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); }