#include "texteditor.h" #include "utility.h" #include TextEditor::TextEditor(QWidget* parent) : QPlainTextEdit(parent), m_toolbar(nullptr), m_lineNumbers(nullptr), m_highlighter(nullptr), m_dirty(false) { m_toolbar = new TextEditorToolbar(*this); m_lineNumbers = new TextEditorLineNumbers(*this); m_highlighter = new TextEditorHighlighter(document()); setDefaultStyle(); wordWrap(true); emit modified(false); connect( document(), &QTextDocument::modificationChanged, [&](bool b){ onModified(b); }); connect( this, &QPlainTextEdit::cursorPositionChanged, [&]{ highlightCurrentLine(); }); } void TextEditor::setDefaultStyle() { const auto font = QFontDatabase::systemFont(QFontDatabase::FixedFont); setFont(font); m_lineNumbers->setFont(font); QColor textColor(Qt::black); QColor altTextColor(Qt::darkGray); QColor backgroundColor(Qt::white); { auto w = std::make_unique(); if (auto* s=style()) { s->polish(w.get()); } textColor = w->palette().color(QPalette::WindowText); altTextColor = w->palette().color(QPalette::Disabled, QPalette::WindowText); backgroundColor = w->palette().color(QPalette::Window); } setTextColor(textColor); m_lineNumbers->setTextColor(altTextColor); setBackgroundColor(backgroundColor); m_lineNumbers->setBackgroundColor(backgroundColor); setHighlightBackgroundColor(backgroundColor); } bool TextEditor::load(const QString& filename) { m_filename = filename; setPlainText(MOBase::readFileText(filename, &m_encoding)); document()->setModified(false); return true; } bool TextEditor::save() { if (m_filename.isEmpty() || m_encoding.isEmpty()) { return false; } QFile file(m_filename); file.open(QIODevice::WriteOnly); file.resize(0); QTextCodec* codec = QTextCodec::codecForName(m_encoding.toUtf8()); QString data = toPlainText().replace("\n", "\r\n"); file.write(codec->fromUnicode(data)); document()->setModified(false); return true; } const QString& TextEditor::filename() const { return m_filename; } void TextEditor::wordWrap(bool b) { if (b) { setLineWrapMode(QPlainTextEdit::WidgetWidth); } else { setLineWrapMode(QPlainTextEdit::NoWrap); } emit wordWrapChanged(b); } void TextEditor::toggleWordWrap() { wordWrap(!wordWrap()); } bool TextEditor::wordWrap() const { return (lineWrapMode() == QPlainTextEdit::WidgetWidth); } void TextEditor::dirty(bool b) { m_dirty = b; } bool TextEditor::dirty() const { return m_dirty; } QColor TextEditor::backgroundColor() const { return m_highlighter->backgroundColor(); } void TextEditor::setBackgroundColor(const QColor& c) { if (m_highlighter->backgroundColor() == c) { return; } m_highlighter->setBackgroundColor(c); setStyleSheet(QString("QPlainTextEdit{ background-color: rgba(%1, %2, %3, %4); }") .arg(c.redF() * 255) .arg(c.greenF() * 255) .arg(c.blueF() * 255) .arg(c.alphaF())); } QColor TextEditor::textColor() const { return m_highlighter->textColor(); } void TextEditor::setTextColor(const QColor& c) { m_highlighter->setTextColor(c); } QColor TextEditor::highlightBackgroundColor() const { return m_highlightBackground; } void TextEditor::setHighlightBackgroundColor(const QColor& c) { m_highlightBackground = c; update(); } void TextEditor::onModified(bool b) { dirty(b); emit modified(b); } void TextEditor::setupToolbar() { auto* widget = wrapEditWidget(); if (!widget) { return; } auto* layout = new QVBoxLayout(widget); // adding toolbar and edit layout->addWidget(m_toolbar); layout->addWidget(this); // make the edit stretch layout->setStretch(0, 0); layout->setStretch(1, 1); // visuals layout->setContentsMargins(0, 0, 0, 0); widget->show(); } QWidget* TextEditor::wrapEditWidget() { auto widget = std::make_unique(); // wrapping the QPlainTextEdit into a new widget so the toolbar can be // displayed above it if (auto* parentLayout=parentWidget()->layout()) { // the edit's parent has a regular layout, replace the edit by the new // widget and delete the QLayoutItem that's returned as it's not needed delete parentLayout->replaceWidget(this, widget.get()); } else if (auto* splitter=qobject_cast(parentWidget())) { // the edit's parent is a QSplitter, which doesn't have a layout; replace // the edit by using its index in the splitter auto index = splitter->indexOf(this); if (index == -1) { qCritical( "TextEditor: cannot wrap edit widget to display a toolbar, " "parent is a splitter, but widget isn't in it"); return nullptr; } splitter->replaceWidget(index, widget.get()); } else { // unknown parent qCritical( "TextEditor: cannot wrap edit widget to display a toolbar, " "no parent or parent has no layout"); return nullptr; } return widget.release(); } void TextEditor::resizeEvent(QResizeEvent* e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); m_lineNumbers->setGeometry(QRect(cr.left(), cr.top(), m_lineNumbers->areaWidth(), cr.height())); } void TextEditor::paintLineNumbers(QPaintEvent* e, const QColor& textColor) { QStyleOption opt; opt.init(m_lineNumbers); QPainter painter(m_lineNumbers); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); int bottom = top + (int) blockBoundingRect(block).height(); while (block.isValid() && top <= e->rect().bottom()) { if (block.isVisible() && bottom >= e->rect().top()) { QString number = QString::number(blockNumber + 1); painter.setPen(textColor); painter.drawText( 0, top, m_lineNumbers->width() - 3, fontMetrics().height(), Qt::AlignRight, number); } block = block.next(); top = bottom; bottom = top + (int) blockBoundingRect(block).height(); ++blockNumber; } } void TextEditor::highlightCurrentLine() { QList extraSelections; if (!isReadOnly()) { QTextEdit::ExtraSelection selection; QColor lineColor = QColor(Qt::yellow).lighter(160); selection.format.setBackground(m_highlightBackground); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); extraSelections.append(selection); } setExtraSelections(extraSelections); } TextEditorHighlighter::TextEditorHighlighter(QTextDocument* doc) : QSyntaxHighlighter(doc), m_background(QColor("transparent")), m_text(QColor("black")) { } QColor TextEditorHighlighter::backgroundColor() const { return m_background; } void TextEditorHighlighter::setBackgroundColor(const QColor& c) { m_background = c; changed(); } QColor TextEditorHighlighter::textColor() const { return m_text; } void TextEditorHighlighter::setTextColor(const QColor& c) { m_text = c; changed(); } void TextEditorHighlighter::highlightBlock(const QString& s) { QTextCharFormat f; f.setBackground(m_background); f.setForeground(m_text); setFormat(0, s.size(), f); } void TextEditorHighlighter::changed() { rehighlight(); } TextEditorLineNumbers::TextEditorLineNumbers(TextEditor& editor) : QFrame(&editor), m_editor(editor) { setFont(editor.font()); connect(&m_editor, &QPlainTextEdit::blockCountChanged, [&]{ updateAreaWidth(); }); connect(&m_editor, &QPlainTextEdit::updateRequest, [&](auto&& rect, int dy){ updateArea(rect, dy); }); updateAreaWidth(); } QSize TextEditorLineNumbers::sizeHint() const { return QSize(areaWidth(), 0); } int TextEditorLineNumbers::areaWidth() const { int digits = 1; int max = std::max(1, m_editor.blockCount()); while (max >= 10) { max /= 10; ++digits; } digits = std::max(3, digits); int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits + 3; return space; } QColor TextEditorLineNumbers::textColor() const { return m_text; } void TextEditorLineNumbers::setTextColor(const QColor& c) { m_text = c; m_editor.update(); } QColor TextEditorLineNumbers::backgroundColor() const { return m_background; } void TextEditorLineNumbers::setBackgroundColor(const QColor& c) { m_background = c; m_editor.update(); } void TextEditorLineNumbers::paintEvent(QPaintEvent* e) { QPainter painter(this); painter.fillRect(e->rect(), m_background); QFrame::paintEvent(e); m_editor.paintLineNumbers(e, m_text); } void TextEditorLineNumbers::updateAreaWidth() { m_editor.setViewportMargins(areaWidth(), 0, 0, 0); } void TextEditorLineNumbers::updateArea(const QRect &rect, int dy) { if (dy) { scroll(0, dy); } else { update(0, rect.y(), width(), rect.height()); } if (rect.contains(m_editor.viewport()->rect())) { updateAreaWidth(); } } TextEditorToolbar::TextEditorToolbar(TextEditor& editor) : m_editor(editor), m_save(new QAction(QIcon(":/MO/gui/save"), QObject::tr("&Save"))), m_wordWrap(new QAction(QIcon(":/MO/gui/word-wrap"), QObject::tr("&Word wrap"))) { QObject::connect(m_save, &QAction::triggered, [&]{ m_editor.save(); }); m_wordWrap->setCheckable(true); QObject::connect(m_wordWrap, &QAction::triggered, [&]{ m_editor.toggleWordWrap(); }); auto* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setAlignment(Qt::AlignLeft); auto* b = new QToolButton; b->setDefaultAction(m_save); layout->addWidget(b); b = new QToolButton; b->setDefaultAction(m_wordWrap); layout->addWidget(b); QObject::connect(&m_editor, &TextEditor::modified, [&](bool b){ onTextModified(b); }); QObject::connect(&m_editor, &TextEditor::wordWrapChanged, [&](bool b){ onWordWrap(b); }); } void TextEditorToolbar::onTextModified(bool b) { m_save->setEnabled(b); } void TextEditorToolbar::onWordWrap(bool b) { m_wordWrap->setChecked(b); }