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
#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
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>395</width>
    <height>373</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="0" column="0">
     <widget class="GuiSourceEdit" name="textEdit">
      <property name="font">
       <font>
        <family>Courier</family>
       </font>
      </property>
      <property name="html">
       <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Courier'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>395</width>
     <height>22</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>GuiSourceEdit</class>
   <extends>QTextEdit</extends>
   <header>mainwindow.h</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>
#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;
}
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
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
-- 
lyx-devel mailing list
lyx-devel@lists.lyx.org
http://lists.lyx.org/mailman/listinfo/lyx-devel

Reply via email to