Hi guys,
In the example in the attachment (lexer_change.py) when reseting the lexer of
the QSciScintilla editor
by pressing the button RESET LEXER on the top, the margin font gets reset to
the default and brace matching
becomes weird (the braces move to the left when the cursor is on a brace-pair).
In the attached example this happens always, but I have code where this does
not happen, but the code is more or less the same.
Am I missing something or is this a bug?
Specs:
Windows 10
PyQt 5.14.0 or PyQt 5.14.1
QScintilla 2.11.4
Thanks,
Matic
# Import the PyQt5 module with some of the GUI widgets
import PyQt5.QtWidgets
# Import the QScintilla module
import PyQt5.Qsci
# Import Python's sys module needed to get the application arguments
import sys
import re
# Custom editor
class Editor(PyQt5.Qsci.QsciScintilla):
def __init__(self, parent=None):
super().__init__(parent)
self.setMarginType(0, PyQt5.Qsci.QsciScintilla.NumberMargin)
self.setMarginWidth(0, "000000")
self.setMarginsFont(PyQt5.QtGui.QFont('Courier', 10))
self.setBraceMatching(PyQt5.Qsci.QsciScintilla.SloppyBraceMatch)
self.setMatchedBraceBackgroundColor(PyQt5.QtGui.QColor(255, 153, 0))
# Create a custom Nim lexer
class LexerNim(PyQt5.Qsci.QsciLexerCustom):
styles = {
"Default" : 0,
"Keyword" : 1,
"Unsafe" : 2,
"MultilineComment" : 3,
}
keyword_list = [
"block", "const", "export", "import", "include", "let",
"static", "type", "using", "var", "when",
"as", "atomic", "bind", "sizeof",
"break", "case", "continue", "converter",
"discard", "distinct", "do", "echo", "elif", "else", "end",
"except", "finally", "for", "from", "defined",
"if", "interface", "iterator", "macro", "method", "mixin",
"of", "out", "proc", "func", "raise", "ref", "result",
"return", "template", "try", "inc", "dec", "new", "quit",
"while", "with", "without", "yield", "true", "false",
"assert", "min", "max", "newseq", "len", "pred", "succ",
"contains", "cmp", "add", "del","deepcopy", "shallowcopy",
"abs", "clamp", "isnil", "open", "reopen", "close","readall",
"readfile", "writefile", "endoffile", "readline", "writeline",
]
unsafe_keyword_list = [
"asm", "addr", "cast", "ptr", "pointer", "alloc", "alloc0",
"allocshared0", "dealloc", "realloc", "nil", "gc_ref",
"gc_unref", "copymem", "zeromem", "equalmem", "movemem",
"gc_disable", "gc_enable",
]
def __init__(self, parent=None):
# Initialize superclass
super().__init__(parent)
# Set the default style values
self.setDefaultColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x00))
self.setDefaultPaper(PyQt5.QtGui.QColor(0xff, 0xff, 0xff))
self.setDefaultFont(PyQt5.QtGui.QFont("Courier", 8))
# Initialize all style colors
self.init_colors()
# Init the fonts
for i in range(len(self.styles)):
if i == self.styles["Keyword"]:
# Make keywords bold
self.setFont(PyQt5.QtGui.QFont("Courier", 8,
weight=PyQt5.QtGui.QFont.Black), i)
else:
self.setFont(PyQt5.QtGui.QFont("Courier", 8), i)
# Set the Keywords style to be clickable with hotspots
# using the scintilla low level messaging system
parent.SendScintilla(
PyQt5.Qsci.QsciScintillaBase.SCI_STYLESETHOTSPOT,
self.styles["Keyword"],
True
)
parent.SendScintilla(
PyQt5.Qsci.QsciScintillaBase.SCI_SETHOTSPOTACTIVEFORE,
True,
PyQt5.QtGui.QColor(0x00, 0x7f, 0xff)
)
parent.SendScintilla(
PyQt5.Qsci.QsciScintillaBase.SCI_SETHOTSPOTACTIVEUNDERLINE, True
)
# Define a hotspot click function
def hotspot_click(position, modifiers):
"""
Simple example for getting the clicked token
"""
text = parent.text()
delimiters = [
'(', ')', '[', ']', '{', '}', ' ', '.', ',', ';', '-',
'+', '=', '/', '*', '#'
]
start = 0
end = 0
for i in range(position+1, len(text)):
if text[i] in delimiters:
end = i
break
for i in range(position,-1,-1):
if text[i] in delimiters:
start = i
break
clicked_token = text[start:end].strip()
# Print the token and replace it with the string "CLICK"
print("'" + clicked_token + "'")
parent.setSelection(0, start+1, 0, end)
parent.replaceSelectedText("CLICK")
# Attach the hotspot click signal to a predefined function
parent.SCN_HOTSPOTCLICK.connect(hotspot_click)
def init_colors(self):
# Font color
self.setColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x00),
self.styles["Default"])
self.setColor(PyQt5.QtGui.QColor(0x00, 0x00, 0x7f),
self.styles["Keyword"])
self.setColor(PyQt5.QtGui.QColor(0x7f, 0x00, 0x00),
self.styles["Unsafe"])
self.setColor(PyQt5.QtGui.QColor(0x7f, 0x7f, 0x00),
self.styles["MultilineComment"])
# Paper color
for i in range(len(self.styles)):
self.setPaper(PyQt5.QtGui.QColor(0xff, 0xff, 0xff), i)
def language(self):
return "Nim"
def description(self, style):
if style < len(self.styles):
description = "Custom lexer for the Nim programming languages"
else:
description = ""
return description
def styleText(self, start, end):
# Initialize the styling
self.startStyling(start)
# Tokenize the text that needs to be styled using regular expressions.
# To style a sequence of characters you need to know the length of the
sequence
# and which style you wish to apply to the sequence. It is up to the
implementer
# to figure out which style the sequence belongs to.
# THE PROCEDURE SHOWN BELOW IS JUST ONE OF MANY!
splitter = re.compile(r"(\{\.|\.\}|\#|\'|\"\"\"|\n|\s+|\w+|\W)")
# Scintilla works with bytes, so we have to adjust the start and end
boundaries.
# Like all Qt objects the lexers parent is the QScintilla editor.
text = bytearray(self.parent().text(),
"utf-8")[start:end].decode("utf-8")
tokens = [
(token, len(bytearray(token, "utf-8")))
for token in splitter.findall(text)
]
# Multiline styles
multiline_comment_flag = False
# Check previous style for a multiline style
if start != 0:
previous_style = editor.SendScintilla(editor.SCI_GETSTYLEAT, start
- 1)
if previous_style == self.styles["MultilineComment"]:
multiline_comment_flag = True
# Style the text in a loop
for i, token in enumerate(tokens):
if multiline_comment_flag == False and token[0] == "#" and
tokens[i+1][0] == "[":
# Start of a multiline comment
self.setStyling(token[1], self.styles["MultilineComment"])
# Set the multiline comment flag
multiline_comment_flag = True
elif multiline_comment_flag == True:
# Multiline comment flag is set
self.setStyling(token[1], self.styles["MultilineComment"])
# Check if a multiline comment ends
if token[0] == "#" and tokens[i-1][0] == "]":
multiline_comment_flag = False
elif token[0] in self.keyword_list:
# Keyword
self.setStyling(token[1], self.styles["Keyword"])
elif token[0] in self.unsafe_keyword_list:
# Keyword
self.setStyling(token[1], self.styles["Unsafe"])
else:
# Style with the default style
self.setStyling(token[1], self.styles["Default"])
# Create the main PyQt application object
application = PyQt5.QtWidgets.QApplication(sys.argv)
# Create a QScintila editor instance
editor = Editor()
# Set the lexer to the custom Nim lexer
nim_lexer = LexerNim(editor)
editor.setLexer(nim_lexer)
# Set the initial text with some Nim code
editor.setText(
"""
proc python_style_text*(self, args: PyObjectPtr): PyObjectPtr {.exportc,
cdecl.} =
var
result_object: PyObjectPtr
value_start, value_end: cint
lexer, editor: PyObjectPtr
parse_result: cint
parse_result = argParseTuple(
args,
"iiOO",
addr(value_start),
addr(value_end),
addr(lexer),
addr(editor),
)
#[
if parse_result == 0:
echo "Napaka v pretvarjanju argumentov v funkcijo!"
returnNone()
]#
var
text_method = objectGetAttr(editor, buildValue("s", cstring("text")))
text_object = objectCallObject(text_method, tupleNew(0))
string_object = unicodeAsEncodedString(text_object, "utf-8", nil)
cstring_whole_text = bytesAsString(string_object)
whole_text = $cstring_whole_text
text = whole_text[int(value_start)..int(value_end-1)]
text_length = len(text)
current_token: string = ""
# Prepare the objects that will be called as functions
var
start_styling_obj: PyObjectPtr
start_args: PyObjectPtr
set_styling_obj: PyObjectPtr
set_args: PyObjectPtr
send_scintilla_obj: PyObjectPtr
send_args: PyObjectPtr
start_styling_obj = objectGetAttr(lexer, buildValue("s",
cstring("startStyling")))
start_args = tupleNew(1)
set_styling_obj = objectGetAttr(lexer, buildValue("s",
cstring("setStyling")))
set_args = tupleNew(2)
send_scintilla_obj = objectGetAttr(editor, buildValue("s",
cstring("SendScintilla")))
send_args = tupleNew(2)
# Template for final cleanup
template clean_up() =
xDecref(text_method)
xDecref(text_object)
xDecref(string_object)
xDecref(args)
xDecref(result_object)
xDecref(start_styling_obj)
xDecref(start_args)
xDecref(set_styling_obj)
xDecref(set_args)
xDecref(send_scintilla_obj)
xDecref(send_args)
# Template for the lexers setStyling function
template set_styling(length: int, style: int) =
discard tupleSetItem(set_args, 0, buildValue("i", length))
discard tupleSetItem(set_args, 1, buildValue("i", style))
discard objectCallObject(set_styling_obj, set_args)
# Procedure for getting previous style
proc get_previous_style(): int =
discard tupleSetItem(send_args, 0, buildValue("i", SCI_GETSTYLEAT))
discard tupleSetItem(send_args, 1, buildValue("i", value_start - 1))
result = longAsLong(objectCallObject(send_scintilla_obj, send_args))
xDecref(send_args)
# Template for starting styling
template start_styling() =
discard tupleSetItem(start_args, 0, buildValue("i", value_start))
discard objectCallObject(start_styling_obj, start_args)
# Safety
if set_styling_obj == nil:
raise newException(FieldError, "Lexer doesn't contain the 'setStyling'
method!")
elif start_styling_obj == nil:
raise newException(FieldError, "Lexer doesn't contain the
'startStyling' method!")
elif send_scintilla_obj == nil:
raise newException(FieldError, "Editor doesn't contain the
'SendScintilla' method!")
# Styling initialization
start_styling()
#------------------------------------------------------------------------------
var
actseq = SeqActive(active: false)
token_name: string = ""
previous_token: string = ""
token_start: int = 0
token_length: int = 0
# Check previous style
if value_start != 0:
var previous_style = get_previous_style()
for i in multiline_sequence_list:
if previous_style == i.style:
actseq.sequence = i
actseq.active = true
break
# Style the tokens accordingly
proc check_start_sequence(pos: int, sequence: var SeqActive): bool =
for s in sequence_lists:
var found = true
for i, ch in s.start.pairs:
if text[pos+i] != ch:
found = false
break
if found == false:
continue
sequence.sequence = s
return true
return false
proc check_stop_sequence(pos: int, actseq: SeqActive): bool =
if text[pos] in actseq.sequence.stop_extra:
return true
if pos > 0 and (text[pos-1] in actseq.sequence.negative_lookbehind):
return false
for i, s in actseq.sequence.stop.pairs:
if text[pos+i] != s:
return false
return true
template style_token(token_name: string, token_length: int) =
if token_length > 0:
if token_name in keywords:
set_styling(token_length, styles["Keyword"])
previous_token = token_name
elif token_name in custom_keywords:
set_styling(token_length, styles["CustomKeyword"])
elif token_name[0].isdigit() or (token_name[0] == '.' and
token_name[1].isdigit()):
set_styling(token_length, styles["Number"])
elif previous_token == "class":
set_styling(token_length, styles["ClassName"])
previous_token = ""
elif previous_token == "def":
set_styling(token_length, styles["FunctionMethodName"])
previous_token = ""
else:
set_styling(token_length, styles["Default"])
var i = 0
token_start = i
while i < text_length:
if actseq.active == true or check_start_sequence(i, actseq) == true:
#[
Multiline sequence already started in the previous line or
a start sequence was found
]#
if actseq.active == false:
# Style the currently accumulated token
token_name = text[token_start..i]
token_length = i - token_start
style_token(token_name, token_length)
# Set the states and reset the flags
token_start = i
token_length = 0
if actseq.active == false:
i += len(actseq.sequence.start)
while i < text_length:
# Check for end of comment
if check_stop_sequence(i, actseq) == true:
i += len(actseq.sequence.stop)
break
i += 1
# Style text
token_length = i - token_start
set_styling(token_length, actseq.sequence.style)
# Style the separator tokens after the sequence, if any
token_start = i
while text[i] in extended_separators and i < text_length:
i += 1
token_length = i - token_start
if token_length > 0:
set_styling(token_length, styles["Default"])
# Set the states and reset the flags
token_start = i
token_length = 0
# Skip to the next iteration, because the index is already
# at the position at the end of the string
actseq.active = false
continue
elif text[i] in extended_separators:
#[
Separator found
]#
token_name = text[token_start..i-1]
token_length = len(token_name)
if token_length > 0:
style_token(token_name, token_length)
token_start = i
while text[i] in extended_separators and i < text_length:
i += 1
token_length = i - token_start
if token_length > 0:
set_styling(token_length, styles["Default"])
# State reset
token_start = i
token_length = 0
continue
else:
while text[i] in extended_separators and i < text_length:
i += 1
token_length = i - token_start
if token_length > 0:
set_styling(token_length, styles["Default"])
# State reset
token_start = i
token_length = 0
continue
# Update loop variables
inc(i)
# Check for end of text
if i == text_length:
token_name = text[token_start..i-1]
token_length = len(token_name)
style_token(token_name, token_length)
elif i > text_length:
raise newException(IndexError, "Styling went over the text length
limit!")
#------------------------------------------------------------------------------
clean_up()
returnNone()
"""
)
# Lexer reset button
button = PyQt5.QtWidgets.QPushButton("RESET LEXER")
def reset_lexer(*args):
nim_lexer = LexerNim(editor)
editor.setLexer(nim_lexer)
button.clicked.connect(reset_lexer)
gb = PyQt5.QtWidgets.QGroupBox()
layout = PyQt5.QtWidgets.QVBoxLayout()
gb.setLayout(layout)
layout.addWidget(button)
layout.addWidget(editor)
# For the QScintilla editor to properly process events we need to add it to
# a QMainWindow object.
main_window = PyQt5.QtWidgets.QMainWindow()
# Set the central widget of the main window to be the editor
main_window.setCentralWidget(gb)
# Resize the main window and show it on the screen
main_window.resize(800, 600)
main_window.show()
# Execute the application
application.exec_()
_______________________________________________
QScintilla mailing list
[email protected]
https://www.riverbankcomputing.com/mailman/listinfo/qscintilla