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}
Sequence2: {ctrl+k, ctrl+5}, {ctrl+k, ctrl+4}, {ctrl+k, ctrl+3}, {ctrl+k, ctrl+2}, {ctrl+k, ctrl+1}
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.
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)