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"