forked from qt-creator/qt-creator
Fixes: QTCREATORBUG-21029 Change-Id: I9894c4384e0e47da6bf030b7b8e07c3ad4737ff3 Reviewed-by: Orgad Shaneh <orgads@gmail.com>
359 lines
12 KiB
C++
359 lines
12 KiB
C++
/*
|
|
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
a copy of this software and associated documentation files (the
|
|
"Software"), to deal in the Software without restriction, including
|
|
without limitation the rights to use, copy, modify, merge, publish,
|
|
distribute, sublicense, and/or sell copies of the Software, and to
|
|
permit persons to whom the Software is furnished to do so, subject to
|
|
the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included
|
|
in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include "codeeditor.h"
|
|
|
|
#include <definition.h>
|
|
#include <foldingregion.h>
|
|
#include <syntaxhighlighter.h>
|
|
#include <theme.h>
|
|
|
|
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QFontDatabase>
|
|
#include <QMenu>
|
|
#include <QPainter>
|
|
#include <QPalette>
|
|
|
|
class CodeEditorSidebar : public QWidget
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit CodeEditorSidebar(CodeEditor *editor);
|
|
QSize sizeHint() const Q_DECL_OVERRIDE;
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
|
|
void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
|
|
|
|
private:
|
|
CodeEditor *m_codeEditor;
|
|
};
|
|
|
|
CodeEditorSidebar::CodeEditorSidebar(CodeEditor *editor) :
|
|
QWidget(editor),
|
|
m_codeEditor(editor)
|
|
{
|
|
}
|
|
|
|
QSize CodeEditorSidebar::sizeHint() const
|
|
{
|
|
return QSize(m_codeEditor->sidebarWidth(), 0);
|
|
}
|
|
|
|
void CodeEditorSidebar::paintEvent(QPaintEvent *event)
|
|
{
|
|
m_codeEditor->sidebarPaintEvent(event);
|
|
}
|
|
|
|
void CodeEditorSidebar::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
if (event->x() >= width() - m_codeEditor->fontMetrics().lineSpacing()) {
|
|
auto block = m_codeEditor->blockAtPosition(event->y());
|
|
if (!block.isValid() || !m_codeEditor->isFoldable(block))
|
|
return;
|
|
m_codeEditor->toggleFold(block);
|
|
}
|
|
QWidget::mouseReleaseEvent(event);
|
|
}
|
|
|
|
|
|
CodeEditor::CodeEditor(QWidget *parent) :
|
|
QPlainTextEdit(parent),
|
|
m_highlighter(new KSyntaxHighlighting::SyntaxHighlighter(document())),
|
|
m_sideBar(new CodeEditorSidebar(this))
|
|
{
|
|
setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
|
|
|
setTheme((palette().color(QPalette::Base).lightness() < 128)
|
|
? m_repository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme)
|
|
: m_repository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme));
|
|
|
|
connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateSidebarGeometry);
|
|
connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateSidebarArea);
|
|
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine);
|
|
|
|
updateSidebarGeometry();
|
|
highlightCurrentLine();
|
|
}
|
|
|
|
CodeEditor::~CodeEditor()
|
|
{
|
|
}
|
|
|
|
void CodeEditor::openFile(const QString& fileName)
|
|
{
|
|
QFile f(fileName);
|
|
if (!f.open(QFile::ReadOnly)) {
|
|
qWarning() << "Failed to open" << fileName << ":" << f.errorString();
|
|
return;
|
|
}
|
|
|
|
clear();
|
|
|
|
const auto def = m_repository.definitionForFileName(fileName);
|
|
m_highlighter->setDefinition(def);
|
|
|
|
setWindowTitle(fileName);
|
|
setPlainText(QString::fromUtf8(f.readAll()));
|
|
}
|
|
|
|
void CodeEditor::contextMenuEvent(QContextMenuEvent *event)
|
|
{
|
|
auto menu = createStandardContextMenu(event->pos());
|
|
menu->addSeparator();
|
|
auto openAction = menu->addAction(QStringLiteral("Open File..."));
|
|
connect(openAction, &QAction::triggered, this, [this]() {
|
|
const auto fileName = QFileDialog::getOpenFileName(this, QStringLiteral("Open File"));
|
|
if (!fileName.isEmpty())
|
|
openFile(fileName);
|
|
});
|
|
|
|
// syntax selection
|
|
auto hlActionGroup = new QActionGroup(menu);
|
|
hlActionGroup->setExclusive(true);
|
|
auto hlGroupMenu = menu->addMenu(QStringLiteral("Syntax"));
|
|
QMenu *hlSubMenu = hlGroupMenu;
|
|
QString currentGroup;
|
|
foreach (const auto &def, m_repository.definitions()) {
|
|
if (def.isHidden())
|
|
continue;
|
|
if (currentGroup != def.section()) {
|
|
currentGroup = def.section();
|
|
hlSubMenu = hlGroupMenu->addMenu(def.translatedSection());
|
|
}
|
|
|
|
Q_ASSERT(hlSubMenu);
|
|
auto action = hlSubMenu->addAction(def.translatedName());
|
|
action->setCheckable(true);
|
|
action->setData(def.name());
|
|
hlActionGroup->addAction(action);
|
|
if (def.name() == m_highlighter->definition().name())
|
|
action->setChecked(true);
|
|
}
|
|
connect(hlActionGroup, &QActionGroup::triggered, this, [this](QAction *action) {
|
|
const auto defName = action->data().toString();
|
|
const auto def = m_repository.definitionForName(defName);
|
|
m_highlighter->setDefinition(def);
|
|
});
|
|
|
|
// theme selection
|
|
auto themeGroup = new QActionGroup(menu);
|
|
themeGroup->setExclusive(true);
|
|
auto themeMenu = menu->addMenu(QStringLiteral("Theme"));
|
|
foreach (const auto &theme, m_repository.themes()) {
|
|
auto action = themeMenu->addAction(theme.translatedName());
|
|
action->setCheckable(true);
|
|
action->setData(theme.name());
|
|
themeGroup->addAction(action);
|
|
if (theme.name() == m_highlighter->theme().name())
|
|
action->setChecked(true);
|
|
}
|
|
connect(themeGroup, &QActionGroup::triggered, this, [this](QAction *action) {
|
|
const auto themeName = action->data().toString();
|
|
const auto theme = m_repository.theme(themeName);
|
|
setTheme(theme);
|
|
});
|
|
|
|
menu->exec(event->globalPos());
|
|
delete menu;
|
|
}
|
|
|
|
void CodeEditor::resizeEvent(QResizeEvent *event)
|
|
{
|
|
QPlainTextEdit::resizeEvent(event);
|
|
updateSidebarGeometry();
|
|
}
|
|
|
|
void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme)
|
|
{
|
|
auto pal = qApp->palette();
|
|
if (theme.isValid()) {
|
|
pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
|
|
pal.setColor(QPalette::Text, theme.textColor(KSyntaxHighlighting::Theme::Normal));
|
|
pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection));
|
|
}
|
|
setPalette(pal);
|
|
|
|
m_highlighter->setTheme(theme);
|
|
m_highlighter->rehighlight();
|
|
highlightCurrentLine();
|
|
}
|
|
|
|
int CodeEditor::sidebarWidth() const
|
|
{
|
|
int digits = 1;
|
|
auto count = blockCount();
|
|
while (count >= 10) {
|
|
++digits;
|
|
count /= 10;
|
|
}
|
|
return 4 + fontMetrics().width(QLatin1Char('9')) * digits + fontMetrics().lineSpacing();
|
|
}
|
|
|
|
void CodeEditor::sidebarPaintEvent(QPaintEvent *event)
|
|
{
|
|
QPainter painter(m_sideBar);
|
|
painter.fillRect(event->rect(), m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::IconBorder));
|
|
|
|
auto block = firstVisibleBlock();
|
|
auto blockNumber = block.blockNumber();
|
|
int top = blockBoundingGeometry(block).translated(contentOffset()).top();
|
|
int bottom = top + blockBoundingRect(block).height();
|
|
const int currentBlockNumber = textCursor().blockNumber();
|
|
|
|
const auto foldingMarkerSize = fontMetrics().lineSpacing();
|
|
|
|
while (block.isValid() && top <= event->rect().bottom()) {
|
|
if (block.isVisible() && bottom >= event->rect().top()) {
|
|
const auto number = QString::number(blockNumber + 1);
|
|
painter.setPen(m_highlighter->theme().editorColor(
|
|
(blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber
|
|
: KSyntaxHighlighting::Theme::LineNumbers));
|
|
painter.drawText(0, top, m_sideBar->width() - 2 - foldingMarkerSize, fontMetrics().height(), Qt::AlignRight, number);
|
|
}
|
|
|
|
// folding marker
|
|
if (block.isVisible() && isFoldable(block)) {
|
|
QPolygonF polygon;
|
|
if (isFolded(block)) {
|
|
polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25);
|
|
polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75);
|
|
polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5);
|
|
} else {
|
|
polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4);
|
|
polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4);
|
|
polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8);
|
|
}
|
|
painter.save();
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CodeFolding)));
|
|
painter.translate(m_sideBar->width() - foldingMarkerSize, top);
|
|
painter.drawPolygon(polygon);
|
|
painter.restore();
|
|
}
|
|
|
|
block = block.next();
|
|
top = bottom;
|
|
bottom = top + blockBoundingRect(block).height();
|
|
++blockNumber;
|
|
}
|
|
}
|
|
|
|
void CodeEditor::updateSidebarGeometry()
|
|
{
|
|
setViewportMargins(sidebarWidth(), 0, 0, 0);
|
|
const auto r = contentsRect();
|
|
m_sideBar->setGeometry(QRect(r.left(), r.top(), sidebarWidth(), r.height()));
|
|
}
|
|
|
|
void CodeEditor::updateSidebarArea(const QRect& rect, int dy)
|
|
{
|
|
if (dy)
|
|
m_sideBar->scroll(0, dy);
|
|
else
|
|
m_sideBar->update(0, rect.y(), m_sideBar->width(), rect.height());
|
|
}
|
|
|
|
void CodeEditor::highlightCurrentLine()
|
|
{
|
|
QTextEdit::ExtraSelection selection;
|
|
selection.format.setBackground(QColor(m_highlighter->theme().editorColor(KSyntaxHighlighting::Theme::CurrentLine)));
|
|
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
|
selection.cursor = textCursor();
|
|
selection.cursor.clearSelection();
|
|
|
|
QList<QTextEdit::ExtraSelection> extraSelections;
|
|
extraSelections.append(selection);
|
|
setExtraSelections(extraSelections);
|
|
}
|
|
|
|
QTextBlock CodeEditor::blockAtPosition(int y) const
|
|
{
|
|
auto block = firstVisibleBlock();
|
|
if (!block.isValid())
|
|
return QTextBlock();
|
|
|
|
int top = blockBoundingGeometry(block).translated(contentOffset()).top();
|
|
int bottom = top + blockBoundingRect(block).height();
|
|
do {
|
|
if (top <= y && y <= bottom)
|
|
return block;
|
|
block = block.next();
|
|
top = bottom;
|
|
bottom = top + blockBoundingRect(block).height();
|
|
} while (block.isValid());
|
|
return QTextBlock();
|
|
}
|
|
|
|
bool CodeEditor::isFoldable(const QTextBlock &block) const
|
|
{
|
|
return m_highlighter->startsFoldingRegion(block);
|
|
}
|
|
|
|
bool CodeEditor::isFolded(const QTextBlock &block) const
|
|
{
|
|
if (!block.isValid())
|
|
return false;
|
|
const auto nextBlock = block.next();
|
|
if (!nextBlock.isValid())
|
|
return false;
|
|
return !nextBlock.isVisible();
|
|
}
|
|
|
|
void CodeEditor::toggleFold(const QTextBlock &startBlock)
|
|
{
|
|
// we also want to fold the last line of the region, therefore the ".next()"
|
|
const auto endBlock = m_highlighter->findFoldingRegionEnd(startBlock).next();
|
|
|
|
if (isFolded(startBlock)) {
|
|
// unfold
|
|
auto block = startBlock.next();
|
|
while (block.isValid() && !block.isVisible()) {
|
|
block.setVisible(true);
|
|
block.setLineCount(block.layout()->lineCount());
|
|
block = block.next();
|
|
}
|
|
|
|
} else {
|
|
// fold
|
|
auto block = startBlock.next();
|
|
while (block.isValid() && block != endBlock) {
|
|
block.setVisible(false);
|
|
block.setLineCount(0);
|
|
block = block.next();
|
|
}
|
|
}
|
|
|
|
// redraw document
|
|
document()->markContentsDirty(startBlock.position(), endBlock.position() - startBlock.position() + 1);
|
|
|
|
// update scrollbars
|
|
emit document()->documentLayout()->documentSizeChanged(document()->documentLayout()->documentSize());
|
|
}
|
|
|
|
#include "codeeditor.moc"
|