Adding a custom lexer with syntax highlighting and folding to a WxStyledTextCtrl
Introduction
This article describes how to write a custom "lexer" to a wxStyledTextCtrl (refered as "STC" in the article) including a sample for text folding.
The STC is basically a 1:1 mapping of the scintilla editor (http://www.scintilla.org). The STC handles text and uses a lexer to add syntax highlighting to this text. There are a lot of internal lexers for the most common uses.Internally, as soon as you scroll a page or modify it, scintilla calls a style routine to style that text. The lexer than applies that style to the text.
Implementing a lexer
You can set any of the included lexers by adding a lexer to the STC:
wxSTC->SetLexer(wxSTC_LEX_xxxx);
where xxxx stands for any of the compilers (like wxSTC_LEX_CPP for the C++ lexer).
Now what if we want to highlight text with a different language? In this example we use CNC G-Code. In case you are not familiar with it, it is used on CNC machines (like a mill or lathe). To make it simple, we highlight everything that is a "G-Code" which is a command to execute a specific action like switching to another work offset (G54).
Also, we use folding- "IF" increases the fold level, "ENDIF" decreases it (just like { and } do in C++). This is a bit tricky since "ENDIF" contains "IF".
The example is a bit shortened- keep in mind that in "real life" you would have to check if that "IF" is part of a comment or a string for example. But this doesn't matter for the example.
Performance is not critical- you could also use RegEx since drawing the screen takes a lot longer than actually parsing the few lines you see. However, you should still keep it as small and fast as possible.
So how do we do this? There is some "hook" that calls a custom style routine that you can set in your own part without modifying wxWidgets itself. It is the LEX_CONTAINER "lexer". If you set this, STC will call an event each time style is needed. So, set this:
STC->SetLexer(wxSTC_LEX_CONTAINER);
and then bind wxEVT_STC_STYLENEEDED to your own routine:
m_activeSTC->Bind(wxEVT_STC_STYLENEEDED, &myFrame::OnStyleNeeded, this);
This way, every time the text in the container is modified or unstyled text comes into sight, myFrame::OnStyleNeeded(wxStyledTextEvent& event) is called which then styles the text. You should be able to understand what styles and folding levels are and how to use them. A lot of it is explained in the scintilla documentation- unfortunately, many open projects lack a good "beginners guide" so I'll explain the basics here:
Styles
A style is applied to a text with a number (more precisely, stylebits) and a color as well as other formatings like bold text, foreground and background.
The number has to be lower than 32 (at least this has been the case in the documentation I have read). The lowest 3 styles seem to be used by extra stylings (like highlighting search occurences) so a number between 4 and 31 should be save.
The style is set with
STC->StyleSetForeground(19,wxcolor);
for example which will set the foreground color for style 19 to the color specified in wxcolor. You should do this before styling text in the document (like when constructing the STC).
Folding
Folding works by applying a fold level to each line. The lowest foldlevel is "0" (but: Since the fold bits don't start at bit 0, we need to add 1024 to this number). This means that this line will never be foldable. The first line that shall be visible when folding text (like the one containing the "IF") stays at that foldlevel, but is assigned a special flag, the wxSTC_FOLDLEVELHEADERFLAG. All following lines are assigned the higher fold level (1 until another IF comes -> increase or ENDIF -> decrease). When the folding ends by specifying ENDIF in a line, the line following that line receives the lower fold level (so the ENDIF line is folded as well). Folding always hides those lines containing the higher fold level when clicking the margin on the position of the header line (the one containing wxSTC_FOLDLEVELHEADERFLAG).
Margins
A margin is shown on the side of the document. It can hold markers, foldlines and similar items. In our case, we'll just add a margin, define the markers on it for folding and catch the event when someone clicks on it with
STC->Bind(wxEVT_STC_MARGINCLICK, &myFrame::onMarginClick, this);
to MyFrame::onMarginClick(wxStyledTextEvent& event) so this function is called whenever someone clicked the margin. This will toggle the fold level. As with styles and folding, refer to the scintilla and/ or wxwidgets documentation to gather more information- it would be too long to explain it in detail here. The below code for setting up the margin was provided by NewPagodi in the wxwidgets forum.
Writing your own "lexer"
Your own "lexer" consists of a style routine called by the wxEVT_STC_STYLENEEDED event. That's basically all. But we need some more things of course:
- A function for parsing your text and sorting out what color shall be applied (if any).
- A function for finding and calculating the fold levels.
- A function to finally apply styles and fold levels to the document.
A fast and dirty parser can be found in the example below- it is far from perfect, but should show the basics. Things you might have to consider:
- Keywords that are part of a comment should not be parsed or overridden by the comment style
- Keywords that are part of a string definition shouldn't affect folding levels
- Umlauts or other non-ascii signs can shift the found positions vs. real positions of a char since they are 2 bytes wide (you can possibly solve this by forcingly converting the search text to UTF8 for example in most cases).
All of those three might happen in the example below- but since we want to show the basics, we won't care about that for now.
Putting it all together in a small demo app
Define a small app:
// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"
#ifdef __BORLANDC__
#pragma hdrstop
#endif
// for all others, include the necessary headers (this file is usually all you
// need because it includes almost all "standard" wxWidgets headers)
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
//includes
#include <wx/stc/stc.h>
#include <vector>
#include <utility>
#include <algorithm>
#define MY_FOLDMARGIN 2
class myFrame : public wxFrame {
//be sure to enable the C++11 standard for this example if you're using GCC !!
public:
myFrame( wxWindow* parent, int id = wxID_ANY, wxString title = "Demo"
, wxPoint pos = wxDefaultPosition, wxSize size = wxSize(481,466)
, int style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL );
private:
void onMarginClick(wxStyledTextEvent& event);
void OnStyleNeeded(wxStyledTextEvent& event);
void highlightSTCsyntax(size_t fromPos,size_t toPos,wxString &text);
void setfoldlevels(size_t fromPos,int startfoldlevel,wxString &text);
wxStyledTextCtrl* m_activeSTC;
wxColor m_GCodecolor{255,130,0}; //color for highlighted parts
//this is the mask for styling- just remember to set this to 0 with >3.1.0
int m_stylemask=255; //for wxwidgets >3.1.0 set this to 0
};
Define the frame, set up the STC and its margins:
myFrame::myFrame( wxWindow* parent, int id, wxString title, wxPoint pos
, wxSize size, int style )
:wxFrame( parent, id, title, pos, size, style ) {
m_activeSTC = new wxStyledTextCtrl(this);
//set Lexer to LEX_CONTAINER: This will trigger the styleneeded event so you can do your own highlighting
m_activeSTC->SetLexer(wxSTC_LEX_CONTAINER);
//folding example by New Pagodi copied from WxWidgets Forum
//Set the fold marging to have a width of 14 pixels and give it a
//distinctive background
m_activeSTC->SetMarginWidth(MY_FOLDMARGIN,14);
m_activeSTC->SetMarginMask(MY_FOLDMARGIN,wxSTC_MASK_FOLDERS);
m_activeSTC->SetFoldMarginColour(true,wxColor(255,255,255));
m_activeSTC->SetFoldMarginHiColour(true,wxColor(233,233,233));
//Set up the markers that will be shown in the fold margin
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEREND,wxSTC_MARK_BOXPLUSCONNECTED);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEREND,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEREND,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEROPENMID,wxSTC_MARK_BOXMINUSCONNECTED);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEROPENMID,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEROPENMID,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERMIDTAIL, wxSTC_MARK_TCORNER);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERMIDTAIL,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERMIDTAIL,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERTAIL,wxSTC_MARK_LCORNER);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERTAIL,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERTAIL,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERSUB,wxSTC_MARK_VLINE);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERSUB,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERSUB,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDER,wxSTC_MARK_BOXPLUS);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDER,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDER,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEROPEN,wxSTC_MARK_BOXMINUS);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEROPEN,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEROPEN,wxColor(128,128,128));
//Turn the fold markers red when the caret is a line in the group (optional)
m_activeSTC->MarkerEnableHighlight(true);
//The margin will only respond to clicks if it set sensitive. Also, connect
//the event handler that will do the collapsing/restoring
m_activeSTC->SetMarginSensitive(MY_FOLDMARGIN,true);
m_activeSTC->Bind(wxEVT_STC_MARGINCLICK, &myFrame::onMarginClick, this);
m_activeSTC->Bind(wxEVT_STC_STYLENEEDED, &myFrame::OnStyleNeeded, this);
//set color for G-Code highlighting
m_activeSTC->StyleSetForeground(19,m_GCodecolor);
//enter some text and set fold levels
m_activeSTC->AppendText(";Foldtest\n");
m_activeSTC->AppendText("DEF BOOL TESTVAR=FALSE\n");
m_activeSTC->AppendText("IF (TESTVAR)\n");
m_activeSTC->AppendText(" G0 G54 M0\n");
m_activeSTC->AppendText("ENDIF\n");
m_activeSTC->AppendText("M30\n");
/*given the text above, folding should produce this output:
m_activeSTC->SetFoldLevel(0, 1024);
m_activeSTC->SetFoldLevel(1, 1024);
m_activeSTC->SetFoldLevel(2, 1024|wxSTC_FOLDLEVELHEADERFLAG); //header flag: one item before increasing fold level!
m_activeSTC->SetFoldLevel(3, 1025); //here comes the new fold level in line G0 G54 M0
m_activeSTC->SetFoldLevel(4, 1025); //the ENDIF
m_activeSTC->SetFoldLevel(5, 1024); //and this has the lower fold level again
m_activeSTC->SetFoldLevel(6, 1024|wxSTC_FOLDLEVELWHITEFLAG); //this is an empty line: set fold level white flag
note: If you load text into the styled text control for example with file load or like we did above:
style the whole document for once. If you don't, the document remains unstyled until you click some position below*/
wxString text=m_activeSTC->GetText().Upper(); //Upper() makes it case insensitive
this->highlightSTCsyntax(0,m_activeSTC->GetTextLength(),text);
this->setfoldlevels(0,0,text);
}
This is the function that toggles the fold levels. Basically we check if this is a header and the next line is hidden or not:
void myFrame::onMarginClick(wxStyledTextEvent& event) {
int margin = event.GetMargin();
int position = event.GetPosition();
int line = m_activeSTC->LineFromPosition(position);
int foldLevel = m_activeSTC->GetFoldLevel(line);
bool headerFlag = (foldLevel & wxSTC_FOLDLEVELHEADERFLAG)!=0;
if( margin==MY_FOLDMARGIN && headerFlag ) {
m_activeSTC->ToggleFold(line);
}
}
This is the function called whenever scintilla needs a style:
void myFrame::OnStyleNeeded(wxStyledTextEvent& event) {
/*this is called every time the styler detects a line that needs style, so we style that range.
This will save a lot of performance since we only style text when needed instead of parsing the whole file every time.*/
size_t line_end=m_activeSTC->LineFromPosition(m_activeSTC->GetCurrentPos());
size_t line_start=m_activeSTC->LineFromPosition(m_activeSTC->GetEndStyled());
/*fold level: May need to include the two lines in front because of the fold level these lines have- the line above
may be affected*/
if(line_start>1) {
line_start-=2;
} else {
line_start=0;
}
//if it is so small that all lines are visible, style the whole document
if(m_activeSTC->GetLineCount()==m_activeSTC->LinesOnScreen()){
line_start=0;
line_end=m_activeSTC->GetLineCount()-1;
}
if(line_end<line_start) {
//that happens when you select parts that are in front of the styled area
size_t temp=line_end;
line_end=line_start;
line_start=temp;
}
//style the line following the style area too (if present) in case fold level decreases in that one
if(line_end<m_activeSTC->GetLineCount()-1){
line_end++;
}
//get exact start positions
size_t startpos=m_activeSTC->PositionFromLine(line_start);
size_t endpos=(m_activeSTC->GetLineEndPosition(line_end));
int startfoldlevel=m_activeSTC->GetFoldLevel(line_start);
startfoldlevel &= wxSTC_FOLDFLAG_LEVELNUMBERS; //mask out the flags and only use the fold level
wxString text=m_activeSTC->GetTextRange(startpos,endpos).Upper();
//call highlighting function
this->highlightSTCsyntax(startpos,endpos,text);
//calculate and apply foldings
this->setfoldlevels(startpos,startfoldlevel,text);
}
Highlighting function:
void myFrame::highlightSTCsyntax(size_t fromPos,size_t toPos, wxString &text) {
//this vector will hold the start and end position of each word to highlight
//if you want to highlight more than one, you should pass a whole class or struct containing the offsets
std::vector<std::pair<size_t,size_t>>GcodeVector;
//the following example is a quick and dirty parser for G-Codes.
//it just iterates through the Text Range and finds "Gxx" where xx is a digit.
//you could also use regex, but one can build a pretty fast routine based on single char evaluation
size_t actual_cursorpos = 0;
size_t startpos = 0;
size_t end_of_text = text.length();
bool word_boundary = true; //check for word boundary
char actualchar;
while (actual_cursorpos<end_of_text) {
actualchar= text[actual_cursorpos];
//check if syntax matches "G" followed by a couple of numbers
if((actualchar=='G')&&(word_boundary==true)) {
//this is a new G-Code, store startposition
startpos=actual_cursorpos;
word_boundary=false;
actual_cursorpos++;
if(actual_cursorpos<end_of_text) {
//refresh actual character
actualchar= text[actual_cursorpos];
}
//add digits
while((std::isdigit(actualchar)&&(actual_cursorpos<end_of_text))) {
actual_cursorpos++;
actualchar= text[actual_cursorpos];
}
//check if word boundary occurs at end of digits
if((actualchar==' ')||(actualchar=='\n')||(actualchar=='\r')||(actualchar=='\t')||(actual_cursorpos==end_of_text)) {
//success, append this one
if((actual_cursorpos-startpos)>1) {
//success, append to vector. DO NOT FORGET THE OFFSET HERE! We start from fromPos, so we need to add this
GcodeVector.push_back(std::make_pair(startpos+fromPos, actual_cursorpos+fromPos));
}
word_boundary=true;
}
}
if((actualchar==' ')||(actualchar=='\n')||(actualchar=='\r')||(actualchar=='\t')||(actual_cursorpos==end_of_text)) {
word_boundary=true;
}
actual_cursorpos++;
}
//remove old styling
m_activeSTC->StartStyling(fromPos,m_stylemask); //from here
m_activeSTC->SetStyling(toPos-fromPos,0); //with that length and style -> cleared
//now style the G-Codes
for (int i=0; i<GcodeVector.size(); i++) {
size_t startpos=GcodeVector[i].first;
size_t endpos=GcodeVector[i].second;
size_t length=(endpos-startpos);
m_activeSTC->StartStyling(startpos,m_stylemask);
m_activeSTC->SetStyling(length,19); //must match the style set above
}
}
Calculate and set fold levels:
void myFrame::setfoldlevels(size_t fromPos, int startfoldlevel, wxString& text) {
/*we'll increase the fold level with "IF" and decrease it with "ENDIF".
First, find all "IF" included in the text. Then we check if this IF is actually an ENDIF.
Keep in mind that you still need to check if this is actually commented out and so on.
This is a pretty simple and not perfect example to demonstrate basic folding*/
std::vector<size_t>if_positions;
size_t actual_cursorpos=0;
while ((actual_cursorpos<text.size())&&(actual_cursorpos!=wxNOT_FOUND)) {
actual_cursorpos=text.find("IF",actual_cursorpos+1);
if(actual_cursorpos!=wxNOT_FOUND) {
if_positions.push_back(actual_cursorpos+fromPos);
}
}
//build a vector to include line and folding level
//also, check if this IF is actually an ENDIF
std::vector<std::pair<size_t,int>>foldingvector;
int actualfoldlevel=startfoldlevel;
for(int i=0; i<if_positions.size(); i++) {
size_t this_line=m_activeSTC->LineFromPosition(if_positions[i]);
//check if that "IF" is an ENDIF
wxString endif_string;
if(if_positions[i]>3) { //if string is longer than 3
endif_string=text.substr(if_positions[i]-3-fromPos,5);
}
//if it's an IF the fold level increases, if it's an ENDIF the foldlevel decreases
if(endif_string=="ENDIF") {
actualfoldlevel--;
foldingvector.push_back(std::make_pair(this_line,actualfoldlevel));
} else {
actualfoldlevel++;
foldingvector.push_back(std::make_pair(this_line,actualfoldlevel));
}
}
//now that we know which lines shall influence folding, we can apply to folding level to the STC line for line
int foldlevel=startfoldlevel; //this is a temporary marker containing the foldlevel of that position
size_t vectorcount=0;
//get positions from line from start and end
size_t startline=m_activeSTC->LineFromPosition(fromPos);
size_t endline=m_activeSTC->LineFromPosition(fromPos+text.size());
//set folding for these lines
for(size_t i=startline; i<=endline; i++) {
int prevlevel=foldlevel; //previous foldlevel
int foldflag=foldlevel; //this flag will be applied to the line
if((foldingvector.size()>0)&&(vectorcount<foldingvector.size())) {
if(i==foldingvector[vectorcount].first) { //if the fold level changes in that line
//new foldlevel = foldlevel in that line
foldlevel=foldingvector[vectorcount].second;
vectorcount++;
if(foldlevel>prevlevel) {
//when incremented, this line keeps the previous fold level (!) but is marked as a folder level header
foldflag= foldflag | wxSTC_FOLDLEVELHEADERFLAG; //incremented, set header flag
}
}
}
foldflag= foldflag | wxSTC_FOLDLEVELBASE; //add 1024 to the fold level
if(m_activeSTC->GetLineLength(i)==0) { //if this does not contain any characters, set the white flag
foldflag= foldflag | wxSTC_FOLDLEVELWHITEFLAG;
}
//finally, set fold level to line
m_activeSTC->SetFoldLevel(i,foldflag);
}
}
Finally, call the app:
class myApp : public wxApp {
public:
virtual bool OnInit() {
myFrame* frame = new myFrame(NULL);
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(myApp);
Now, you can play around with entering a few "G1 G2 G3 this is a text IF this do that ENDIF" and so on to test it.
Complete code for copy& paste:
// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"
#ifdef __BORLANDC__
#pragma hdrstop
#endif
// for all others, include the necessary headers (this file is usually all you
// need because it includes almost all "standard" wxWidgets headers)
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include <wx/stc/stc.h>
#include <vector>
#include <utility>
#include <algorithm>
#define MY_FOLDMARGIN 2
class myFrame : public wxFrame {
//be sure to enable the C++11 standard for this example if you're using GCC
public:
myFrame( wxWindow* parent, int id = wxID_ANY, wxString title = "Demo"
, wxPoint pos = wxDefaultPosition, wxSize size = wxSize(481,466)
, int style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL );
private:
void onMarginClick(wxStyledTextEvent& event);
void OnStyleNeeded(wxStyledTextEvent& event);
void highlightSTCsyntax(size_t fromPos,size_t toPos,wxString &text);
void setfoldlevels(size_t fromPos,int startfoldlevel,wxString &text);
wxStyledTextCtrl* m_activeSTC;
wxColor m_GCodecolor{255,130,0}; //color for highlighted parts
//this is the mask for styling- just remember to set this to 0 with >3.1.0
int m_stylemask=255; //for wxwidgets >3.1.0 set this to 0
};
myFrame::myFrame( wxWindow* parent, int id, wxString title, wxPoint pos
, wxSize size, int style )
:wxFrame( parent, id, title, pos, size, style ) {
m_activeSTC = new wxStyledTextCtrl(this);
//set Lexer to LEX_CONTAINER: This will trigger the styleneeded event so you can do your own highlighting
m_activeSTC->SetLexer(wxSTC_LEX_CONTAINER);
//folding example by New Pagodi copied from WxWidgets Forum
//Set the fold marging to have a width of 14 pixels and give it a
//distinctive background
m_activeSTC->SetMarginWidth(MY_FOLDMARGIN,14);
m_activeSTC->SetMarginMask(MY_FOLDMARGIN,wxSTC_MASK_FOLDERS);
m_activeSTC->SetFoldMarginColour(true,wxColor(255,255,255));
m_activeSTC->SetFoldMarginHiColour(true,wxColor(233,233,233));
//Set up the markers that will be shown in the fold margin
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEREND,wxSTC_MARK_BOXPLUSCONNECTED);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEREND,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEREND,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEROPENMID,wxSTC_MARK_BOXMINUSCONNECTED);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEROPENMID,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEROPENMID,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERMIDTAIL, wxSTC_MARK_TCORNER);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERMIDTAIL,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERMIDTAIL,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERTAIL,wxSTC_MARK_LCORNER);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERTAIL,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERTAIL,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDERSUB,wxSTC_MARK_VLINE);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDERSUB,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDERSUB,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDER,wxSTC_MARK_BOXPLUS);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDER,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDER,wxColor(128,128,128));
m_activeSTC->MarkerDefine(wxSTC_MARKNUM_FOLDEROPEN,wxSTC_MARK_BOXMINUS);
m_activeSTC->MarkerSetForeground(wxSTC_MARKNUM_FOLDEROPEN,wxColor(243,243,243));
m_activeSTC->MarkerSetBackground(wxSTC_MARKNUM_FOLDEROPEN,wxColor(128,128,128));
//Turn the fold markers red when the caret is a line in the group (optional)
m_activeSTC->MarkerEnableHighlight(true);
//The margin will only respond to clicks if it set sensitive. Also, connect
//the event handler that will do the collapsing/restoring
m_activeSTC->SetMarginSensitive(MY_FOLDMARGIN,true);
m_activeSTC->Bind(wxEVT_STC_MARGINCLICK, &myFrame::onMarginClick, this);
m_activeSTC->Bind(wxEVT_STC_STYLENEEDED, &myFrame::OnStyleNeeded, this);
//set color for G-Code highlighting
m_activeSTC->StyleSetForeground(19,m_GCodecolor);
//enter some text and set fold levels
m_activeSTC->AppendText(";Foldtest\n");
m_activeSTC->AppendText("DEF BOOL TESTVAR=FALSE\n");
m_activeSTC->AppendText("IF (TESTVAR)\n");
m_activeSTC->AppendText(" G0 G54 M0\n");
m_activeSTC->AppendText("ENDIF\n");
m_activeSTC->AppendText("M30\n");
/*given the text above, folding should produce this output:
m_activeSTC->SetFoldLevel(0, 1024);
m_activeSTC->SetFoldLevel(1, 1024);
m_activeSTC->SetFoldLevel(2, 1024|wxSTC_FOLDLEVELHEADERFLAG); //header flag: one item before increasing fold level!
m_activeSTC->SetFoldLevel(3, 1025); //here comes the new fold level in line G0 G54 M0
m_activeSTC->SetFoldLevel(4, 1025); //the ENDIF
m_activeSTC->SetFoldLevel(5, 1024); //and this has the lower fold level again
m_activeSTC->SetFoldLevel(6, 1024|wxSTC_FOLDLEVELWHITEFLAG); //this is an empty line: set fold level white flag
*/
/*note: If you load text into the styled text control for example with file load or like we did above:
style the whole document for once. If you don't, the document remains unstyled until you click some position below*/
wxString text=m_activeSTC->GetText().Upper(); //Upper() makes it case insensitive
this->highlightSTCsyntax(0,m_activeSTC->GetTextLength(),text);
this->setfoldlevels(0,0,text);
}
void myFrame::onMarginClick(wxStyledTextEvent& event) {
int margin = event.GetMargin();
int position = event.GetPosition();
int line = m_activeSTC->LineFromPosition(position);
int foldLevel = m_activeSTC->GetFoldLevel(line);
bool headerFlag = (foldLevel & wxSTC_FOLDLEVELHEADERFLAG)!=0;
if( margin==MY_FOLDMARGIN && headerFlag ) {
m_activeSTC->ToggleFold(line);
}
}
void myFrame::OnStyleNeeded(wxStyledTextEvent& event) {
/*this is called every time the styler detects a line that needs style, so we style that range.
This will save a lot of performance since we only style text when needed instead of parsing the whole file every time.*/
size_t line_end=m_activeSTC->LineFromPosition(m_activeSTC->GetCurrentPos());
size_t line_start=m_activeSTC->LineFromPosition(m_activeSTC->GetEndStyled());
/*fold level: May need to include the two lines in front because of the fold level these lines have- the line above
may be affected*/
if(line_start>1) {
line_start-=2;
} else {
line_start=0;
}
//if it is so small that all lines are visible, style the whole document
if(m_activeSTC->GetLineCount()==m_activeSTC->LinesOnScreen()){
line_start=0;
line_end=m_activeSTC->GetLineCount()-1;
}
if(line_end<line_start) {
//that happens when you select parts that are in front of the styled area
size_t temp=line_end;
line_end=line_start;
line_start=temp;
}
//style the line following the style area too (if present) in case fold level decreases in that one
if(line_end<m_activeSTC->GetLineCount()-1){
line_end++;
}
//get exact start positions
size_t startpos=m_activeSTC->PositionFromLine(line_start);
size_t endpos=(m_activeSTC->GetLineEndPosition(line_end));
int startfoldlevel=m_activeSTC->GetFoldLevel(line_start);
startfoldlevel &= wxSTC_FOLDFLAG_LEVELNUMBERS; //mask out the flags and only use the fold level
wxString text=m_activeSTC->GetTextRange(startpos,endpos).Upper();
this->highlightSTCsyntax(startpos,endpos,text);
//calculate and apply foldings
this->setfoldlevels(startpos,startfoldlevel,text);
}
void myFrame::highlightSTCsyntax(size_t fromPos,size_t toPos, wxString &text) {
//this vector will hold the start and end position of each word to highlight
//if you want to highlight more than one, you should pass a whole class or struct containing the offsets
std::vector<std::pair<size_t,size_t>>GcodeVector;
//the following example is a quick and dirty parser for G-Codes.
//it just iterates through the Text Range and finds "Gxx" where xx is a digit.
//you could also use regex, but one can build a pretty fast routine based on single char evaluation
size_t actual_cursorpos = 0;
size_t startpos = 0;
size_t end_of_text = text.length();
bool word_boundary = true; //check for word boundary
char actualchar;
while (actual_cursorpos<end_of_text) {
actualchar= text[actual_cursorpos];
//check if syntax matches "G" followed by a couple of numbers
if((actualchar=='G')&&(word_boundary==true)) {
//this is a new G-Code, store startposition
startpos=actual_cursorpos;
word_boundary=false;
actual_cursorpos++;
if(actual_cursorpos<end_of_text) {
//refresh actual character
actualchar= text[actual_cursorpos];
}
//add digits
while((std::isdigit(actualchar)&&(actual_cursorpos<end_of_text))) {
actual_cursorpos++;
actualchar= text[actual_cursorpos];
}
//check if word boundary occurs at end of digits
if((actualchar==' ')||(actualchar=='\n')||(actualchar=='\r')||(actualchar=='\t')||(actual_cursorpos==end_of_text)) {
//success, append this one
if((actual_cursorpos-startpos)>1) {
//success, append to vector. DO NOT FORGET THE OFFSET HERE! We start from fromPos, so we need to add this
GcodeVector.push_back(std::make_pair(startpos+fromPos, actual_cursorpos+fromPos));
}
word_boundary=true;
}
}
if((actualchar==' ')||(actualchar=='\n')||(actualchar=='\r')||(actualchar=='\t')||(actual_cursorpos==end_of_text)) {
word_boundary=true;
}
actual_cursorpos++;
}
//remove old styling
m_activeSTC->StartStyling(fromPos,m_stylemask); //from here
m_activeSTC->SetStyling(toPos-fromPos,0); //with that length and style -> cleared
//now style the G-Codes
for (int i=0; i<GcodeVector.size(); i++) {
size_t startpos=GcodeVector[i].first;
size_t endpos=GcodeVector[i].second;
size_t length=(endpos-startpos);
m_activeSTC->StartStyling(startpos,m_stylemask);
m_activeSTC->SetStyling(length,19); //must match the style set above
}
}
void myFrame::setfoldlevels(size_t fromPos, int startfoldlevel, wxString& text) {
/*we'll increase the fold level with "IF" and decrease it with "ENDIF".
First, find all "IF" included in the text. Then we check if this IF is actually an ENDIF.
Keep in mind that you still need to check if this is actually commented out and so on.
This is a pretty simple and not perfect example to demonstrate basic folding*/
std::vector<size_t>if_positions;
size_t actual_cursorpos=0;
while ((actual_cursorpos<text.size())&&(actual_cursorpos!=wxNOT_FOUND)) {
actual_cursorpos=text.find("IF",actual_cursorpos+1);
if(actual_cursorpos!=wxNOT_FOUND) {
if_positions.push_back(actual_cursorpos+fromPos);
}
}
//build a vector to include line and folding level
//also, check if this IF is actually an ENDIF
std::vector<std::pair<size_t,int>>foldingvector;
int actualfoldlevel=startfoldlevel;
for(int i=0; i<if_positions.size(); i++) {
size_t this_line=m_activeSTC->LineFromPosition(if_positions[i]);
//check if that "IF" is an ENDIF
wxString endif_string;
if(if_positions[i]>3) {
endif_string=text.substr(if_positions[i]-3-fromPos,5);
}
//if it's an IF the fold level increases, if it's an ENDIF the foldlevel decreases
if(endif_string=="ENDIF") {
actualfoldlevel--;
foldingvector.push_back(std::make_pair(this_line,actualfoldlevel));
} else {
actualfoldlevel++;
foldingvector.push_back(std::make_pair(this_line,actualfoldlevel));
}
}
//now that we know which lines shall influence folding, we can apply to folding level to the STC line for line
int foldlevel=startfoldlevel; //this is a temporary marker containing the foldlevel of that position
size_t vectorcount=0;
//get positions from line from start and end
size_t startline=m_activeSTC->LineFromPosition(fromPos);
size_t endline=m_activeSTC->LineFromPosition(fromPos+text.size());
//set folding for these lines
for(size_t i=startline; i<=endline; i++) {
int prevlevel=foldlevel; //previous foldlevel
int foldflag=foldlevel; //this flag will be applied to the line
if((foldingvector.size()>0)&&(vectorcount<foldingvector.size())) {
if(i==foldingvector[vectorcount].first) { //if the fold level changes in that line
//new foldlevel = foldlevel in that line
foldlevel=foldingvector[vectorcount].second;
vectorcount++;
if(foldlevel>prevlevel) {
//when incremented, this line keeps the previous fold level (!) but is marked as a folder level header
foldflag= foldflag | wxSTC_FOLDLEVELHEADERFLAG; //incremented, set header flag
}
}
}
foldflag= foldflag | wxSTC_FOLDLEVELBASE; //add 1024 to the fold level
if(m_activeSTC->GetLineLength(i)==0) { //if this does not contain any characters, set the white flag
foldflag= foldflag | wxSTC_FOLDLEVELWHITEFLAG;
}
//finally, set fold level to line
m_activeSTC->SetFoldLevel(i,foldflag);
}
}
class myApp : public wxApp {
public:
virtual bool OnInit() {
myFrame* frame = new myFrame(NULL);
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(myApp);
Compilation notes
This example has been tested with Code::Blocks 16.01, TDM-GCC-32 5.1.0 and wxWidgets 3.0.3 stable under Windows 7.
Libraries needed:
- lib_xx_core.a
- lib_xx_stc.a
- lib_xx_adv.a
- libwxbase_xx.a
This example uses C++11 so be sure to enable it when compiling ([-std=c++11]).