File indexing completed on 2024-05-05 04:01:43

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "codeeditor.h"
0008 
0009 #include <KSyntaxHighlighting/Definition>
0010 #include <KSyntaxHighlighting/FoldingRegion>
0011 #include <KSyntaxHighlighting/SyntaxHighlighter>
0012 #include <KSyntaxHighlighting/Theme>
0013 
0014 #include <QActionGroup>
0015 #include <QApplication>
0016 #include <QDebug>
0017 #include <QFile>
0018 #include <QFileDialog>
0019 #include <QFontDatabase>
0020 #include <QMenu>
0021 #include <QPainter>
0022 #include <QPalette>
0023 
0024 class CodeEditorSidebar : public QWidget
0025 {
0026     Q_OBJECT
0027 public:
0028     explicit CodeEditorSidebar(CodeEditor *editor);
0029     QSize sizeHint() const override;
0030 
0031 protected:
0032     void paintEvent(QPaintEvent *event) override;
0033     void mouseReleaseEvent(QMouseEvent *event) override;
0034 
0035 private:
0036     CodeEditor *m_codeEditor;
0037 };
0038 
0039 CodeEditorSidebar::CodeEditorSidebar(CodeEditor *editor)
0040     : QWidget(editor)
0041     , m_codeEditor(editor)
0042 {
0043 }
0044 
0045 QSize CodeEditorSidebar::sizeHint() const
0046 {
0047     return QSize(m_codeEditor->sidebarWidth(), 0);
0048 }
0049 
0050 void CodeEditorSidebar::paintEvent(QPaintEvent *event)
0051 {
0052     m_codeEditor->sidebarPaintEvent(event);
0053 }
0054 
0055 void CodeEditorSidebar::mouseReleaseEvent(QMouseEvent *event)
0056 {
0057     if (event->pos().x() >= width() - m_codeEditor->fontMetrics().lineSpacing()) {
0058         auto block = m_codeEditor->blockAtPosition(event->pos().y());
0059         if (!block.isValid() || !m_codeEditor->isFoldable(block)) {
0060             return;
0061         }
0062         m_codeEditor->toggleFold(block);
0063     }
0064     QWidget::mouseReleaseEvent(event);
0065 }
0066 
0067 CodeEditor::CodeEditor(QWidget *parent)
0068     : QPlainTextEdit(parent)
0069     , m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(document()))
0070     , m_sideBar(new CodeEditorSidebar(this))
0071 {
0072     setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0073 
0074     setTheme((palette().color(QPalette::Base).lightness() < 128) ? m_repository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme)
0075                                                                  : m_repository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme));
0076 
0077     connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateSidebarGeometry);
0078     connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateSidebarArea);
0079     connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine);
0080 
0081     updateSidebarGeometry();
0082     highlightCurrentLine();
0083 }
0084 
0085 CodeEditor::~CodeEditor()
0086 {
0087 }
0088 
0089 void CodeEditor::openFile(const QString &fileName)
0090 {
0091     QFile f(fileName);
0092     if (!f.open(QFile::ReadOnly)) {
0093         qWarning() << "Failed to open" << fileName << ":" << f.errorString();
0094         return;
0095     }
0096 
0097     clear();
0098 
0099     const auto def = m_repository.definitionForFileName(fileName);
0100     m_highlighter->setDefinition(def);
0101 
0102     setWindowTitle(fileName);
0103     setPlainText(QString::fromUtf8(f.readAll()));
0104 }
0105 
0106 void CodeEditor::contextMenuEvent(QContextMenuEvent *event)
0107 {
0108     auto menu = createStandardContextMenu(event->pos());
0109     menu->addSeparator();
0110     auto openAction = menu->addAction(QStringLiteral("Open File..."));
0111     connect(openAction, &QAction::triggered, this, [this]() {
0112         const auto fileName = QFileDialog::getOpenFileName(this, QStringLiteral("Open File"));
0113         if (!fileName.isEmpty()) {
0114             openFile(fileName);
0115         }
0116     });
0117 
0118     // syntax selection
0119     auto hlActionGroup = new QActionGroup(menu);
0120     hlActionGroup->setExclusive(true);
0121     auto hlGroupMenu = menu->addMenu(QStringLiteral("Syntax"));
0122     QMenu *hlSubMenu = hlGroupMenu;
0123     QString currentGroup;
0124     for (const auto &def : m_repository.definitions()) {
0125         if (def.isHidden()) {
0126             continue;
0127         }
0128         if (currentGroup != def.section()) {
0129             currentGroup = def.section();
0130             hlSubMenu = hlGroupMenu->addMenu(def.translatedSection());
0131         }
0132 
0133         Q_ASSERT(hlSubMenu);
0134         auto action = hlSubMenu->addAction(def.translatedName());
0135         action->setCheckable(true);
0136         action->setData(def.name());
0137         hlActionGroup->addAction(action);
0138         if (def.name() == m_highlighter->definition().name()) {
0139             action->setChecked(true);
0140         }
0141     }
0142     connect(hlActionGroup, &QActionGroup::triggered, this, [this](QAction *action) {
0143         const auto defName = action->data().toString();
0144         const auto def = m_repository.definitionForName(defName);
0145         m_highlighter->setDefinition(def);
0146     });
0147 
0148     // theme selection
0149     auto themeGroup = new QActionGroup(menu);
0150     themeGroup->setExclusive(true);
0151     auto themeMenu = menu->addMenu(QStringLiteral("Theme"));
0152     for (const auto &theme : m_repository.themes()) {
0153         auto action = themeMenu->addAction(theme.translatedName());
0154         action->setCheckable(true);
0155         action->setData(theme.name());
0156         themeGroup->addAction(action);
0157         if (theme.name() == m_highlighter->theme().name()) {
0158             action->setChecked(true);
0159         }
0160     }
0161     connect(themeGroup, &QActionGroup::triggered, this, [this](QAction *action) {
0162         const auto themeName = action->data().toString();
0163         const auto theme = m_repository.theme(themeName);
0164         setTheme(theme);
0165     });
0166 
0167     menu->exec(event->globalPos());
0168     delete menu;
0169 }
0170 
0171 void CodeEditor::resizeEvent(QResizeEvent *event)
0172 {
0173     QPlainTextEdit::resizeEvent(event);
0174     updateSidebarGeometry();
0175 }
0176 
0177 void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme)
0178 {
0179     auto pal = qApp->palette();
0180     if (theme.isValid()) {
0181         pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
0182         pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection));
0183     }
0184     setPalette(pal);
0185 
0186     m_highlighter->setTheme(theme);
0187     m_highlighter->rehighlight();
0188     highlightCurrentLine();
0189 }
0190 
0191 int CodeEditor::sidebarWidth() const
0192 {
0193     int digits = 1;
0194     auto count = blockCount();
0195     while (count >= 10) {
0196         ++digits;
0197         count /= 10;
0198     }
0199     return 4 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits + fontMetrics().lineSpacing();
0200 }
0201 
0202 void CodeEditor::sidebarPaintEvent(QPaintEvent *event)
0203 {
0204     QPainter painter(m_sideBar);
0205     painter.fillRect(event->rect(), m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::IconBorder));
0206 
0207     auto block = firstVisibleBlock();
0208     auto blockNumber = block.blockNumber();
0209     int top = blockBoundingGeometry(block).translated(contentOffset()).top();
0210     int bottom = top + blockBoundingRect(block).height();
0211     const int currentBlockNumber = textCursor().blockNumber();
0212 
0213     const auto foldingMarkerSize = fontMetrics().lineSpacing();
0214 
0215     while (block.isValid() && top <= event->rect().bottom()) {
0216         if (block.isVisible() && bottom >= event->rect().top()) {
0217             const auto number = QString::number(blockNumber + 1);
0218             painter.setPen(m_highlighter->theme().editorColor((blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber
0219                                                                                                   : KSyntaxHighlighting::Theme::LineNumbers));
0220             painter.drawText(0, top, m_sideBar->width() - 2 - foldingMarkerSize, fontMetrics().height(), Qt::AlignRight, number);
0221         }
0222 
0223         // folding marker
0224         if (block.isVisible() && isFoldable(block)) {
0225             QPolygonF polygon;
0226             if (isFolded(block)) {
0227                 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25);
0228                 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75);
0229                 polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5);
0230             } else {
0231                 polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4);
0232                 polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4);
0233                 polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8);
0234             }
0235             painter.save();
0236             painter.setRenderHint(QPainter::Antialiasing);
0237             painter.setPen(Qt::NoPen);
0238             painter.setBrush(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CodeFolding)));
0239             painter.translate(m_sideBar->width() - foldingMarkerSize, top);
0240             painter.drawPolygon(polygon);
0241             painter.restore();
0242         }
0243 
0244         block = block.next();
0245         top = bottom;
0246         bottom = top + blockBoundingRect(block).height();
0247         ++blockNumber;
0248     }
0249 }
0250 
0251 void CodeEditor::updateSidebarGeometry()
0252 {
0253     setViewportMargins(sidebarWidth(), 0, 0, 0);
0254     const auto r = contentsRect();
0255     m_sideBar->setGeometry(QRect(r.left(), r.top(), sidebarWidth(), r.height()));
0256 }
0257 
0258 void CodeEditor::updateSidebarArea(const QRect &rect, int dy)
0259 {
0260     if (dy) {
0261         m_sideBar->scroll(0, dy);
0262     } else {
0263         m_sideBar->update(0, rect.y(), m_sideBar->width(), rect.height());
0264     }
0265 }
0266 
0267 void CodeEditor::highlightCurrentLine()
0268 {
0269     QTextEdit::ExtraSelection selection;
0270     selection.format.setBackground(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CurrentLine)));
0271     selection.format.setProperty(QTextFormat::FullWidthSelection, true);
0272     selection.cursor = textCursor();
0273     selection.cursor.clearSelection();
0274 
0275     QList<QTextEdit::ExtraSelection> extraSelections;
0276     extraSelections.append(selection);
0277     setExtraSelections(extraSelections);
0278 }
0279 
0280 QTextBlock CodeEditor::blockAtPosition(int y) const
0281 {
0282     auto block = firstVisibleBlock();
0283     if (!block.isValid()) {
0284         return QTextBlock();
0285     }
0286 
0287     int top = blockBoundingGeometry(block).translated(contentOffset()).top();
0288     int bottom = top + blockBoundingRect(block).height();
0289     do {
0290         if (top <= y && y <= bottom) {
0291             return block;
0292         }
0293         block = block.next();
0294         top = bottom;
0295         bottom = top + blockBoundingRect(block).height();
0296     } while (block.isValid());
0297     return QTextBlock();
0298 }
0299 
0300 bool CodeEditor::isFoldable(const QTextBlock &block) const
0301 {
0302     return m_highlighter->startsFoldingRegion(block);
0303 }
0304 
0305 bool CodeEditor::isFolded(const QTextBlock &block) const
0306 {
0307     if (!block.isValid()) {
0308         return false;
0309     }
0310     const auto nextBlock = block.next();
0311     if (!nextBlock.isValid()) {
0312         return false;
0313     }
0314     return !nextBlock.isVisible();
0315 }
0316 
0317 void CodeEditor::toggleFold(const QTextBlock &startBlock)
0318 {
0319     // we also want to fold the last line of the region, therefore the ".next()"
0320     const auto endBlock = m_highlighter->findFoldingRegionEnd(startBlock).next();
0321 
0322     if (isFolded(startBlock)) {
0323         // unfold
0324         auto block = startBlock.next();
0325         while (block.isValid() && !block.isVisible()) {
0326             block.setVisible(true);
0327             block.setLineCount(block.layout()->lineCount());
0328             block = block.next();
0329         }
0330 
0331     } else {
0332         // fold
0333         auto block = startBlock.next();
0334         while (block.isValid() && block != endBlock) {
0335             block.setVisible(false);
0336             block.setLineCount(0);
0337             block = block.next();
0338         }
0339     }
0340 
0341     // redraw document
0342     document()->markContentsDirty(startBlock.position(), endBlock.position() - startBlock.position() + 1);
0343 
0344     // update scrollbars
0345     Q_EMIT document()->documentLayout()->documentSizeChanged(document()->documentLayout()->documentSize());
0346 }
0347 
0348 #include "codeeditor.moc"
0349 #include "moc_codeeditor.cpp"