pythonpyqtfoldingscintillaqscintilla

How to implement SublimeText fold-by-level feature in QScintilla


I'm trying to implement the fold_by_level SublimeText3 feature on a QScintilla component but I don't know very well how to do it, so far I've come up with this code:

import sys
import re
import math

from PyQt5.Qt import *  # noqa

from PyQt5.Qsci import QsciScintilla
from PyQt5 import Qsci
from PyQt5.Qsci import QsciLexerCPP


class Foo(QsciScintilla):

    def __init__(self, parent=None):
        super().__init__(parent)

        # http://www.scintilla.org/ScintillaDoc.html#Folding
        self.setFolding(QsciScintilla.BoxedTreeFoldStyle)

        # Indentation
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setBackspaceUnindents(True)
        self.setIndentationGuides(True)

        # Set the default font
        self.font = QFont()
        self.font.setFamily('Consolas')
        self.font.setFixedPitch(True)
        self.font.setPointSize(10)
        self.setFont(self.font)
        self.setMarginsFont(self.font)

        # Margin 0 is used for line numbers
        fontmetrics = QFontMetrics(self.font)
        self.setMarginsFont(self.font)
        self.setMarginWidth(0, fontmetrics.width("000") + 6)
        self.setMarginLineNumbers(0, True)
        self.setMarginsBackgroundColor(QColor("#cccccc"))

        # Indentation
        self.setIndentationsUseTabs(False)
        self.setIndentationWidth(4)
        self.setBackspaceUnindents(True)

        lexer = QsciLexerCPP()
        lexer.setFoldAtElse(True)
        lexer.setFoldComments(True)
        lexer.setFoldCompact(False)
        lexer.setFoldPreprocessor(True)
        self.setLexer(lexer)

        QShortcut(QKeySequence("Ctrl+K, Ctrl+J"), self,
                  lambda level=-1: self.fold_by_level(level))
        QShortcut(QKeySequence("Ctrl+K, Ctrl+1"), self,
                  lambda level=1: self.fold_by_level(level))
        QShortcut(QKeySequence("Ctrl+K, Ctrl+2"), self,
                  lambda level=2: self.fold_by_level(level))
        QShortcut(QKeySequence("Ctrl+K, Ctrl+3"), self,
                  lambda level=3: self.fold_by_level(level))
        QShortcut(QKeySequence("Ctrl+K, Ctrl+4"), self,
                  lambda level=4: self.fold_by_level(level))
        QShortcut(QKeySequence("Ctrl+K, Ctrl+5"), self,
                  lambda level=5: self.fold_by_level(level))

    def fold_by_level(self, lvl):
        if lvl < 0:
            self.foldAll(True)
        else:
            for i in range(self.lines()):
                level = self.SendScintilla(
                    QsciScintilla.SCI_GETFOLDLEVEL, i) & QsciScintilla.SC_FOLDLEVELNUMBERMASK
                level -= 0x400
                print(f"line={i+1}, level={level}")
                if lvl == level:
                    self.foldLine(i)


def main():
    app = QApplication(sys.argv)
    ex = Foo()
    ex.setText("""\
#include <iostream>
using namespace std;

void Function0() {
    cout << "Function0";
}

void Function1() {
    cout << "Function1";
}

void Function2() {
    cout << "Function2";
}

void Function3() {
    cout << "Function3";
}


int main(void) {
    if (1) {
        if (1) {
            if (1) {
                if (1) {
                    int yay;
                }
            }
        }
    }

    if (1) {
        if (1) {
            if (1) {
                if (1) {
                    int yay2;
                }
            }
        }
    }

    return 0;
}\
    """)
    ex.resize(800, 600)
    ex.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

The docs I've followed are https://www.scintilla.org/ScintillaDoc.html#Folding and http://pyqt.sourceforge.net/Docs/QScintilla2/classQsciScintilla.html.

As I said, the fold_by_level feature is intended to behave exactly like SublimeText but I'm unsure about ST's feature implementation details. In any case, let me post some screenshots after testing some basic sequences on SublimeText that could clarify a bit what I'm trying to achieve here:

Sequence1: {ctrl+k, ctrl+5}, {ctrl+k, ctrl+j} {ctrl+k, ctrl+4}, {ctrl+k, ctrl+j} {ctrl+k, ctrl+3}, {ctrl+k, ctrl+j} {ctrl+k, ctrl+2}, {ctrl+k, ctrl+j} {ctrl+k, ctrl+1}, {ctrl+k, ctrl+j}

enter image description here

Sequence2: {ctrl+k, ctrl+5}, {ctrl+k, ctrl+4}, {ctrl+k, ctrl+3}, {ctrl+k, ctrl+2}, {ctrl+k, ctrl+1}

enter image description here

I'm sure there are more inner details on SublimeText behaviour but if my example behaved exactly like posted on those shots after testing the sequences you could say the feature has become quite handy to use.


Solution

  • The problems with your example are mostly caused by some poor naming in the QsciScintilla APIs. The foldLine and foldAll methods should really be called toggleFoldLine and toggleFoldAll, because they actually undo the previous state. This means, for instance, that if two successive lines have the same fold level, calling foldLine twice will result in no net changes.

    In the implementation below, I have used the more explicit Scintilla messages so that only the lines which really require folding are affected. I have also changed the keyboard shortcuts to match the defaults in SublimeText:

    class Foo(QsciScintilla):
        def __init__(self, parent=None):
            ...
            QShortcut(QKeySequence("Ctrl+K, Ctrl+J"), self, self.fold_by_level)
            QShortcut(QKeySequence("Ctrl+K, Ctrl+0"), self, self.fold_by_level)
            ...
    
        def fold_by_level(self, level=0):
            SCI = self.SendScintilla
            if level:
                level += 0x400
                MASK = QsciScintilla.SC_FOLDLEVELNUMBERMASK
                for line in range(self.lines()):
                    foldlevel = SCI(QsciScintilla.SCI_GETFOLDLEVEL, line) & MASK
                    print('line=%i, level=%i' % (line + 1, foldlevel), end='')
                    if foldlevel == level:
                        line = SCI(QsciScintilla.SCI_GETFOLDPARENT, line)
                        if SCI(QsciScintilla.SCI_GETFOLDEXPANDED, line):
                            print(', foldline:', line + 1, end='')
                            SCI(QsciScintilla.SCI_FOLDLINE, line,
                                QsciScintilla.SC_FOLDACTION_CONTRACT)
                    print()
            else:
                SCI(QsciScintilla.SCI_FOLDALL, QsciScintilla.SC_FOLDACTION_EXPAND)