Simple QML interpreter

This is a simple QML interpreter that hope can be useful for others. It allows use QT creator for build LVGL UIs

import imp, sys
sys.path.append('https://raw.githubusercontent.com/littlevgl/lv_binding_micropython/master/lib')
import display_driver
import lvgl as lv

class Lexer:
    def __init__( self, string ):
        self.str = string
        self.ptr = 0
    
    def skip_spaces( self ):
        while( self.is_string( " " ) or self.is_string( "\n" ) ):
            pass
    
    def is_name( self ):
        self.skip_spaces()
        ptr = self.ptr
        if( self.is_alpha() ):
            while( self.is_alpha() or self.is_digit() or self.is_string( "." ) or self.is_string( "_" ) ):
                pass
            return True, self.str[ ptr: self.ptr ]
        return False, ""
    
    def is_number( self ):
        self.skip_spaces()
        ptr = self.ptr
        if( self.is_digit() ):
            while( self.is_digit() or self.is_symbol( "." ) ):
                pass
            return True, self.str[ ptr: self.ptr ]
        return False, ""
    
    def is_qstring( self ):
        self.skip_spaces()
        ptr = self.ptr
        if( self.is_symbol( "\"" ) ):
            while( not self.is_symbol( "\"" ) ):
                self.ptr += 1
            return True, self.str[ ptr: self.ptr ]
        return False, ""
    
    def is_symbol( self, symbol ):
        self.skip_spaces()
        ptr = self.ptr
        if( self.str[self.ptr] == symbol ):
            self.ptr += 1
            return True
        return False
    
    def is_digit( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( "0" <= c and c <= "9" ):
            self.ptr += 1
            return True
        return False
    
    def is_alpha( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( "a" <= c and c <= "z" or "A" <= c and c <= "Z" ):
            self.ptr += 1
            return True
        return False

    def is_graph( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( c == "\"" ): # to simplify quoted string detection, "\"" is not treated as a graph symbol
            return False
        if( "!" <= c and c <= "~" ):
            self.ptr += 1
            return True
        return False
    
    def is_string( self, string ):
        l = len( string )
        if( self.ptr + l - 1 >= len(self.str) ):
            return False
        if( l == 0 ):
            return False
        if( string == self.str[self.ptr:self.ptr+l] ):
            self.ptr += l
            return True
        return False

class Parser:
    def __init__( self, lexer ):
        self.lexer = lexer
    
    def parse( self ):
        return self.parse_object()
    
    def parse_object( self ):
        name = ""
        attribs = []
        ptr = self.lexer.ptr
        res, name = self.lexer.is_name()
        if( res ):
            if( self.lexer.is_symbol( "{" ) ):
                res, attrib = self.parse_attribute()
                if( res ):
                    attribs += [attrib]
                    while( True ):#self.lexer.is_symbol( "\n" ) ):
                        res, attrib = self.parse_attribute()
                        if( res ):
                            attribs += [attrib]
                        else:
                            break
                if( self.lexer.is_symbol( "}" ) ):
                    return True, (name, attribs)

        self.lexer.ptr = ptr
        return False, (None, None)
    
    def parse_attribute( self ):
        name = ""
        attribs = []
        ptr = self.lexer.ptr
        res, (name, value) = self.parse_property()
        if( res ):
            return True, (name, value)
        res, (name, attribs) = self.parse_object()
        if( res ):
            return True, (name, attribs)
        self.lexer.ptr = ptr
        return False, (None, None)
    
    def parse_property( self ):
        name = ""
        value = ""
        ptr = self.lexer.ptr
        res, name = self.lexer.is_name()
        if( res ):
            if( self.lexer.is_symbol( ":" ) ):
                res, value = self.lexer.is_name()
                if( res ):
                    return True, (name, value)
                res, value = self.lexer.is_number()
                if( res ):
                    value = value.replace("\n", "")
                    return True, (name, value)
                res, value = self.lexer.is_qstring()
                if( res ):
                    return True, (name, value)
        self.lexer.ptr = ptr
        return False, (None, None)
    
    def dump( self, obj, indent=0 ):
        name, attribs = obj
        print( "    "*indent, name, "{" )
        for attrib in attribs:
            if( attrib[0][0].isupper() ):
                self.dump( attrib, indent+1 )
            else:
                print( "    "*(indent+1), attrib[0], ":", attrib[1] )
        print( "    "*indent, "}" )

    def is_digit( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( "0" <= c and c <= "9" ):
            self.ptr += 1
            return True
        return False
    
    def is_alpha( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( "a" <= c and c <= "z" or "A" <= c and c <= "Z" ):
            self.ptr += 1
            return True
        return False

    def is_graph( self ):
        if( self.ptr >= len(self.str) ):
            return False
        c = self.str[self.ptr]
        if( c == "\"" ): # to simplify quoted string detection, "\"" is not treated as a graph symbol
            return False
        if( "!" <= c and c <= "~" ):
            self.ptr += 1
            return True
        return False
    
    def is_string( self, string ):
        l = len( string )
        if( self.ptr + l - 1 >= len(self.str) ):
            return False
        if( l == 0 ):
            return False
        if( string == self.str[self.ptr:self.ptr+l] ):
            self.ptr += l
            return True
        return False


class Qml:
    def __init__( self ):
        pass
    
    def build( self, obj, parent ):
        return self.build_obj( obj, parent )
    
    def set_attrib( self, lv_obj, attrib ):
        print( "set_attrib", attrib[0], attrib[1] )
        if( attrib[0] == "id" ):
            pass
        elif( attrib[0] == "x" ):
            lv_obj.set_x( int( attrib[1] ) )
        elif( attrib[0] == "y" ):
            lv_obj.set_y( int( attrib[1] ) )
        elif( attrib[0] == "width" ):
            lv_obj.set_width( int( attrib[1] ) )
        elif( attrib[0] == "height" ):
            lv_obj.set_height( int( attrib[1] ) )
        elif( attrib[0] == "text" ):
            try:
                #print("set_text", attrib[1][1:-1] )
                lv_obj.set_text( attrib[1][1:-1] )
            except Exception as e:
                print( "except set_text", e )
                try:
                    #print("set_style_local_value_str", attrib[1][1:-1] )
                    lv_obj.set_style_local_value_str( lv_obj.PART.MAIN, lv.STATE.DEFAULT, attrib[1][1:-1] )
                except Exception as e:
                    print( "except set_style_local_value_str", e )
        elif( attrib[0] == "value" ):
            lv_obj.set_value( int( float( attrib[1] ) ), lv.ANIM.OFF ) 
        elif( attrib[0] == "color" ):
            pass
        else:
            print("Warning. set_attrib. unsupported attrib name", attrib[0] )
    
    def set_attibs( self, attribs, lv_obj ):
        for attrib in attribs:
            if( not attrib[0][0].isupper() ):
                self.set_attrib( lv_obj, attrib )
    
    def get_id( self, attribs ):
        for attrib in attribs:
            if( attrib[0] == "id" ):
                return attrib[1]
        print( "Warning. id not found" )
        return None
    
    def build_childs( self, attribs, lv_obj ):
        lv_childs = []
        for attrib in attribs:
            if( attrib[0][0].isupper() ):
                lv_childs += [ self.build_obj( attrib, lv_obj ) ]
        return lv_childs
    
    def build_obj( self, obj, parent ):
        name, attribs = obj
        print( "build_obj", name )
        if( name == "Rectangle" ):
            lv_obj = lv.obj( parent )
        elif( name == "Label" ):
            lv_obj = lv.label( parent )
        elif( name == "Button" ):
            lv_obj = lv.btn( parent )
            lv_obj.set_style_local_value_str( lv_obj.PART.MAIN, lv.STATE.DEFAULT, "Button" )
        elif( name == "CheckBox" ):
            lv_obj = lv.checkbox( parent )
        elif( name == "Switch" ):
            lv_obj = lv.switch( parent )
        elif( name == "Slider" ):
            lv_obj = lv.slider( parent )
        elif( name == "Bar" ):
            lv_obj = lv.bar( parent )
        elif( name == "DropDown" ):
            lv_obj = lv.dropdown( parent )
        elif( name == "Column" ):
            lv_obj = lv.cont( parent )
            lv_obj.set_layout( lv.LAYOUT.COLUMN_LEFT )
            #lv_obj.set_fit( lv.FIT.TIGHT )
        elif( name == "Row" ):
            lv_obj = lv.cont( parent )
            lv_obj.set_layout( lv.LAYOUT.ROW_TOP )
            #lv_obj.set_fit( lv.FIT.TIGHT )
        elif( name == "Grid" ):
            lv_obj = lv.cont( parent )
            lv_obj.set_layout( lv.LAYOUT.GRID )
            #lv_obj.set_fit( lv.FIT.TIGHT )
        else:
            print( "Warning. build_obj. unsupported obj name", name )
            lv_obj = None
        self.set_attibs( attribs, lv_obj )
        lv_childs = self.build_childs( attribs, lv_obj )
        ide = self.get_id( attribs )
        return ide, lv_obj, lv_childs
    
    def find( self, name, tree ):
        ide, lv_obj, lv_childs = tree
        if( name == ide ):
            print( "found", name)
            return lv_obj
        else:
            for lv_child in lv_childs:
                child = self.find( name, lv_child )
                if( child ):
                    return child
        return None


string = """
Rectangle{
    width:420
    height:320
    
    Button{
        id: button_1
        x: 8
        y: 8
        text: "Button 1"
    }
    
    Button {
        id: button_2
        x: 8
        y: 54
        text: "Button 2"
    }
}
"""
lexer = Lexer( string )
parser = Parser( lexer )
qml = Qml()

res, obj = parser.parse()
print("res", res )
if( res ):
    parser.dump( obj )
    tree = qml.build( obj, lv.scr_act() )
    btn1 = qml.find( "button_1", tree )
    btn2 = qml.find( "button_2", tree )
    btn1.set_event_cb( lambda obj, event: print( "button 1 clicked" ) if event == lv.EVENT.CLICKED else None )
    btn2.set_event_cb( lambda obj, event: print( "button 2 clicked" ) if event == lv.EVENT.CLICKED else None )
1 Like

Thank you for sharing this @jgpeiro!

Regarding a GUI tool for building LVGL UI -

I’m not sure how well LVGL widgets/styles/events map into QML, but it certainly could be useful to leverage QT Creator for building LVGL GUI!

However, theirs is legally vendor-locked to NXP platforms only, which is understandable, but limits its use for the wider community.

@jgpeiro
Thanks for sharing. If you wish you can post about it in the “My projects” category too.

Doesn’t NXP use the open source eclipse as the underlying framework for their IDE? If so, at minimum they could show some appreciation to the community and open their LVGL designer.

My guess is as good as yours, but I suspect it’s a marketing strategy to get LVGL users to choose NXP hardware.

Yes, you are probably right, but personally I am less inclined to support companies that build their business model on open source products such as eclipse and gcc and do not contribute back.

Open sourcing that LVGL editor or at least not locking it legally could be a good opportunity for NXP to show their gratitude to the community.

2 Likes

@amirgon This tool only maps basic widgets and properties. I published it because it can be easily expanded but with the close arrival of an official builder it will be useless.
While I’m waiting for the EdgeLine release, I’m thinking of making a tool similar to this one, but to generate the widgets, so you can use SVG editor for draw custom gauges/meters… and use like:

class MySvgWidget(lv.obj):
    ...
    def design( self, line, clip_area, mode ):
        ...
        if( mode == lv.DESIGN.DRAW_MAIN ):
            render_svg( self.svg, self.state )

@kisvegabor You are right. Better next time I will post in that subsection.
By the way, when will EdgeLine be released? I know it’s early, but I’m dying to try it :wink:

zapta* I tried GUI Guider as soon as it came out. I never like these “manufacturer-linked tools” … also that I always prefer to use STM32 / ESP32 microcontrollers

*new users can only mention 2 users in a post.

1 Like

We are targeting March 1.

1 Like