From dce877989f71e119fc94a3408e6d6435b7c9435b Mon Sep 17 00:00:00 2001 From: Kubat <mael.martin31@gmail.com> Date: Tue, 24 Aug 2021 16:40:35 +0200 Subject: [PATCH] FakeVim: Add the FakeVim editor proxy to the script views --- src/Lib/Utils.hh | 3 + src/UI/ScriptViews/EditorProxy.cc | 411 +++++++++++++++++++++++++++++ src/UI/ScriptViews/EditorProxy.hh | 88 ++++++ src/UI/ScriptViews/ScriptEditor.cc | 38 +++ src/UI/ScriptViews/ScriptEditor.hh | 6 +- 5 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 src/UI/ScriptViews/EditorProxy.cc create mode 100644 src/UI/ScriptViews/EditorProxy.hh diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh index 0b087c02..3283a2c8 100644 --- a/src/Lib/Utils.hh +++ b/src/Lib/Utils.hh @@ -16,6 +16,9 @@ #include <qglobal.h> +#define VIVY_PRAGMA(x) _Pragma(#x) +#define TODO(x) VIVY_PRAGMA(message("\"TODO: " #x "\"")) + #define VIVY_WIN_EXE_SUFFIX ".exe" #define VIVY_ASSERT_STRINGIFY_HELPER(x) #x #define VIVY_ASSERT_STRINGIFY(x) VIVY_ASSERT_STRINGIFY_HELPER(x) diff --git a/src/UI/ScriptViews/EditorProxy.cc b/src/UI/ScriptViews/EditorProxy.cc new file mode 100644 index 00000000..7187cb5c --- /dev/null +++ b/src/UI/ScriptViews/EditorProxy.cc @@ -0,0 +1,411 @@ +#include "EditorProxy.hh" +#include "../FakeVim/FakeVimHandler.hh" +#include "../FakeVim/FakeVimActions.hh" + +#include <QMessageBox> +#include <QStatusBar> +#include <QMainWindow> +#include <QTemporaryFile> + +using namespace Vivy; + +void +Vivy::initHandler(FakeVimHandler *handler) +{ + handler->handleCommand(QStringLiteral("set nopasskeys")); + handler->handleCommand(QStringLiteral("set nopasscontrolkey")); + handler->installEventFilter(); + handler->setupWidget(); +} + +void +Vivy::clearUndoRedo(QPlainTextEdit *scriptEditor) +{ + scriptEditor->setUndoRedoEnabled(false); + scriptEditor->setUndoRedoEnabled(true); +} + +Vivy::EditorProxy * +Vivy::connectSignals(FakeVimHandler *handler, QMainWindow *mainWindow, QPlainTextEdit *editor) +{ + EditorProxy *proxy = new EditorProxy(editor, mainWindow, handler); + + handler->commandBufferChanged.connect([proxy](const QString &contents, int cursorPos, + int /* anchorPos */, + int /* messageLevel */) noexcept -> void { + proxy->changeStatusMessage(contents, cursorPos); + }); + handler->extraInformationChanged.connect( + [proxy](const QString &text) noexcept -> void { proxy->changeExtraInformation(text); }); + handler->statusDataChanged.connect( + [proxy](const QString &text) noexcept -> void { proxy->changeStatusData(text); }); + handler->highlightMatches.connect( + [proxy](const QString &needle) noexcept -> void { proxy->highlightMatches(needle); }); + handler->handleExCommandRequested.connect( + [proxy](bool *handled, const ExCommand &cmd) noexcept -> void { + proxy->handleExCommand(handled, cmd); + }); + handler->requestSetBlockSelection.connect([proxy](const QTextCursor &cursor) noexcept -> void { + proxy->requestSetBlockSelection(cursor); + }); + handler->requestDisableBlockSelection.connect( + [proxy]() noexcept -> void { proxy->requestDisableBlockSelection(); }); + handler->requestHasBlockSelection.connect( + [proxy](bool *on) noexcept -> void { proxy->requestHasBlockSelection(on); }); + + handler->indentRegion.connect( + [proxy](int beginBlock, int endBlock, QChar typedChar) noexcept -> void { + proxy->indentRegion(beginBlock, endBlock, typedChar); + }); + handler->checkForElectricCharacter.connect([proxy](bool *result, QChar c) noexcept -> void { + proxy->checkForElectricCharacter(result, c); + }); + + return proxy; +} + +EditorProxy::EditorProxy(QPlainTextEdit *widg, QMainWindow *mainWin, QObject *parent) noexcept + : QObject(parent) + , widget(widg) + , mainWindow(mainWin) +{ +} + +void +EditorProxy::openFile(const QString &filename) noexcept +{ + emit handleInput(QString(QStringLiteral(":r %1<CR>")).arg(filename)); +} + +void +EditorProxy::changeStatusData(const QString &info) noexcept +{ + statusData = info; + updateStatusBar(); +} + +void +EditorProxy::highlightMatches(const QString &pattern) noexcept +{ + QTextDocument *doc = widget->document(); + Q_ASSERT(doc); + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(Qt::yellow); + selection.format.setForeground(Qt::black); + + // Highlight matches. + QRegExp re(pattern); + QTextCursor cur = doc->find(re); + searchSelection.clear(); + + int a = cur.position(); + while (!cur.isNull()) { + if (cur.hasSelection()) { + selection.cursor = cur; + searchSelection.append(selection); + } else { + cur.movePosition(QTextCursor::NextCharacter); + } + + cur = doc->find(re, cur); + int b = cur.position(); + + if (a == b) { + cur.movePosition(QTextCursor::NextCharacter); + cur = doc->find(re, cur); + b = cur.position(); + if (a == b) + break; + } + + a = b; + } + + updateExtraSelections(); +} + +void +EditorProxy::changeStatusMessage(const QString &contents, int cursorPos) noexcept +{ + statusMessage = (cursorPos == -1) + ? contents + : contents.left(cursorPos) + QChar(10073) + contents.mid(cursorPos); + updateStatusBar(); +} + +void +EditorProxy::changeExtraInformation(const QString &info) noexcept +{ + QMessageBox::information(widget, tr("Information"), info); +} + +void +EditorProxy::updateStatusBar() noexcept +{ + static constexpr int msgMaxSize = 80; + int slack = msgMaxSize - statusMessage.size() - statusData.size(); + QString msg = statusMessage + QString(slack, QLatin1Char(' ')) + statusData; + mainWindow->statusBar()->showMessage(msg); +} + +void +EditorProxy::handleExCommand(bool *handled, const ExCommand &cmd) noexcept +{ + if (wantSaveAndQuit(cmd)) + emit requestSaveAndQuit(); // :wq + + else if (wantSave(cmd)) + emit requestSave(); // :w + + else if (wantQuit(cmd)) { + if (cmd.hasBang) + invalidate(); // :q! + else + emit requestQuit(); // :q + } + + else if (wantRun(cmd)) + emit requestRun(); + + else { + *handled = false; + return; + } + + *handled = true; +} + +void +EditorProxy::requestSetBlockSelection(const QTextCursor &tc) noexcept +{ + const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette() + : QApplication::palette(); + + blockSelection.clear(); + clearSelection.clear(); + + QTextCursor cur = tc; + + QTextEdit::ExtraSelection selection; + selection.format.setBackground(pal.color(QPalette::Base)); + selection.format.setForeground(pal.color(QPalette::Text)); + selection.cursor = cur; + clearSelection.append(selection); + + selection.format.setBackground(pal.color(QPalette::Highlight)); + selection.format.setForeground(pal.color(QPalette::HighlightedText)); + + const int from = cur.positionInBlock(); + const int to = cur.anchor() - cur.document()->findBlock(cur.anchor()).position(); + const int min = qMin(cur.position(), cur.anchor()); + const int max = qMax(cur.position(), cur.anchor()); + for (QTextBlock block = cur.document()->findBlock(min); + block.isValid() && block.position() < max; block = block.next()) { + cur.setPosition(block.position() + qMin(from, block.length())); + cur.setPosition(block.position() + qMin(to, block.length()), QTextCursor::KeepAnchor); + selection.cursor = cur; + blockSelection.append(selection); + } + + disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + widget->setTextCursor(tc); + connect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + + QPalette pal2 = widget->palette(); + pal2.setColor(QPalette::Highlight, Qt::transparent); + pal2.setColor(QPalette::HighlightedText, Qt::transparent); + widget->setPalette(pal2); + + updateExtraSelections(); +} + +void +EditorProxy::requestDisableBlockSelection() noexcept +{ + const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette() + : QApplication::palette(); + + blockSelection.clear(); + clearSelection.clear(); + widget->setPalette(pal); + + disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection); + + updateExtraSelections(); +} + +void +EditorProxy::updateBlockSelection() noexcept +{ + requestSetBlockSelection(widget->textCursor()); +} + +void +EditorProxy::requestHasBlockSelection(bool *on) noexcept +{ + *on = !blockSelection.isEmpty(); +} + +void +EditorProxy::indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept +{ + QTextDocument *doc = widget->document(); + Q_ASSERT(doc); + + const int indentSize = + static_cast<int>(FakeVim::Internal::fakeVimSettings()->shiftWidth.value()); + QTextBlock startBlock = doc->findBlockByNumber(beginBlock); + + // Record line lenghts for mark adjustments + QVector<int> lineLengths(endBlock - beginBlock + 1); + QTextBlock block = startBlock; + + for (int i = beginBlock; i <= endBlock; ++i) { + const QString line = block.text(); + lineLengths[i - beginBlock] = line.length(); + + if (typedChar.unicode() == 0 && line.simplified().isEmpty()) { + // clear empty lines + QTextCursor cursor(block); + while (!cursor.atBlockEnd()) + cursor.deleteChar(); + } + + else { + const QTextBlock previousBlock = block.previous(); + const QString previousLine = previousBlock.isValid() ? previousBlock.text() : QString(); + + int indent = firstNonSpace(previousLine); + if (typedChar == '}') + indent = std::max(0, indent - indentSize); + else if (previousLine.endsWith("{")) + indent += indentSize; + const QString indentString = QString(" ").repeated(indent); + + QTextCursor cursor(block); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::StartOfBlock); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, + firstNonSpace(line)); + cursor.removeSelectedText(); + cursor.insertText(indentString); + cursor.endEditBlock(); + } + + block = block.next(); + } +} + +void +EditorProxy::checkForElectricCharacter(bool *result, QChar c) noexcept +{ + *result = c == '{' || c == '}'; +} + +int +EditorProxy::firstNonSpace(const QString &text) noexcept +{ + int indent = 0; + while (indent < text.length() && text.at(indent) == ' ') + ++indent; + return indent; +} + +void +EditorProxy::updateExtraSelections() noexcept +{ + widget->setExtraSelections(clearSelection + searchSelection + blockSelection); +} + +bool +EditorProxy::wantSaveAndQuit(const ExCommand &cmd) noexcept +{ + return cmd.cmd == "wq"; +} + +bool +EditorProxy::wantSave(const ExCommand &cmd) noexcept +{ + return cmd.matches("w", "write") || cmd.matches("wa", "wall"); +} + +bool +EditorProxy::wantQuit(const ExCommand &cmd) noexcept +{ + return cmd.matches("q", "quit") || cmd.matches("qa", "qall"); +} + +bool +EditorProxy::wantRun(const ExCommand &cmd) noexcept +{ + return cmd.matches("run", "run") || cmd.matches("make", "make"); +} + +bool +EditorProxy::save(const QString &fileName) noexcept +{ + if (!hasChanges(fileName)) + return true; + + QTemporaryFile tmpFile; + if (!tmpFile.open()) { + qCritical() << tr("Cannot create temporary file: %1").arg(tmpFile.errorString()); + return false; + } + + QTextStream ts(&tmpFile); + ts << content(); + ts.flush(); + + QFile::remove(fileName); + if (!QFile::copy(tmpFile.fileName(), fileName)) { + qCritical() << tr("Cannot write to file \"%1\"").arg(fileName); + return false; + } + + return true; +} + +void +EditorProxy::cancel(const QString &fileName) noexcept +{ + if (hasChanges(fileName)) { + qCritical() << tr("File \"%1\" was changed").arg(fileName); + } else { + invalidate(); + } +} + +void +EditorProxy::invalidate() noexcept +{ + TODO(Use a Vivy thing here !) + QApplication::quit(); +} + +bool +EditorProxy::hasChanges(const QString &fileName) noexcept +{ + if (fileName.isEmpty() && content().isEmpty()) + return false; + + QFile f(fileName); + if (!f.open(QIODevice::ReadOnly)) + return true; + + QTextStream ts(&f); + return content() != ts.readAll(); +} + +QTextDocument * +EditorProxy::document() const noexcept +{ + return widget->document(); +} + +QString +EditorProxy::content() const noexcept +{ + return document()->toPlainText(); +} diff --git a/src/UI/ScriptViews/EditorProxy.hh b/src/UI/ScriptViews/EditorProxy.hh new file mode 100644 index 00000000..50a5b1e0 --- /dev/null +++ b/src/UI/ScriptViews/EditorProxy.hh @@ -0,0 +1,88 @@ +#pragma once + +#include <QObject> +#include <QTextEdit> +#include "ScriptEditor.hh" + +class QMainWindow; +class QTextDocument; +class QString; +class QWidget; +class QTextCursor; + +namespace FakeVim::Internal +{ +class FakeVimHandler; +struct ExCommand; +} + +namespace Vivy +{ +class EditorProxy; +using FakeVimHandler = FakeVim::Internal::FakeVimHandler; +using ExCommand = FakeVim::Internal::ExCommand; + +class EditorProxy : public QObject { + Q_OBJECT + +public: + explicit EditorProxy(QPlainTextEdit *widget, QMainWindow *mw, + QObject *parent = nullptr) noexcept; + void openFile(const QString &fileName) noexcept; + + bool save(const QString &fileName) noexcept; + void cancel(const QString &fileName) noexcept; + +signals: + void handleInput(const QString &keys); + void requestSave(); + void requestSaveAndQuit(); + void requestQuit(); + void requestRun(); + +public slots: + void changeStatusData(const QString &info) noexcept; + void highlightMatches(const QString &pattern) noexcept; + void changeStatusMessage(const QString &contents, int cursorPos) noexcept; + void changeExtraInformation(const QString &info) noexcept; + void updateStatusBar() noexcept; + void handleExCommand(bool *handled, const FakeVim::Internal::ExCommand &cmd) noexcept; + void requestSetBlockSelection(const QTextCursor &tc) noexcept; + void requestDisableBlockSelection() noexcept; + void updateBlockSelection() noexcept; + void requestHasBlockSelection(bool *on) noexcept; + void indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept; + void checkForElectricCharacter(bool *result, QChar c) noexcept; + +private: + static int firstNonSpace(const QString &text) noexcept; + + void updateExtraSelections() noexcept; + bool wantSaveAndQuit(const FakeVim::Internal::ExCommand &cmd) noexcept; + bool wantSave(const FakeVim::Internal::ExCommand &cmd) noexcept; + bool wantQuit(const FakeVim::Internal::ExCommand &cmd) noexcept; + bool wantRun(const FakeVim::Internal::ExCommand &cmd) noexcept; + + void invalidate() noexcept; + bool hasChanges(const QString &fileName) noexcept; + + QTextDocument *document() const noexcept; + QString content() const noexcept; + + QPlainTextEdit *widget; + QMainWindow *mainWindow; + QString statusMessage; + QString statusData; + + QList<QTextEdit::ExtraSelection> searchSelection; + QList<QTextEdit::ExtraSelection> clearSelection; + QList<QTextEdit::ExtraSelection> blockSelection; +}; + +QWidget *createEditorWidget(bool usePlainTextEdit); +void initHandler(FakeVimHandler *handler); +void clearUndoRedo(QPlainTextEdit *editor); +EditorProxy *connectSignals(FakeVimHandler *handler, QMainWindow *mainWindow, + QPlainTextEdit *editor); + +} diff --git a/src/UI/ScriptViews/ScriptEditor.cc b/src/UI/ScriptViews/ScriptEditor.cc index 7b8a73d9..56dff1a6 100644 --- a/src/UI/ScriptViews/ScriptEditor.cc +++ b/src/UI/ScriptViews/ScriptEditor.cc @@ -44,8 +44,46 @@ ScriptEditor::ScriptEditor(QWidget *parent) noexcept textFormat.setForeground(QBrush(Qt::white)); mergeCurrentCharFormat(textFormat); + QPlainTextEdit::setCursorWidth(0); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setBackgroundVisible(true); updateLineNumberAreaWidth(0); + setObjectName(QStringLiteral("Editor")); + setFocus(); +} + +void +ScriptEditor::paintEvent(QPaintEvent *e) noexcept +{ + QPlainTextEdit::paintEvent(e); + + if (!cursorRect.isNull() && e->rect().intersects(cursorRect)) { + QRect rect = cursorRect; + cursorRect = QRect(); + QPlainTextEdit::viewport()->update(rect); + } + + // Draw text cursor. + QRect rect = QPlainTextEdit::cursorRect(); + if (e->rect().intersects(rect)) { + QPainter painter(QPlainTextEdit::viewport()); + + if (QPlainTextEdit::overwriteMode()) { + QFontMetrics fm(QPlainTextEdit::font()); + const int position = QPlainTextEdit::textCursor().position(); + const QChar c = QPlainTextEdit::document()->characterAt(position); + rect.setWidth(fm.horizontalAdvance(c)); + painter.setPen(Qt::NoPen); + painter.setBrush(QPlainTextEdit::palette().color(QPalette::Base)); + painter.setCompositionMode(QPainter::CompositionMode_Difference); + } else { + rect.setWidth(QPlainTextEdit::cursorWidth()); + painter.setPen(QPlainTextEdit::palette().color(QPalette::Text)); + } + + painter.drawRect(rect); + cursorRect = rect; + } } void diff --git a/src/UI/ScriptViews/ScriptEditor.hh b/src/UI/ScriptViews/ScriptEditor.hh index 01c01769..491a427d 100644 --- a/src/UI/ScriptViews/ScriptEditor.hh +++ b/src/UI/ScriptViews/ScriptEditor.hh @@ -42,8 +42,9 @@ public slots: void updateLastLuaError(int, QString); protected: - void resizeEvent(QResizeEvent *event) noexcept override; - void keyPressEvent(QKeyEvent *e) noexcept override; + void resizeEvent(QResizeEvent *) noexcept override; + void keyPressEvent(QKeyEvent *) noexcept override; + void paintEvent(QPaintEvent *) noexcept override; private slots: void updateLineNumberAreaWidth(int newBlockCount) noexcept; @@ -51,5 +52,6 @@ private slots: private: QWidget *lineNumberArea{ nullptr }; + QRect cursorRect{}; }; } -- GitLab