Skip to content

Instantly share code, notes, and snippets.

@andr1972
Created March 9, 2019 19:08
Show Gist options
  • Save andr1972/e1fab5778648fee5099b7ce16abccc7e to your computer and use it in GitHub Desktop.
Save andr1972/e1fab5778648fee5099b7ce16abccc7e to your computer and use it in GitHub Desktop.
No rule to make target 'debug/codeeditor.moc
#include <QApplication>
#include <QDebug>
#include <QFile>
#include <QTextBlock>
#include <QFileDialog>
#include <QFontDatabase>
#include <QMenu>
#include <QPainter>
#include <QPalette>
#include <QRegExp>
#include <QMessageBox>
#include <QTextCodec>
#include <QDateTime>
#include <QInputDialog>
#include <QLineEdit>
#include <definition.h>
#include <foldingregion.h>
#include <syntaxhighlighter.h>
#include <theme.h>
#include "codeeditor.h"
#include "codeeditorsidebar.h"
#include "een.h"
#include "tabcontrolable.h"
using namespace qedytor;
CodeEditor::CodeEditor(syntaxhl::Repository *repository,QString name, QWidget *parent): QPlainTextEdit(parent),
m_repositoryRef(repository),
highlighter(new syntaxhl::SyntaxHighlighter(document())),
m_sideBar(new CodeEditorSidebar(this))
{
this->fileName = name;
setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
setTheme((palette().color(QPalette::Base).lightness() < 128)
? m_repositoryRef->defaultTheme(syntaxhl::Repository::DarkTheme)
: m_repositoryRef->defaultTheme(syntaxhl::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();
installEventFilter(this);
}
CodeEditor::~CodeEditor()
{
}
int CodeEditor::countNonAscii(QByteArray &content)
{
int count = 0;
for (int i=0; i<content.size(); i++)
if (content.at(i)>127)
count++;
return count;
}
void CodeEditor::openFile(const QString& fileName)
{
clear();
const auto def = m_repositoryRef->definitionForFileName(fileName);
//if (def.foldingEnabled())
highlighter->setDefinition(def);
QFile f(fileName);
if (!f.open(QFile::ReadOnly)) {
qWarning() << "Failed to open" << fileName << ":" << f.errorString();
return;
}
QByteArray content = f.readAll();
QRegExp rx("*.een");
rx.setPatternSyntax(QRegExp::Wildcard);
if (rx.exactMatch(fileName))
{
Een een;
QString password = QInputDialog::getText(this, "Crypted", "Get password", QLineEdit::Password);
content = een.decrypt(content, password.toStdString());
}
QString string;
QTextCodec *codecBOM = QTextCodec::codecForUtfText(content.mid(0,4), QTextCodec::codecForName("System"));
if (!codecBOM || codecBOM->name()=="System")
{
QTextCodec::ConverterState state;
QTextCodec *codec = QTextCodec::codecForName("UTF-8");
string = codec->toUnicode(content, content.size(), &state);
int count = countNonAscii(content);
if (state.invalidChars > count/10)
{
QTextCodec *codecLocale = QTextCodec::codecForLocale();
if (codecLocale)
{
string = codecLocale->toUnicode(content);
loadUtfKind = UtfKind::locale;
}
else
{
string = "";
loadUtfKind = UtfKind::error;
}
}
else loadUtfKind = UtfKind::detected;
}
else
{
string = codecBOM->toUnicode(content);
loadUtfKind = UtfKind::Bom;
}
saveUtfKind = loadUtfKind;
setPlainText(string);
#ifdef _WIN32
QFont font("Courier New");
#else
QFont font("Bitstream Vera Sans Mono");
#endif
font.setPointSize(10);
setFont(font);
}
void CodeEditor::saveFile(const QString& fileName)
{
QTextCodec *codec;
if (saveUtfKind==UtfKind::locale)
{
codec = QTextCodec::codecForLocale();
}
else {
codec = QTextCodec::codecForName("UTF-8");
}
QTextCodec::ConverterState state;
QString text = toPlainText();
QByteArray content = codec->fromUnicode(text);
if (state.invalidChars>0)
{
QMessageBox::warning(nullptr, "Warning","Can't convert from Unicode",
QMessageBox::Ok);
return;
}
if (saveUtfKind==UtfKind::locale && loadUtfKind!=UtfKind::locale)
{
QString againUTF = codec->toUnicode(content);
if (againUTF!=text)
{
QMessageBox::warning(nullptr, "Warning","Can't convert from Unicode",
QMessageBox::Ok);
return;
}
}
QFile f(fileName);
if (!f.open(QFile::WriteOnly | QIODevice::Text)) {
qWarning() << "Failed to open" << fileName << ":" << f.errorString();
return;
}
if (saveUtfKind==UtfKind::Bom)
{
QByteArray bom = QByteArrayLiteral("\xef\xbb\xbf");
f.write(bom);
}
f.write(content);
loadUtfKind = saveUtfKind;
}
void CodeEditor::contextMenuEvent(QContextMenuEvent *event)
{
QMenu *menu = new QMenu(this);
QAction *action;
action = new QAction(tr("Word wrap"), this);
action->setCheckable(true);
action->setChecked(wordWrapMode()!=QTextOption::NoWrap);
connect(action, &QAction::changed, this,
[this,action]() {
action->isChecked()?
this->setWordWrapMode(QTextOption::WordWrap):
this->setWordWrapMode(QTextOption::NoWrap);
});
action->setEnabled(true);
menu->addAction(action);
action = new QAction(tr("Line numbers"), this);
action->setCheckable(true);
action->setChecked(showLineNumbers);
connect(action, &QAction::changed, this,
[this,action]() {
showLineNumbers = action->isChecked();
updateSidebarGeometry();
});
action->setEnabled(true);
menu->addAction(action);
QAction *actionUtf = new QAction(tr("Save as UTF"), this);
actionUtf->setCheckable(true);
actionUtf->setChecked(saveUtfKind!=UtfKind::locale);
actionUtf->setEnabled(true);
menu->addAction(actionUtf);
QAction *actionBOM = new QAction(tr("Save UTF with BOM"), this);
actionBOM->setCheckable(true);
actionBOM->setChecked(saveUtfKind==UtfKind::Bom);
actionBOM->setEnabled(true);
menu->addAction(actionBOM);
connect(actionUtf, &QAction::changed, this,
[this ,actionUtf, actionBOM]() {
if (actionUtf->isChecked())
{
if (actionBOM->isChecked())
saveUtfKind = UtfKind::Bom;
else
saveUtfKind = UtfKind::detected;
}
else
saveUtfKind = UtfKind::locale;
});
connect(actionBOM, &QAction::changed, this,
[this ,actionUtf, actionBOM]() {
if (actionUtf->isChecked())
{
if (actionBOM->isChecked())
saveUtfKind = UtfKind::Bom;
else
saveUtfKind = UtfKind::detected;
}
else
saveUtfKind = UtfKind::locale;
});
action = new QAction(tr("&Undo"), this);
action->setShortcut(QKeySequence(QKeySequence::Undo));
connect(action, SIGNAL(triggered()), this, SLOT(undo()));
action->setEnabled(document()->isUndoAvailable());
menu->addAction(action);
action = new QAction(tr("&Redo"), this);
action->setShortcut(QKeySequence(QKeySequence::Redo));
connect(action, SIGNAL(triggered()), this, SLOT(redo()));
action->setEnabled(document()->isRedoAvailable());
menu->addAction(action);
QMenu *selectionMenu = menu->addMenu(tr("Selection"));
action = new QAction(tr("&Copy"), this);
action->setShortcut(QKeySequence(QKeySequence::Copy));
connect(action, SIGNAL(triggered()), this, SLOT(copy()));
action->setEnabled(textCursor().hasSelection());
selectionMenu->addAction(action);
action = new QAction(tr("&Paste"), this);
action->setShortcut(QKeySequence(QKeySequence::Paste));
connect(action, SIGNAL(triggered()), this, SLOT(paste()));
action->setEnabled(canPaste());
selectionMenu->addAction(action);
action = new QAction(tr("&Delete"), this);
action->setShortcut(QKeySequence(QKeySequence::Delete));
connect(action, &QAction::triggered, this,
[this]() {
this->insertPlainText(""); //tricky because not delete slot
});
action->setEnabled(textCursor().hasSelection());
selectionMenu->addAction(action);
selectionMenu->addSeparator();
action = new QAction(tr("Select &All"), this);
action->setShortcut(QKeySequence(QKeySequence::SelectAll));
connect(action, SIGNAL(triggered()), this, SLOT(selectAll()));
action->setEnabled(!document()->isEmpty());
selectionMenu->addAction(action);
// 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_repositoryRef->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() == highlighter->definition().name())
action->setChecked(true);
}
connect(hlActionGroup, &QActionGroup::triggered, this, [this](QAction *action) {
const auto defName = action->data().toString();
const auto def = m_repositoryRef->definitionForName(defName);
highlighter->setDefinition(def);
});
// theme selection
auto themeGroup = new QActionGroup(menu);
themeGroup->setExclusive(true);
auto themeMenu = menu->addMenu(QStringLiteral("Theme"));
foreach (const auto &theme, m_repositoryRef->themes()) {
auto action = themeMenu->addAction(theme.translatedName());
action->setCheckable(true);
action->setData(theme.name());
themeGroup->addAction(action);
if (theme.name() == highlighter->theme().name())
action->setChecked(true);
}
connect(themeGroup, &QActionGroup::triggered, this, [this](QAction *action) {
const auto themeName = action->data().toString();
const auto theme = m_repositoryRef->theme(themeName);
setTheme(theme);
});
menu->exec(event->globalPos());
delete menu;
}
void CodeEditor::resizeEvent(QResizeEvent *event)
{
QPlainTextEdit::resizeEvent(event);
updateSidebarGeometry();
}
void CodeEditor::setTheme(const syntaxhl::Theme &theme)
{
auto pal = qApp->palette();
if (theme.isValid()) {
pal.setColor(QPalette::Base, theme.editorColor(syntaxhl::Theme::BackgroundColor));
pal.setColor(QPalette::Text, theme.textColor(syntaxhl::Theme::Normal));
pal.setColor(QPalette::Highlight, theme.editorColor(syntaxhl::Theme::TextSelection));
}
setPalette(pal);
highlighter->setTheme(theme);
highlighter->rehighlight();
highlightCurrentLine();
}
int CodeEditor::sidebarWidth() const
{
if (showLineNumbers)
{
int digits = 1;
auto count = blockCount();
while (count >= 10) {
++digits;
count /= 10;
}
return 4 + fontMetrics().width(QLatin1Char('9')) * digits + fontMetrics().lineSpacing();
}
else
return fontMetrics().lineSpacing();
}
void CodeEditor::sidebarPaintEvent(QPaintEvent *event)
{
QPainter painter(m_sideBar);
painter.fillRect(event->rect(), highlighter->theme().editorColor(syntaxhl::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(highlighter->theme().editorColor(
(blockNumber == currentBlockNumber) ? syntaxhl::Theme::CurrentLineNumber
: syntaxhl::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(highlighter->theme().editorColor(syntaxhl::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;
QColor lineColor = QColor(highlighter->theme().editorColor(syntaxhl::Theme::CurrentLine));
selection.format.setBackground(lineColor);
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 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 = 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());
}
bool CodeEditor::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key()>=Qt::Key_0 && keyEvent->key()<=Qt::Key_9 && keyEvent->modifiers()==Qt::AltModifier)
{
TabControlable *controlable = dynamic_cast<TabControlable *>(parent()->parent()->parent()->parent());
int n = keyEvent->key()>=Qt::Key_1?keyEvent->key()-Qt::Key_1:9;
controlable->setTab(n);
return true;
}
}
else if (event->type() == QEvent::KeyRelease)
{
}
return false;
}
void CodeEditor::findNext(QString textToFind, QTextDocument::FindFlags flags)
{
QTextCursor cursor(document());
cursor = document()->find(textToFind, textCursor(), flags);
if (cursor.isNull())
QMessageBox::warning(nullptr, "Warning","Can't find ["+textToFind+"]",
QMessageBox::Ok);
else
setTextCursor(cursor);
}
Properties CodeEditor::getProperties()
{
Properties properties;
if (loadUtfKind==UtfKind::Bom)
properties.isUtfBOM = true;
else if (loadUtfKind==UtfKind::detected)
properties.isUtfDetected = true;
else if (loadUtfKind==UtfKind::locale)
properties.isLocale = true;
return properties;
}
void CodeEditor::onTextChanged()
{
QDateTime current = QDateTime::currentDateTime();
lastEditTime = current.toMSecsSinceEpoch() ;
}
#include "codeeditor.moc"
#pragma once
#include <QPlainTextEdit>
#include <QWidget>
#include "repository.h"
#include "properties.h"
#include "codeeditorsidebar.h"
namespace syntaxhl {
class SyntaxHighlighter;
}
namespace qedytor {
class CodeEditorSidebar;
enum class UtfKind {Bom, detected, locale, error};
class CodeEditor : public QPlainTextEdit
{
Q_OBJECT
private:
friend class CodeEditorSidebar;
void setTheme(const syntaxhl::Theme &theme);
int sidebarWidth() const;
void sidebarPaintEvent(QPaintEvent *event);
void updateSidebarGeometry();
void updateSidebarArea(const QRect &rect, int dy);
void highlightCurrentLine();
QTextBlock blockAtPosition(int y) const;
bool isFoldable(const QTextBlock &block) const;
bool isFolded(const QTextBlock &block) const;
void toggleFold(const QTextBlock &block);
static int countNonAscii(QByteArray &content);
syntaxhl::Repository *m_repositoryRef;
CodeEditorSidebar *m_sideBar;
bool eventFilter(QObject *watched, QEvent *event);
protected:
void contextMenuEvent(QContextMenuEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
public slots:
void onTextChanged();
public:
syntaxhl::SyntaxHighlighter *highlighter;
UtfKind loadUtfKind;
UtfKind saveUtfKind;
bool showLineNumbers = false;
explicit CodeEditor(syntaxhl::Repository *repository,
QString fileName, QWidget *parent = nullptr);
~CodeEditor();
QString fileName;
qint64 lastEditTime = 0;
void openFile(const QString &fileName);
void saveFile(const QString& fileName);
void findNext(QString textToFind, QTextDocument::FindFlags flags);
Properties getProperties();
};
}
#include <QTextBlock>
#include "codeeditorsidebar.h"
#include "codeeditor.h"
using namespace qedytor;
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);
}
#pragma once
#include <QWidget>
namespace qedytor {
class CodeEditor;
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;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment