From 439c09912482489e79399fd9e10fa2b7f717f565 Mon Sep 17 00:00:00 2001 From: Daniel Ramoeller Date: Sat, 14 Jan 2023 13:22:57 +0100 Subject: [PATCH] Extended comment and indentation for source code - automatically inherit indentation from previous block - (un)indent blocks - (un)comment blocks - add feature to show tabs and spaces --- src/frontends/qt/GuiDocument.cpp | 21 +- src/frontends/qt/GuiSourceEdit.cpp | 311 ++++++++++++++++++++++++++ src/frontends/qt/GuiSourceEdit.h | 94 ++++++++ src/frontends/qt/LaTeXHighlighter.cpp | 18 ++ src/frontends/qt/Makefile.am | 2 + src/frontends/qt/ui/LocalLayoutUi.ui | 9 +- src/frontends/qt/ui/PreambleUi.ui | 9 +- 7 files changed, 442 insertions(+), 22 deletions(-) create mode 100644 src/frontends/qt/GuiSourceEdit.cpp create mode 100644 src/frontends/qt/GuiSourceEdit.h diff --git a/src/frontends/qt/GuiDocument.cpp b/src/frontends/qt/GuiDocument.cpp index 60f9f844b4..7b4cedbfb5 100644 --- a/src/frontends/qt/GuiDocument.cpp +++ b/src/frontends/qt/GuiDocument.cpp @@ -479,7 +479,7 @@ PreambleModule::PreambleModule(QWidget * parent) // @ is letter in the LyX user preamble (void) new LaTeXHighlighter(preambleTE->document(), true); preambleTE->setFont(guiApp->typewriterSystemFont()); - preambleTE->setWordWrapMode(QTextOption::NoWrap); + preambleTE->setCommentMarker("%"); setFocusProxy(preambleTE); // Install event filter on find line edit to capture return/enter key findLE->installEventFilter(this); @@ -489,15 +489,6 @@ PreambleModule::PreambleModule(QWidget * parent) connect(editPB, SIGNAL(clicked()), this, SLOT(editExternal())); connect(findLE, SIGNAL(returnPressed()), this, SLOT(findText())); checkFindButton(); - int const tabStop = 4; - QFontMetrics metrics(preambleTE->currentFont()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) - // horizontalAdvance() is available starting in 5.11.0 - // setTabStopDistance() is available starting in 5.10.0 - preambleTE->setTabStopDistance(tabStop * metrics.horizontalAdvance(' ')); -#else - preambleTE->setTabStopWidth(tabStop * metrics.width(' ')); -#endif } @@ -627,20 +618,10 @@ LocalLayout::LocalLayout(QWidget * parent) : UiWidget(parent), current_id_(nullptr), validated_(false) { locallayoutTE->setFont(guiApp->typewriterSystemFont()); - locallayoutTE->setWordWrapMode(QTextOption::NoWrap); connect(locallayoutTE, SIGNAL(textChanged()), this, SLOT(textChanged())); connect(validatePB, SIGNAL(clicked()), this, SLOT(validatePressed())); connect(convertPB, SIGNAL(clicked()), this, SLOT(convertPressed())); connect(editPB, SIGNAL(clicked()), this, SLOT(editExternal())); - int const tabStop = 4; - QFontMetrics metrics(locallayoutTE->currentFont()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) - // horizontalAdvance() is available starting in 5.11.0 - // setTabStopDistance() is available starting in 5.10.0 - locallayoutTE->setTabStopDistance(tabStop * metrics.horizontalAdvance(' ')); -#else - locallayoutTE->setTabStopWidth(tabStop * metrics.width(' ')); -#endif } diff --git a/src/frontends/qt/GuiSourceEdit.cpp b/src/frontends/qt/GuiSourceEdit.cpp new file mode 100644 index 0000000000..9b008a5d03 --- /dev/null +++ b/src/frontends/qt/GuiSourceEdit.cpp @@ -0,0 +1,311 @@ +// -*- C++ -*- +/** + * \file GuiSourceEdit.cpp + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * Full author contact details are available in file CREDITS. + */ + +#include + +#include "GuiSourceEdit.h" + +#include "qt_helpers.h" + +#include +#include + +namespace lyx { +namespace frontend { + +GuiSourceEdit::GuiSourceEdit(QWidget *parent) : QTextEdit(parent) +{ + setWordWrapMode(QTextOption::NoWrap); + // Set the default tab stop + setTabStop(tabStop_); + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, SIGNAL(customContextMenuRequested(QPoint)), + this, SLOT(showMenu(QPoint))); +} + +void GuiSourceEdit::showMenu(const QPoint& pos) +{ + // Move the cursor to the click position unless clicked within selection + QTextCursor cursor = textCursor(); + int const textPos = cursorForPosition(pos).position(); + bool const textPosInSel = textCursor().selectionStart() <= textPos && + textPos <= textCursor().selectionEnd(); + if (!textCursor().hasSelection() || !textPosInSel) { + cursor.setPosition(textPos); + setTextCursor(cursor); + } + // The standard menu + QMenu * menu = QTextEdit::createStandardContextMenu(); + QAction * firstAction = menu->actions().at(0); + // Insert toggle comment entry at the top + QKeySequence keySeq = QKeySequence(Qt::ControlModifier | Qt::Key_Slash); + QAction * toggleComment = new QAction(qt_("Toggle Comment") + "\t" + + keySeq.toString(QKeySequence::NativeText), menu); + connect(toggleComment, SIGNAL(triggered()), this, SLOT(toggleComment())); + menu->insertAction(firstAction, toggleComment); + // Insert toggle spaces and tabs entry at the top + QAction * showTabsAndSpaces = new QAction(qt_("Show Tabs and Spaces"), menu); + showTabsAndSpaces->setCheckable(true); + auto currentFlags = document()->defaultTextOption().flags(); + showTabsAndSpaces->setChecked(currentFlags & + QTextOption::ShowTabsAndSpaces); + connect(showTabsAndSpaces, SIGNAL(triggered()), this, + SLOT(toggleShowTabsAndSpaces())); + menu->insertAction(firstAction, showTabsAndSpaces); + // Add separator to default entries + menu->insertSeparator(firstAction); + menu->exec(mapToGlobal(pos)); +} + +void GuiSourceEdit::setCommentMarker(QString marker) +{ + commentMarker_ = marker; +} + +void GuiSourceEdit::setTabStop(int spaces) +{ + tabStop_ = spaces; + QFontMetrics metrics(currentFont()); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) + // horizontalAdvance() is available starting in 5.11.0 + // setTabStopDistance() is available starting in 5.10.0 + setTabStopDistance(tabStop_ * metrics.horizontalAdvance(' ')); +#else + setTabStopWidth(tabStop_ * metrics.width(' ')); +#endif +} + +void GuiSourceEdit::setFont(const QFont & font) +{ + QTextEdit::setFont(font); + // Re-calculate tabstop width based on new font + setTabStop(tabStop_); +} + +QTextBlock GuiSourceEdit::blockAtSelPos(Position position) const +{ + QTextCursor cursor = textCursor(); + QTextDocument * doc = cursor.document(); + int pos = position == START ? qMin(cursor.anchor(), cursor.position()) : + qMax(cursor.anchor(), cursor.position()); + return doc->findBlock(pos); +} + +QTextCursor GuiSourceEdit::cursorAt(int position) const { + // Create text cursor + QTextCursor cursor = textCursor(); + // Move the cursor to position + cursor.setPosition(position); + return cursor; +} + +void GuiSourceEdit::removeMarker(int positionStart, QString marker, + bool addedSpace) +{ + // Create text cursor + QTextCursor cursor = cursorAt(positionStart); + QTextBlock const block = cursor.block(); + QString blockText = block.text(); + int const index = blockText.indexOf(marker); + int length = marker.length(); + if (index != -1) { + // Check for extra space after marker + if (addedSpace && blockText.remove(0, index + length).startsWith(" ")) + ++length; + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, + index); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, + length); + cursor.deleteChar(); + } +} + +void GuiSourceEdit::insertMarkerAtIndentation(int positionStart, QString marker, + bool addedSpace, int indentation) +{ + // Create text cursor + QTextCursor cursor = cursorAt(positionStart); + // Move the cursor to the given indentation + int i = 0; + while (i < indentation) { + if (toPlainText().at(cursor.position()) == '\t') + i += tabStop_; + else + ++i; + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor); + } + // Add the comment marker + cursor.insertText(marker + (addedSpace ? " " : ""), QTextCharFormat()); +} + +void GuiSourceEdit::modifyMarkerInSel(QString marker, Modification modification, + bool allEmpty, bool addedSpace) +{ + QTextBlock const startBlock = blockAtSelPos(START); + QTextBlock const endBlock = blockAtSelPos(END); + QTextBlock const endBlockNext = endBlock.next(); + // Create text cursor + QTextCursor cursor = textCursor(); + cursor.beginEditBlock(); + for (QTextBlock block = startBlock; block != endBlockNext; + block = block.next()) { + if (modification == REMOVE) + removeMarker(block.position(), marker, addedSpace); + // Disregard white space blocks + else if (allEmpty || !block.text().trimmed().isEmpty()) { + insertMarkerAtIndentation(block.position(), marker, addedSpace, + selMinIndentation(allEmpty)); + } + } + cursor.endEditBlock(); +} + +int GuiSourceEdit::getIndentation(QString text) const +{ + int tabs = 0; + for (QChar c : text) { + if (c == QChar::Tabulation) + ++tabs; + if (!c.isSpace()) + break; + } + return tabs; +} + +QString GuiSourceEdit::getIndentationString(QString text) const +{ + // Regex to capture indentation string (i.e. spaces and tab stops) + QRegularExpression static re("^( |\t)+"); + return re.match(text).captured(0); +} + +void GuiSourceEdit::newLineWithInheritedIndentation() +{ + QTextCursor cursor = textCursor(); + cursor.beginEditBlock(); + // Start new line + cursor.insertText("\n", QTextCharFormat()); + // Insert as many tabstops as on the previous block + QTextBlock const previousBlock = blockAtSelPos(START).previous(); + QString const indentation = getIndentationString(previousBlock.text()); + cursor.insertText(indentation); + cursor.endEditBlock(); +} + +int GuiSourceEdit::getLengthInSpaces(QString const & text) const +{ + // Replace tab stops by spaces and return length + return QString(text).replace("\t", QString(" ").repeated(tabStop_)) + .length(); +} + +int GuiSourceEdit::selMinIndentation(bool allEmpty) const +{ + QTextBlock const startBlock = blockAtSelPos(START); + QTextBlock const endBlock = blockAtSelPos(END); + QTextBlock const endBlockNext = endBlock.next(); + QString minIndentationString = getIndentationString(startBlock.text()); + for (QTextBlock block = startBlock; block != endBlockNext; + block = block.next()) { + QString text = block.text(); + if (allEmpty || !text.trimmed().isEmpty()) { + QString curIndentationString = getIndentationString(text); + // Chop off indentation until they have the same length + while (!minIndentationString.isEmpty()) { + int minLen = getLengthInSpaces(minIndentationString); + int curLen = getLengthInSpaces(curIndentationString); + if (minLen == curLen) + break; + else if (minLen > curLen) + minIndentationString.chop(1); + else + curIndentationString.chop(1); + } + } + } + return getLengthInSpaces(minIndentationString); +} + +bool GuiSourceEdit::selBlocksStartWith(QString marker) const +{ + QTextBlock const startBlock = blockAtSelPos(START); + QTextBlock const endBlock = blockAtSelPos(END); + QTextBlock const endBlockNext = endBlock.next(); + for (QTextBlock block = startBlock; block != endBlockNext; + block = block.next()) { + QString const blockText = block.text(); + QString trimmedText = blockText.trimmed(); + // Disregard white space blocks + if (trimmedText.isEmpty()) + continue; + else if (!trimmedText.startsWith(marker)) + return false; + } + return true; +} + +bool GuiSourceEdit::selBlocksWhiteSpace() const +{ + QTextBlock const startBlock = blockAtSelPos(START); + QTextBlock const endBlockNext = blockAtSelPos(END).next(); + for (QTextBlock block = startBlock; block != endBlockNext; + block = block.next()) { + QString const blockText = block.text(); + QString const trimmedText = blockText.trimmed(); + // Disregard white space blocks + if (trimmedText.isEmpty()) + continue; + else + return false; + } + return true; +} + +void GuiSourceEdit::toggleComment() +{ + bool const allEmpty = selBlocksWhiteSpace(); + modifyMarkerInSel(commentMarker_, + !allEmpty && selBlocksStartWith(commentMarker_) ? + REMOVE : INSERT, + allEmpty, addedSpaceAfterComment_); +} + +void GuiSourceEdit::toggleShowTabsAndSpaces() +{ + QTextOption option = document()->defaultTextOption(); + auto currentFlags = document()->defaultTextOption().flags(); + if (currentFlags & QTextOption::ShowTabsAndSpaces) + currentFlags &= ~QTextOption::ShowTabsAndSpaces; + else + currentFlags |= QTextOption::ShowTabsAndSpaces; + option.setFlags(currentFlags); + document()->setDefaultTextOption(option); +} + +void GuiSourceEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->modifiers() == Qt::ControlModifier && + event->key() == Qt::Key_Slash) + toggleComment(); + else if (event->key() == Qt::Key_Tab && + blockAtSelPos(START) != blockAtSelPos(END)) + modifyMarkerInSel("\t", INSERT, selBlocksWhiteSpace(), false); + else if (event->key() == Qt::Key_Backtab) + modifyMarkerInSel("\t", REMOVE); + else if (event->key() == Qt::Key_Return) + newLineWithInheritedIndentation(); + else + // Call base class for other events + QTextEdit::keyPressEvent(event); +} + +} // namespace frontend +} // namespace lyx + +#include "moc_GuiSourceEdit.cpp" diff --git a/src/frontends/qt/GuiSourceEdit.h b/src/frontends/qt/GuiSourceEdit.h new file mode 100644 index 0000000000..4a8bc1ce15 --- /dev/null +++ b/src/frontends/qt/GuiSourceEdit.h @@ -0,0 +1,94 @@ +// -*- C++ -*- +/** + * \file GuiSourceEdit.h + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * Full author contact details are available in file CREDITS. + */ + +#ifndef GUISOURCEEDIT_H +#define GUISOURCEEDIT_H + +#include +#include +#include +#include + +namespace lyx { +namespace frontend { + +class GuiSourceEdit : public QTextEdit +{ + Q_OBJECT + +public: + explicit GuiSourceEdit(QWidget *parent = nullptr); + // Set and get tab stop in number of spaces + void setTabStop(int spaces); + int tabStop() const {return tabStop_; }; + // Set and get line marker, e.g. "//" + void setCommentMarker(QString marker); + QString commentMarker() const { return commentMarker_; }; + // Set and get whether a space is added after the line marker + void setAddedSpaceAfterComment(int spaces); + bool addedSpaceAfterComment() const { return addedSpaceAfterComment_; }; + // Set font and update tab stop + void setFont(const QFont & font); + +private Q_SLOTS: + void showMenu(const QPoint& pos); + void toggleComment(); + void toggleShowTabsAndSpaces(); + +protected: + void keyPressEvent(QKeyEvent *event) override; + +private: + enum Modification { INSERT, REMOVE }; + enum Position { START, END }; + + // Get block at selection start/end + QTextBlock blockAtSelPos(Position position) const; + // Whether all blocks start with marker + bool selBlocksStartWith(QString marker) const; + // Wehther all blocks are white space + bool selBlocksWhiteSpace() const; + // Get length of string substituting tabs for spaces + int getLengthInSpaces(QString const & text) const; + // Minimum tab indentation the paragraphs selected start with + int selMinIndentation(bool allEmpty) const; + // Number of tabs as indentation + int getIndentation(QString text) const; + // String of white space + QString getIndentationString(QString text) const; + // Copy of textCursor() [at position] + QTextCursor cursorAt(int position) const; + + // From positionStart remove marker from the line + void removeMarker(int positionStart, QString marker, + bool addedSpace = false); + // From positionStart insert marker at indentation + void insertMarkerAtIndentation(int positionStart, QString marker, + bool addedSpace = false, + int indentation = 0); + // Modify (insert/remove) marker + void modifyMarkerInSel(QString marker, + Modification modification = INSERT, + bool allEmpty = false, bool addedSpace = false); + // Create a new line at cursor same indentation + void newLineWithInheritedIndentation(); + + // The comment marker + QString commentMarker_ = "#"; + // The tab stop in spaces + int tabStop_ = 4; + // Whether a space gets added after the comment marker + bool addedSpaceAfterComment_ = true; +}; + + +} // namespace frontend +} // namespace lyx + +#endif // GUISOURCEEDIT_H diff --git a/src/frontends/qt/LaTeXHighlighter.cpp b/src/frontends/qt/LaTeXHighlighter.cpp index 80e7a09cbd..51d5867d5f 100644 --- a/src/frontends/qt/LaTeXHighlighter.cpp +++ b/src/frontends/qt/LaTeXHighlighter.cpp @@ -123,6 +123,14 @@ void LaTeXHighlighter::highlightBlock(QString const & text) setFormat(index, length, keywordFormat); index = exprKeyword.indexIn(text, index + length); } + // White space + QRegExp exprWhiteSpace("\\s"); + index = exprWhiteSpace.indexIn(text); + while (index >= 0) { + int length = exprWhiteSpace.matchedLength(); + setFormat(index, length, commentFormat); + index = exprWhiteSpace.indexIn(text, index + length); + } // %comment // Treat a line as a comment starting at a percent sign // * that is the first character in a line @@ -237,6 +245,16 @@ void LaTeXHighlighter::highlightBlock(QString const & text) match = exprKeyword.match(text, index + length); index = match.capturedStart(0); } + // White space + QRegularExpression exprWhiteSpace("\\s"); + match = exprWhiteSpace.match(text); + index = match.capturedStart(0); + while (index >= 0) { + int length = match.capturedLength(0); + setFormat(index, length, commentFormat); + match = exprWhiteSpace.match(text, index + length); + index = match.capturedStart(0); + } // %comment // Treat a line as a comment starting at a percent sign // * that is the first character in a line diff --git a/src/frontends/qt/Makefile.am b/src/frontends/qt/Makefile.am index 9ca258d9d3..486b28fd9b 100644 --- a/src/frontends/qt/Makefile.am +++ b/src/frontends/qt/Makefile.am @@ -118,6 +118,7 @@ SOURCEFILES = \ GuiSendto.cpp \ GuiSetBorder.cpp \ GuiShowFile.cpp \ + GuiSourceEdit.cpp \ GuiSpellchecker.cpp \ GuiSymbols.cpp \ GuiTabular.cpp \ @@ -232,6 +233,7 @@ MOCHEADER = \ GuiSendto.h \ GuiSetBorder.h \ GuiShowFile.h \ + GuiSourceEdit.h \ GuiSpellchecker.h \ GuiSymbols.h \ GuiTabularCreate.h \ diff --git a/src/frontends/qt/ui/LocalLayoutUi.ui b/src/frontends/qt/ui/LocalLayoutUi.ui index 1a3e17809e..c2377b1ca3 100644 --- a/src/frontends/qt/ui/LocalLayoutUi.ui +++ b/src/frontends/qt/ui/LocalLayoutUi.ui @@ -15,7 +15,7 @@ - + Document-specific layout information @@ -105,6 +105,13 @@ + + + lyx::frontend::GuiSourceEdit + QTextEdit +
GuiSourceEdit.h
+
+
qt_i18n.h diff --git a/src/frontends/qt/ui/PreambleUi.ui b/src/frontends/qt/ui/PreambleUi.ui index eb27e91adb..dece98299c 100644 --- a/src/frontends/qt/ui/PreambleUi.ui +++ b/src/frontends/qt/ui/PreambleUi.ui @@ -54,7 +54,7 @@ - + false @@ -62,6 +62,13 @@ + + + lyx::frontend::GuiSourceEdit + QTextEdit +
GuiSourceEdit.h
+
+
qt_i18n.h