Indentation and (un)commenting in local layout and preamble
Daniel
xracoonx at gmx.de
Wed Aug 10 02:15:30 UTC 2022
It would be nice if LyX's source editors (for Local Layout and LaTeX
Preamble) would have proper indentation and (un)commenting support.
I know that the external editing is supported now, but I consider this
more of a pro feature since it presupposes already having set up an
editor (other than the standard Windows and macOS text editors) and even
then it seems often unnecessary cumbersome to use.
In the attached Qt project, I implemented those features. It probably
needs some more cleaning up. But it seems to work and you could already
try it out if you like. The (un)commenting feature leans heavily on code
from QtCreator. (I tried to improve a bit upon it, e.g. comments are
added at the deepest common indentation as in
Begin
% Comment
Code
End
and it is possible to start a comment in an empty line. Both seem to me
quite a bit of an oversight in Qt Creator.)
If there is interest, what I would at least need help with for bringing
this over to LyX is a basic setup of the "GuiSourceEdit" class. I tried
it but failed (linker error). I guess it should be in its own
h/.cpp file. It could already have the constructor as in the attached
"mainwindow.h" which is code from the source text edits in
"GuiDocument.cpp". I could then add all the other stuff when it is ready
but, first, I wanted to make sure that there is some interest.
Daniel
-------------- next part --------------
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QKeyEvent>
#include <QTextCursor>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextEdit>
#include <QDebug>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
class GuiSourceEdit : public QTextEdit
{
Q_OBJECT
public:
GuiSourceEdit(QWidget * parent) : QTextEdit(parent) {
int const tabStop = 4;
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 keyPressEvent(QKeyEvent *event)
{
QTextCursor cursor = textCursor();
QTextDocument *doc = cursor.document();
int pos = cursor.position();
int anchor = cursor.anchor();
int start = qMin(anchor, pos);
int end = qMax(anchor, pos);
QTextBlock startBlock = doc->findBlock(start);
QTextBlock endBlock = doc->findBlock(end);
if ((event->modifiers() & Qt::ControlModifier) && event->key() == Qt::Key_Slash) {
unCommentSelection(cursor, "%");
} else if (event->key() == Qt::Key_Tab && startBlock != endBlock) {
indent(cursor);
} else if (event->key() == Qt::Key_Backtab && startBlock != endBlock) {
indent(cursor, true);
} else {
return QTextEdit::keyPressEvent(event);
}
}
QTextCursor indent(const QTextCursor &cursorIn, bool unIndent = false) {
QTextCursor cursor = cursorIn;
QTextDocument *doc = cursor.document();
cursor.beginEditBlock();
int pos = cursor.position();
int anchor = cursor.anchor();
int start = qMin(anchor, pos);
int end = qMax(anchor, pos);
bool anchorIsStart = (anchor == start);
QTextBlock startBlock = doc->findBlock(start);
QTextBlock endBlock = doc->findBlock(end);
endBlock = endBlock.next();
if (end > start && endBlock.position() == end) {
--end;
endBlock = endBlock.previous();
}
bool hasSelection = cursor.hasSelection();
if (unIndent) {
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
if (block.text().at(0) == '\t') {
cursor.setPosition(block.position());
cursor.movePosition(QTextCursor::NextCharacter,
QTextCursor::KeepAnchor, 1);
cursor.removeSelectedText();
}
}
} else {
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
cursor.setPosition(block.position());
cursor.insertText("\t", QTextCharFormat());
}
}
cursor.endEditBlock();
cursor = cursorIn;
if (hasSelection && !unIndent) {
start = startBlock.position();
int lastSelPos = anchorIsStart ? cursor.position() : cursor.anchor();
if (anchorIsStart) {
cursor.setPosition(start);
cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
} else {
cursor.setPosition(lastSelPos);
cursor.setPosition(start, QTextCursor::KeepAnchor);
}
}
return cursor;
}
// Slightly modified version of Qt Creator's unCommentSelection
QTextCursor unCommentSelection(const QTextCursor &cursorIn, QString singleLine)
{
QTextCursor cursor = cursorIn;
QTextDocument *doc = cursor.document();
cursor.beginEditBlock();
int pos = cursor.position();
int anchor = cursor.anchor();
int start = qMin(anchor, pos);
int end = qMax(anchor, pos);
bool anchorIsStart = (anchor == start);
QTextBlock startBlock = doc->findBlock(start);
QTextBlock endBlock = doc->findBlock(end);
if (end > start && endBlock.position() == end) {
--end;
endBlock = endBlock.previous();
}
bool hasSelection = cursor.hasSelection();
bool oneBlock = startBlock == endBlock;
endBlock = endBlock.next();
bool doUncomment = true;
// Check whether uncommenting
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
QString text = block.text().trimmed();
if ((oneBlock || !text.isEmpty()) && !text.startsWith(singleLine)) {
doUncomment = false;
break;
}
}
// Determine for minimal indentation (tabs)
int minIndent = 1000;
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
const QString text = block.text();
int tabs = 0;
for (QChar c : text) {
if (c == QChar::Tabulation) {
tabs++;
}
if (!c.isSpace()) {
minIndent = qMin(minIndent, tabs);
break;
}
}
}
if (minIndent == 1000) minIndent = 0;
const int singleLineLength = singleLine.length();
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
if (doUncomment) {
QString text = block.text();
int i = 0;
while (i <= text.size() - singleLineLength) {
if (isComment(text, i, singleLine)) {
// Check for whether there is a space after the comment
bool hasSpace = false;
if (text.size() > i + 1 && text.at(i + 1) == ' ')
hasSpace = true;
cursor.setPosition(block.position() + i);
cursor.movePosition(QTextCursor::NextCharacter,
QTextCursor::KeepAnchor,
singleLineLength + (hasSpace ? 1 : 0));
cursor.removeSelectedText();
break;
}
if (!text.at(i).isSpace())
break;
++i;
}
} else {
if (!block.text().trimmed().isEmpty() || oneBlock) {
cursor.setPosition(block.position() + minIndent);
// Insert comment string with space and without formatting
cursor.insertText(singleLine + " ", QTextCharFormat());
}
}
}
cursor.endEditBlock();
cursor = cursorIn;
// adjust selection when commenting out
if (hasSelection && !doUncomment) {
start = startBlock.position(); // move the comment into the selection
int lastSelPos = anchorIsStart ? cursor.position() : cursor.anchor();
if (anchorIsStart) {
cursor.setPosition(start);
cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
} else {
cursor.setPosition(lastSelPos);
cursor.setPosition(start, QTextCursor::KeepAnchor);
}
}
return cursor;
}
static bool isComment(const QString &text, int index,
const QString &commentType)
{
const int length = commentType.length();
Q_ASSERT(text.length() - index >= length);
int i = 0;
while (i < length) {
if (text.at(index + i) != commentType.at(i))
return false;
++i;
}
return true;
}
};
#endif // MAINWINDOW_H
-------------- next part --------------
A non-text attachment was scrubbed...
Name: mainwindow.ui
Type: text/xml
Size: 1886 bytes
Desc: not available
URL: <http://lists.lyx.org/pipermail/lyx-devel/attachments/20220810/0296c6a4/attachment-0001.xml>
-------------- next part --------------
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFont>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->textEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}
MainWindow::~MainWindow()
{
delete ui;
}
-------------- next part --------------
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
-------------- next part --------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
More information about the lyx-devel
mailing list