Introduction
QCodeEditor
is a code editor supporting auto completion, syntax highlighting and line numbering. It is directed to all people who want to support a wide set of languages, ranging from programming languages to markup languages and even custom scripting languages. In its current state, it can perfectly handle all the features mentioned above, but there are still many things to be implemented. QCodeEditor
is based on Qt's QPlainTextEdit
that already contains an interface for adding syntax highlighting and auto completion.
Using the Code
Using the editor itself is quite simple. It can either be done by adding a QPlainTextEdit
to the form and promoting it to kgl::QCodeEditor
(note that 'include/KGL/Widgets' has to be added to the INCLUDEPATH
variable and that the 'global include
' checkbox has to be checked) or by adding it programmatically to the form:
using namespace kgl;
QCodeEditor *editor = new QCodeEditor;
setCentralWidget(editor);
There are many possibilities to change the visual appearance of the editor by using the QCodeEditorDesign
class. In the following example, we assume that the code editor is surrounded by multiple widgets and therefore add a border to it. We additionally modify the appearance to have a 'dark' style:
using namespace kgl;
QCodeEditorDesign design;
design.setLineColumnVisible(false);
design.setEditorBackColor(0xff333333);
design.setEditorTextColor(0xffdddddd);
design.setEditorBorderColor(0xff999999);
design.setEditorBorder(QMargins(1,1,1,1)); editor->setDesign(design);
Click here to see the above code in action. Click here for a visual property reference.
But how to actually add some syntax highlighting rules, as shown in the picture above? There are two ways to do it: To add them programmatically or to extract them out of an XML file.
Programmatically:
using namespace kgl;
QList<QSyntaxRule> rules;
QSyntaxRule rule;
rule.setForeColor(QColor(Qt::blue));
rule.setRegex("\\bFoo\\b");
rule.setId("Foo");
rules.push_back(rule);
editor->setRules(rules);
XML file:
using namespace kgl;
QCodeEditorDesign design;
QList<QSyntaxRule> rules =
QSyntaxRules::loadFromFile(":/rule_cpp.xml", design);
editor->setRules(rules);
A guide of how to create these XML rules will be provided in the next chapter. But first, our editor requires some auto completion keywords:
QStringList keywords = { "printf", "scanf" };
editor->setKeywords(keywords);
If one wishes to add icons in order to imply that a keyword is a function/member/macro/... a custom QStandardItemModel
needs to be created and passed to 'QCodeEditor::setKeywordModel(model)
'.
Creating the XML Rules File
The XML rules file contains a topmost <rules>
element that consists of multiple <rule>
child elements. Each of the child element has to contain either a regular expression or a list of keywords, all other properties are optional:
<rules>
<rule>
<regex>\bFoo\b</regex>
<keywords>foo bar key</keywords>
</rule>
</rules>
For a reference of all the available properties, go to the github page of the rules_template.xml. QCodeEditor
even supports rules that consist of more than just one line. While they are useful for implementing multi-line comments, they can serve other purposes, too.
The Usefulness of IDs
As can be seen in the rules_template.xml, rules can even define a custom ID. In this section, I will illustrate how to use IDs and why they are so useful. One might have noticed that adding keywords statically is not a nice practice, especially if you have a language that allows one to include other files or define variables.
The 'onMatch' Signal
QCodeEditorHighlighter
emits a signal called 'onMatch
' as soon as a string
- found via regex - was highlighted. This allows us to retrieve the string
in question:
using namespace kgl;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
...
private slots:
void addMacro(const QSyntaxRule &rule, QString seq, QTextBlock block);
private:
QMap<QTextBlock, QSyntaxRule> m_RuleMap;
QMap<QTextBlock, QString> m_MacroMap;
QCodeEditor *m_Editor;
};
QSyntaxRule defineRule;
defineRule.setRegex("(#define\\s+)\\K(\\D\\w*)(?=\s+\S+)");
defineRule.setId("define");
editor->setRules({ defineRule });
connect(m_Editor->highlighter(), SIGNAL(onMatch(QSyntaxRule,QString,QTextBlock)),
this, SLOT(addMacro(QSyntaxRule,QString,QTextBlock)));
if (rule.id() == "define") {
foreach (const QTextBlock &b, m_RuleMap.keys()) {
if (b.userData() != NULL && block.userData() != NULL) {
auto *d1 = static_cast<QCodeEditorBlockData *>(block.userData());
auto *d2 = static_cast<QCodeEditorBlockData *>(b.userData());
if (d1->id == d2->id) {
return;
}
}
}
QString def = seq.split(' ').at(0);
m_RuleMap.insert(block, rule);
m_MacroMap.insert(block, def);
m_Editor->addKeyword(def);
}
This way, one can provide auto-completion for custom classes, variables and definitions or include other files and import symbols from them.
The 'onRemove' Signal
Removing a once added macro can be a bit tricky because the design of the QTextBlock gives us little to no possibility to track it. QCodeEditor
Highlighter
provides the 'onRemove
' signal, that is emitted once the highlighter detects that a previously matched rule does not match anymore:
private slots:
void addMacro(const QSyntaxRule &rule, QString seq, QTextBlock block);
void removeMacro(QCodeEditorBlockData *data);
connect(m_Editor->highlighter(), SIGNAL(onRemove(QCodeEditorBlockData*)),
this, SLOT(removeMacro(QCodeEditorBlockData*)));
foreach (const QTextBlock &b, m_RuleMap.keys()) {
if (b.userData()) {
auto *d = static_cast<QCodeEditorBlockData *>(b.userData());
if (d->id == data->id) {
m_Editor->removeKeyword(m_MacroMap.value(b));
m_RuleMap.remove(b);
m_MacroMap.remove(b);
}
}
}
Realising such a signal that is relatively simple to use was a hard task. Check out the 'Points of Interest' chapter for more information.
In order to compile QCodeEditor, you need to define 'KGL_BUILD
' to export symbols to a dynamic library. If you want to build a static library instead, simply define 'KGL_STATIC
'. Also make sure you use a Qt version that is 5
or higher.
Points of Interest
One of the big obstacles was rendering the line numbers correctly. While adding the line column as child widget was quite easy, determining all the visible line numbers on scrolling was not. After reading the Qt documentation for a good amount of time, I figured I could just jump to the next line in an iteration and stop the iteration as soon as the current line is not visible anymore.
Another big obstacle was implementing indentation of multiple selected lines when the tabulator key is pressed. I solved it by using the truly amazing 'QTextCursor::movePosition
' method which made implementing this feature (and back-indentation) an easy job.
QTextBlock
Despite of QTextBlock's amazing features and possibilities, it still has one weakness: It is for us practically impossible to track a QTextBlock within a text widget. In order to implement a signal that lets one remove keywords, I first tried to copy the QTextBlock in question and later on check for equality with the overloaded '==' operator. That did not work because the line numbers can change and cause the equation to fail. The only possibility to track down the QTextBlock was assigning it a QTextBlockUserData using the 'setUserData
' function. To achieve that, I subclassed QTextBlockUserData and stored a uuid
and the regex string
in it. The uuid (+ some do-while loop) ensures that the block really stays unique across the entire application. With these measurements, the 'onRemove
' signal finally was reliable and bug-free.
Update 26th of October
A horrendous carelessness of mine caused everyone using the Microsoft Visual C++ compiler to have tons of compiler errors and warnings. It was my mistake to not point out that KGL_BUILD
must be specified in order to export the library's symbols. Another mistake caused all Windows users (regardless of compiling with MSVC or GCC) to define __declspec(dllimport)
. GCC ignored this and just quietly didn't create dynamic symbols but MSVC on the other hand cared about that. Take a look at the Compile Instructions section to now properly compile QCodeEditor. I am sorry for any inconveniences!
Update 29th of October
In the last version, removing once added keywords was hacky and complicated. With the newly introduced onRemove
signal, it should now be easy. For a complete demo, download the QCodeEditor_-_Example.zip
at the beginning of this article. It is capable of adding and removing macroes defined via C/C++'s #define
.
TODO
XML file parsing for QCodeEditorDesign
QCodeEditor::addKeyword
& QCodeEditor::removeKeyword
& QCodeEditor::keywordExists
onMatch
signal emitted along with current line number - Interface for a real-time code validator
- Other related widgets (search&replace, hotkey mapping, ...)
Suggestions are highly appreciated.
History
- October 16th, 2016: First release of
QCodeEditor
- October 26th, 2016: Fix various compilation bugs
- October 29th, 2016: XML file for editor design, dynamic keyword addition/deletion & enhanced
onMatch
/onRemove
signals