1/*
2 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: MIT
5*/
6
7#include "codeeditor.h"
8
9#include <KSyntaxHighlighting/Definition>
10#include <KSyntaxHighlighting/FoldingRegion>
11#include <KSyntaxHighlighting/SyntaxHighlighter>
12#include <KSyntaxHighlighting/Theme>
13
14#include <QActionGroup>
15#include <QApplication>
16#include <QDebug>
17#include <QFile>
18#include <QFileDialog>
19#include <QFontDatabase>
20#include <QMenu>
21#include <QPainter>
22#include <QPalette>
23
24class CodeEditorSidebar : public QWidget
25{
26 Q_OBJECT
27public:
28 explicit CodeEditorSidebar(CodeEditor *editor);
29 QSize sizeHint() const override;
30
31protected:
32 void paintEvent(QPaintEvent *event) override;
33 void mouseReleaseEvent(QMouseEvent *event) override;
34
35private:
36 CodeEditor *m_codeEditor;
37};
38
39CodeEditorSidebar::CodeEditorSidebar(CodeEditor *editor)
40 : QWidget(editor)
41 , m_codeEditor(editor)
42{
43}
44
45QSize CodeEditorSidebar::sizeHint() const
46{
47 return QSize(m_codeEditor->sidebarWidth(), 0);
48}
49
50void CodeEditorSidebar::paintEvent(QPaintEvent *event)
51{
52 m_codeEditor->sidebarPaintEvent(event);
53}
54
55void CodeEditorSidebar::mouseReleaseEvent(QMouseEvent *event)
56{
57 if (event->pos().x() >= width() - m_codeEditor->fontMetrics().lineSpacing()) {
58 auto block = m_codeEditor->blockAtPosition(y: event->pos().y());
59 if (!block.isValid() || !m_codeEditor->isFoldable(block)) {
60 return;
61 }
62 m_codeEditor->toggleFold(block);
63 }
64 QWidget::mouseReleaseEvent(event);
65}
66
67CodeEditor::CodeEditor(QWidget *parent)
68 : QPlainTextEdit(parent)
69 , m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(document()))
70 , m_sideBar(new CodeEditorSidebar(this))
71{
72 setFont(QFontDatabase::systemFont(type: QFontDatabase::FixedFont));
73
74 setTheme((palette().color(cr: QPalette::Base).lightness() < 128) ? m_repository.defaultTheme(t: KSyntaxHighlighting::Repository::DarkTheme)
75 : m_repository.defaultTheme(t: KSyntaxHighlighting::Repository::LightTheme));
76
77 connect(sender: this, signal: &QPlainTextEdit::blockCountChanged, context: this, slot: &CodeEditor::updateSidebarGeometry);
78 connect(sender: this, signal: &QPlainTextEdit::updateRequest, context: this, slot: &CodeEditor::updateSidebarArea);
79 connect(sender: this, signal: &QPlainTextEdit::cursorPositionChanged, context: this, slot: &CodeEditor::highlightCurrentLine);
80
81 updateSidebarGeometry();
82 highlightCurrentLine();
83}
84
85CodeEditor::~CodeEditor()
86{
87}
88
89void CodeEditor::openFile(const QString &fileName)
90{
91 QFile f(fileName);
92 if (!f.open(flags: QFile::ReadOnly)) {
93 qWarning() << "Failed to open" << fileName << ":" << f.errorString();
94 return;
95 }
96
97 clear();
98
99 const auto def = m_repository.definitionForFileName(fileName);
100 m_highlighter->setDefinition(def);
101
102 setWindowTitle(fileName);
103 setPlainText(QString::fromUtf8(ba: f.readAll()));
104}
105
106void CodeEditor::contextMenuEvent(QContextMenuEvent *event)
107{
108 auto menu = createStandardContextMenu(position: event->pos());
109 menu->addSeparator();
110 auto openAction = menu->addAction(QStringLiteral("Open File..."));
111 connect(sender: openAction, signal: &QAction::triggered, context: this, slot: [this]() {
112 const auto fileName = QFileDialog::getOpenFileName(parent: this, QStringLiteral("Open File"));
113 if (!fileName.isEmpty()) {
114 openFile(fileName);
115 }
116 });
117
118 // syntax selection
119 auto hlActionGroup = new QActionGroup(menu);
120 hlActionGroup->setExclusive(true);
121 auto hlGroupMenu = menu->addMenu(QStringLiteral("Syntax"));
122 QMenu *hlSubMenu = hlGroupMenu;
123 QString currentGroup;
124 for (const auto &def : m_repository.definitions()) {
125 if (def.isHidden()) {
126 continue;
127 }
128 if (currentGroup != def.section()) {
129 currentGroup = def.section();
130 hlSubMenu = hlGroupMenu->addMenu(title: def.translatedSection());
131 }
132
133 Q_ASSERT(hlSubMenu);
134 auto action = hlSubMenu->addAction(text: def.translatedName());
135 action->setCheckable(true);
136 action->setData(def.name());
137 hlActionGroup->addAction(a: action);
138 if (def.name() == m_highlighter->definition().name()) {
139 action->setChecked(true);
140 }
141 }
142 connect(sender: hlActionGroup, signal: &QActionGroup::triggered, context: this, slot: [this](QAction *action) {
143 const auto defName = action->data().toString();
144 const auto def = m_repository.definitionForName(defName);
145 m_highlighter->setDefinition(def);
146 });
147
148 // theme selection
149 auto themeGroup = new QActionGroup(menu);
150 themeGroup->setExclusive(true);
151 auto themeMenu = menu->addMenu(QStringLiteral("Theme"));
152 for (const auto &theme : m_repository.themes()) {
153 auto action = themeMenu->addAction(text: theme.translatedName());
154 action->setCheckable(true);
155 action->setData(theme.name());
156 themeGroup->addAction(a: action);
157 if (theme.name() == m_highlighter->theme().name()) {
158 action->setChecked(true);
159 }
160 }
161 connect(sender: themeGroup, signal: &QActionGroup::triggered, context: this, slot: [this](QAction *action) {
162 const auto themeName = action->data().toString();
163 const auto theme = m_repository.theme(themeName);
164 setTheme(theme);
165 });
166
167 menu->exec(pos: event->globalPos());
168 delete menu;
169}
170
171void CodeEditor::resizeEvent(QResizeEvent *event)
172{
173 QPlainTextEdit::resizeEvent(e: event);
174 updateSidebarGeometry();
175}
176
177void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme)
178{
179 auto pal = qApp->palette();
180 if (theme.isValid()) {
181 pal.setColor(acr: QPalette::Base, acolor: theme.editorColor(role: KSyntaxHighlighting::Theme::BackgroundColor));
182 pal.setColor(acr: QPalette::Highlight, acolor: theme.editorColor(role: KSyntaxHighlighting::Theme::TextSelection));
183 }
184 setPalette(pal);
185
186 m_highlighter->setTheme(theme);
187 m_highlighter->rehighlight();
188 highlightCurrentLine();
189}
190
191int CodeEditor::sidebarWidth() const
192{
193 int digits = 1;
194 auto count = blockCount();
195 while (count >= 10) {
196 ++digits;
197 count /= 10;
198 }
199 return 4 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits + fontMetrics().lineSpacing();
200}
201
202void CodeEditor::sidebarPaintEvent(QPaintEvent *event)
203{
204 QPainter painter(m_sideBar);
205 painter.fillRect(event->rect(), color: m_highlighter->theme().editorColor(role: KSyntaxHighlighting::Theme::IconBorder));
206
207 auto block = firstVisibleBlock();
208 auto blockNumber = block.blockNumber();
209 int top = blockBoundingGeometry(block).translated(p: contentOffset()).top();
210 int bottom = top + blockBoundingRect(block).height();
211 const int currentBlockNumber = textCursor().blockNumber();
212
213 const auto foldingMarkerSize = fontMetrics().lineSpacing();
214
215 while (block.isValid() && top <= event->rect().bottom()) {
216 if (block.isVisible() && bottom >= event->rect().top()) {
217 const auto number = QString::number(blockNumber + 1);
218 painter.setPen(m_highlighter->theme().editorColor(role: (blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber
219 : KSyntaxHighlighting::Theme::LineNumbers));
220 painter.drawText(x: 0, y: top, w: m_sideBar->width() - 2 - foldingMarkerSize, h: fontMetrics().height(), flags: Qt::AlignRight, str: number);
221 }
222
223 // folding marker
224 if (block.isVisible() && isFoldable(block)) {
225 QPolygonF polygon;
226 if (isFolded(block)) {
227 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25);
228 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75);
229 polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5);
230 } else {
231 polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4);
232 polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4);
233 polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8);
234 }
235 painter.save();
236 painter.setRenderHint(hint: QPainter::Antialiasing);
237 painter.setPen(Qt::NoPen);
238 painter.setBrush(QColor(m_highlighter->theme().editorColor(role: KSyntaxHighlighting::Theme::CodeFolding)));
239 painter.translate(dx: m_sideBar->width() - foldingMarkerSize, dy: top);
240 painter.drawPolygon(polygon);
241 painter.restore();
242 }
243
244 block = block.next();
245 top = bottom;
246 bottom = top + blockBoundingRect(block).height();
247 ++blockNumber;
248 }
249}
250
251void CodeEditor::updateSidebarGeometry()
252{
253 setViewportMargins(left: sidebarWidth(), top: 0, right: 0, bottom: 0);
254 const auto r = contentsRect();
255 m_sideBar->setGeometry(QRect(r.left(), r.top(), sidebarWidth(), r.height()));
256}
257
258void CodeEditor::updateSidebarArea(const QRect &rect, int dy)
259{
260 if (dy) {
261 m_sideBar->scroll(dx: 0, dy);
262 } else {
263 m_sideBar->update(ax: 0, ay: rect.y(), aw: m_sideBar->width(), ah: rect.height());
264 }
265}
266
267void CodeEditor::highlightCurrentLine()
268{
269 QTextEdit::ExtraSelection selection;
270 selection.format.setBackground(QColor(m_highlighter->theme().editorColor(role: KSyntaxHighlighting::Theme::CurrentLine)));
271 selection.format.setProperty(propertyId: QTextFormat::FullWidthSelection, value: true);
272 selection.cursor = textCursor();
273 selection.cursor.clearSelection();
274
275 QList<QTextEdit::ExtraSelection> extraSelections;
276 extraSelections.append(t: selection);
277 setExtraSelections(extraSelections);
278}
279
280QTextBlock CodeEditor::blockAtPosition(int y) const
281{
282 auto block = firstVisibleBlock();
283 if (!block.isValid()) {
284 return QTextBlock();
285 }
286
287 int top = blockBoundingGeometry(block).translated(p: contentOffset()).top();
288 int bottom = top + blockBoundingRect(block).height();
289 do {
290 if (top <= y && y <= bottom) {
291 return block;
292 }
293 block = block.next();
294 top = bottom;
295 bottom = top + blockBoundingRect(block).height();
296 } while (block.isValid());
297 return QTextBlock();
298}
299
300bool CodeEditor::isFoldable(const QTextBlock &block) const
301{
302 return m_highlighter->startsFoldingRegion(startBlock: block);
303}
304
305bool CodeEditor::isFolded(const QTextBlock &block) const
306{
307 if (!block.isValid()) {
308 return false;
309 }
310 const auto nextBlock = block.next();
311 if (!nextBlock.isValid()) {
312 return false;
313 }
314 return !nextBlock.isVisible();
315}
316
317void CodeEditor::toggleFold(const QTextBlock &startBlock)
318{
319 // we also want to fold the last line of the region, therefore the ".next()"
320 const auto endBlock = m_highlighter->findFoldingRegionEnd(startBlock).next();
321
322 if (isFolded(block: startBlock)) {
323 // unfold
324 auto block = startBlock.next();
325 while (block.isValid() && !block.isVisible()) {
326 block.setVisible(true);
327 block.setLineCount(block.layout()->lineCount());
328 block = block.next();
329 }
330
331 } else {
332 // fold
333 auto block = startBlock.next();
334 while (block.isValid() && block != endBlock) {
335 block.setVisible(false);
336 block.setLineCount(0);
337 block = block.next();
338 }
339 }
340
341 // redraw document
342 document()->markContentsDirty(from: startBlock.position(), length: endBlock.position() - startBlock.position() + 1);
343
344 // update scrollbars
345 Q_EMIT document()->documentLayout()->documentSizeChanged(newSize: document()->documentLayout()->documentSize());
346}
347
348#include "codeeditor.moc"
349#include "moc_codeeditor.cpp"
350

source code of syntax-highlighting/examples/codeeditor/codeeditor.cpp