-
Notifications
You must be signed in to change notification settings - Fork 5
Building the Droid DSL editor with Xtext
This tutorial covers the instalation of Xtext and the creation of a DSL to model Android Applications.
- Eclipse IDE for Java Developeers, version 3.6.2 (Helios SR2 release).
Available at: http://www.eclipse.org/downloads/packages/eclipse-ide-java-developers/heliossr2 - Xtext (version 1.0.2)
Install "Xtext SDK" plugin from the built-in "Helios" update site (http://download.eclipse.org/releases/helios) under the "Modeling" category.
Our main objective is the creation of a textual editor that assists the creation of Android Applications by using an specific textual language. So, since we are going to talk about Android Applications, that is going to be our language domain (the subject of our language).
Once defined our language domain, we must to decide on choosing a (textual) syntax to our language that must represent every component of our domain.
Finally, we must create a parser that is capable of processing a file written acording to this textual syntax (a source file) and build an appropriate instance of our domain model.
That particular kind of language is called Domain Specific Language: a language specific to a particular domain.
Xtext provides the tooling needed to create and use textual DSLs in a MDD environment. The language's domain is captured and described in a metamodel (the language's abstract syntax) and the language's textual syntax (it's concrete syntax) is described in a grammar file (.xtext file). Xtext can even infer your language metamodel (an Ecore model) from the language grammar.
Xtext will also create a parser and an editor to your language (with all the Eclipse editor features included).
The Android Operating system has its own concepts regarding application development. An Application consists of: Activities, Resources and Layouts.
From Android Dev Guide
An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.
From Android Dev Guide
In addition to code, an application consists of resources that are separate from source code, such as images, audio files, and anything relating to the visual presentation of the application.
Layouts are collection of Views that define a hierarchy of widgets (UI objects).
To create a DSL, the first step is to decide the level of abstraction that it will deal with. The finer the granularity level, the more detailed the DSL is going to be (and more work to be built). So we chose a coarser granularity in order to make things easier.
We created an abstraction of the original Android concepts with the following concepts:
- Application: The root concept of our abstraction
- Screen: An application have several screens that display UI to users.
- Widgets, Resources and Layouts: The same concepts but with less properties than the original concepts.
commit 16cdb466000acab21c7bf31c91b7e0f49c0d2ca8
Creating the Xtext project
To create a Xtext project, open the "Xtext Project Wizard":
- Open menu "File > New > Project..."
- Choose "Xtext > Xtext Project"
And fill the project properties:
- Change the "Project name" to "org.xtext.example.droid"
- Under the "Language" section
- Change the "Name" to "org.xtext.example.droid.Droid"
- Change the "Extension" to "droid"
- Click "Finish"
- Grammar File (Droid.xtext): A grammar has two purposes: First, it is used to describe the concrete syntax of your language. Second, it contains information about how the parser shall create a model during parsing.
- Language generator Worflow (GenerateDroid.mwe2): This workflow is used to generate the various language components (metamodel, parser, editor UI, etc) from an existing grammar file.
Xtext has its own grammar definition language so the rest of the tutorial is about the basics of this language. You can find more info on this language in the Xtext User Manual.
The grammar file (Droid.xtext) specifies the textual concrete syntax of your DSL by defining rules to the parser.
commit 4a33a8373b42ce92c9424e8891fc600620060090
Generating Language Artifacts
In order to use the DSL you need to generate the language components.
- Right click the GenerateDroid.mwe2 workflow file.
- From its context menu, choose "Run As > MWE2 Workflow"
Installing ANTLR 3 parser generator
At the first time, the generator will warn yout about the instalation of the ANTLR 3 parser generator. Go to the console, type 'y' and press "Enter". This allows the generator to download the parser generator based on ANTLR 3.
The workflow will generate all the files needed to create an Eclipse plugin with a textual editor to your DSL. Every time you make any change to the grammar file (Droid.xtext) you should regenerate the language artifacts.
If the code generation succeeded, we are now able to test the IDE integration.
- Right-click on the Xtext project (org.xtext.example.droid)
- Choose "Run As > Eclipse Application".
This will starts a new Eclipse workbench with your DSL plug-ins installed.
In the new workbench create a new project and therein a new file with the .droid file extension. This will open the generated Droid editor.
Try it a bit and discover the default functionality for code completion (Ctrl+Space even for cross-references), syntax highlighting, syntactic validation, linking errors, outline, find references etc.
You may notice that the editor ignores 'white-space characters' between tokens (new line, space, tab, etc) and has built-in support to both single and multi-line comments (//
and /* */
).
This will be our test project.
commit e75c45d1ae4457d8cdae5c705e5a4d7978de3c11
The first concept in our DSL is the "Application" concept. Add these lines to the grammar file:
Application:
'application' name=STRING
;
This is a parser rule. A parser rule describes the concrete syntax of a specific class of your metamodel, that is, the pattern used by the parser in order to identificate an instance of a specific class in a source file.
In our example, the parser creates an Application
object when it evaluates a source file and finds the word 'application' followed by a STRING
(like "first app").
Simply put, an Application object is represented textually by the keyword 'application' followed by a STRING.
The name=STRING
is an asignment. Assignments are used to assign the parsed information to a property (a feature) of the current object. It means that the string found will be the value of the name
property of the created object.
The keyword STRING
is a terminal. It is made available to your grammar by the inclusion of another grammar inside your grammar (through the with org.eclipse.xtext.common.Terminals
statement). The grammar will evaluate the tokens defined in the STRING
terminal rule and convert them to a string value.
Moreover, this first rule in the grammar has a special meaning: that is the entry rule of your grammar. Therefore, to create a valid source file (an instance of your model) you must start creating an object of the entry rule's type, i.e., an Application.
In order to test this change you need to regenerate the language artifacts and run the generated plugin. Do it now.
commit 052f05e9eb5cdc0ab3a9800e5c678658017d7def
An Android application also has a package name, where the generated Java files are going to be placed. Let's add this property:
Application:
'application' name=STRING
'=>' packageName=ID
;
This time I created a packageName
property that will receive the value of an ID
terminal found. An ID is a identifier, like xtext_is_cool
. Let's test the editor.
Open the sample project and create a .droid
file:
application "My First App" => myfirstapp
Save the file and it should work. Now try the following example:
application "My First App" => org.xtext.example.droid.myfirstapp
And you'll see an error: mismatched input '.' expecting EOF
. That's because the parser have found an ID
(org) and was expecting to reach the end of the file. If we want to use a qualified name (like org.eclipse.test) as the value of the package
property we need to create a rule to recognize several tokens as a single value.
commit 4e8dce9f17bc419f901e4438b4b6ef98a7e209d8
What is a qualified name? It can be just an ID
but can also be an ID
followed by a .
and another ID
. The pattern '.' followed by an ID
can be repeated several times. This is enough to creating our datatype rule describing the PackageName
datatype:
PackageName:
ID ('.' ID)*
;
A datatype rule describes the concrete syntax of a datatype, which can be used as the value of a property.
Now we change the Application
rule to use this datatype in the packageName
property:
Application:
'application' name=STRING
'=>' packageName=PackageName
;
And the package is working as expected.
commit afd8e816d7b14ce0d0ffe6ac91894283cc3c4b78
We can create the Screen concept in a similar way:
Screen:
'screen' name=ID
'{'
'}'
;
But now we need to associate an Application to one or more Screens.
commit 72a3d4af18dbf28f8bceae7e53d023a7dedad2fe
We can associate an Application to one or more Screens by creating an assignment rule, like we did to assign a STRING
terminal to an Application
when creating the name
property:
Application:
'application' name=STRING
'=>' packageName=PackageName
(screens+=Screen)+
;
This assignment is pretty similar to the previous ones, except by the +=
and +
operators.
From Xtext manual:
There are three different assignment operators, each with different semantics.
- The simple equal sign ' =' is the straight forward assignment, and used for features which take only one element.
- The ' +=' sign (the add operator) expects a multi-valued feature and adds the value on the right hand to that feature, which is a list feature.
- The ' ?=' sign (boolean assignment operator) expects a feature of type EBoolean and sets it to true if the right hand side was consumed independently from the concrete value of the right hand side.
Again, from Xtext manual:
There are four different possible cardinalities:
- exactly one (the default, no operator)
- one or none (operator ?)
- any (zero or more, operator *)
- one or more (operator +)
That way, we are saying that after the packageName
property, an Application must have ONE OR MORE assignments of Screen
objects to the multi-valued screens
property.
Run the editor and the following source file should be valid:
application "My First App" => org.xtext.example.droid.myfirstapp
screen Welcome { }
screen Main { }
Now we are going to introduce some more concepts to our DSL: Layout, View, Widget.
commit c5ee45f8b8870dacabef8d849d5b33c7484ad821
Add these concepts to the grammar:
Layout:
'layout' name=ID
'{'
views=ViewCollection
'}'
;
ViewCollection:
('#' views+=View)+
;
View:
'view'
;
And change the Application to include layouts
in addition to the existing screens:
Application:
'application' name=STRING
'=>' packageName=PackageName
(screens+=Screen | layouts+=Layout)+
;
It states that where we previously should have ONE OR MORE Screen
assignments, now we must have ONE OR MORE Screen assignments OR Layout assigments (the |
operator).
Note that both assignments are being made to multi-valued features.
Note also that the assignments can occur in any order (try it).
After the modification, this sould be a valid source file:
application "My First App" => org.xtext.example.droid.myfirstapp
screen Welcome { }
layout Default {
# view
# view
# view
}
screen App { }
Another interesting feature is using the |
operator to create unassigned rule calls.
This allows us to work with parser rules like abstract classes.
commit be6e4c3f6cf85c75f69b5558fe4c78208da420d4
From Xtext manual:
Unassigned rule calls (the name suggests it) are rule calls to other parser rules, which are not used within an assignment. If there is no feature the returned value shall be assigned to, the value is assigned to the “to-be-returned” result of the calling rule.
For example, an layout can have Widgets
or nested Layout
s composing its CollectionView
, so a View
rule must delegate to a Widget
or Layout
rule.
View:
Widget | Layout
;
Now we are going to use the same pattern to delegate the abstract Widget
concept to the concrete widgets.
Widget:
TextView | Button | ImageView | EditText | Spinner
;
TextView:
'textView' (name=ID)?
text=STRING
'{'
'}'
;
Button:
'button' (name=ID)?
( 'text' '=' text=STRING | 'image' '=' src=STRING )
'{'
'}'
;
ImageView:
'imageView' (name=ID)?
src=STRING
'{'
'}'
;
Spinner:
'spinner' (name=ID)?
prompt=STRING
'{'
'}'
;
EditText:
'editText' (name=ID)?
text=STRING?
'{'
'}'
;
application "My First App" => org.xtext.example.droid.myfirstapp
screen Welcome { }
layout Default {
# textView "Welcome to First App" { }
# layout Sublayout {
# textView "Put your name" { }
# editText { }
}
# button text="Continue" { }
}
screen App { }
commit 21caad3d1dce0446727f7fe15624f9529a76b0fd
Xtext provides a simple way to have cross-references in your language. For example, we want to set the Screen
layout property pointing to a Layout
declared elsewhere in the code.
Screen:
'screen' name=ID
'{'
('show' layout=[Layout])?
'}'
;
application "My First App" => org.xtext.example.droid.myfirstapp
screen Welcome {
show Default
}
layout Default {
# textView "Welcome to First App" { }
# layout Sublayout {
# textView "Put your name" { }
# editText { }
}
# button text="Continue" { }
}
screen App { }
commit c3e39d244f0a06a6e63d947d5b1e5a937899a530
Using the tools learned thus far, we'll introduce the Resource
concept:
Application:
'application' name=STRING
'=>' packageName=PackageName
(screens+=Screen | layouts+=Layout | resources+=Resource)+
;
Resource:
StringResource | IntegerResource | BooleanResource
| ColorResource | DimensionResource
| ArrayResource | DrawableResource
;
StringResource:
name=ID '=' value=STRING
;
IntegerResource:
name=ID '=' value=INT
;
BooleanResource:
name=ID '=' value=BOOL
;
ColorResource:
name=ID '=' value=HEX_COLOR
;
DimensionResource:
name=ID '=' value=DimensionValue
;
ArrayResource:
IntegerArrayResource | StringArrayResource
;
IntegerArrayResource:
name=ID '=' '['
(items+=INT (',' items+=INT)* )
']'
;
StringArrayResource:
name=ID '=' '['
(items+=STRING (',' items+=STRING)* )
']'
;
DrawableResource:
(BitmapDrawableResource | TransitionDrawableResource)
;
BitmapDrawableResource:
name=ID '=' filename=ID
;
TransitionDrawableResource:
name=ID
from=[BitmapDrawableResource] '<->' to=[BitmapDrawableResource]
;
DimensionValue:
FLOAT ('dp' | 'sp' | 'pt' | 'px' | 'mm' | 'in')
;
BOOL:
'YES' | 'NO'
;
FLOAT:
INT ('.' INT)?
;
terminal HEX_COLOR:
'#'
('0'..'9'|'A'..'F'|'a'..'f') ('0'..'9'|'A'..'F'|'a'..'f')
('0'..'9'|'A'..'F'|'a'..'f') ('0'..'9'|'A'..'F'|'a'..'f')
('0'..'9'|'A'..'F'|'a'..'f') ('0'..'9'|'A'..'F'|'a'..'f')
(('0'..'9'|'A'..'F'|'a'..'f') ('0'..'9'|'A'..'F'|'a'..'f'))?
;
application "My First App" => org.xtext.example.droid.myfirstapp
title = "Welcome to First App"
name_label = "Put your name here"
button_label = "Continue"
default_number = 125
should_run = YES
max_width = 25 px
points_per_game = [
12, 18, 9, 25, 14
]
screen Welcome {
show Default
}
layout Default {
# textView "Welcome to First App" { }
# layout Sublayout {
# textView "Put your name" { }
# editText { }
}
# button text="Continue" { }
}
screen App { }
Note: the BOOL
, FLOAT
and HEX_COLOR
rules will all generate string values.
To generate, for example, boolean and float values you need to create a value converter to each rule (covered in the appendix).
commit 09bf777e8acea7360594e6a29a1cb062fbba930b
Now we'll create some rules that are not going to represent any concept from the domain. They will only help to ease the language specification.
The Android environment allows you to specify the Activity and View properties using both literal values and references to resources. So, we are going to create a concept to represent this value-access concept.
ValueAccess:
(StringVA | IntegerVA | BooleanVA | AnyDrawableVA | DimensionVA)
| ResourceAccess
;
AnyDrawableVA:
DrawableVA | ColorVA
;
StringVA:
StringRA | value=STRING
;
IntegerVA:
IntegerRA | value=INT
;
BooleanVA:
BooleanRA | value=BOOL
;
ColorVA:
ColorRA | value=HEX_COLOR
;
DimensionVA:
DimensionRA | value=DimensionValue
;
DrawableVA:
DrawableRA
;
ResourceAccess:
StringRA | IntegerRA | BooleanRA | ColorRA | DimensionRA | DrawableRA
;
StringRA:
'string'
(
( '(' (package=PackageName '/')? resource=[StringResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
IntegerRA:
'integer'
(
( '(' (package=PackageName '/')? resource=[IntegerResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
BooleanRA:
'bool'
(
( '(' (package=PackageName '/')? resource=[BooleanResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
ColorRA:
'color'
(
( '(' (package=PackageName '/')? resource=[ColorResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
DimensionRA:
'dimen'
(
( '(' (package=PackageName '/')? resource=[DimensionResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
ArrayRA:
'array'
(
( '(' (package=PackageName '/')? resource=[ArrayResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
DrawableRA:
'drawable'
(
( '(' (package=PackageName '/')? resource=[DrawableResource] ')' )
| ( '[' (package=PackageName '/')? externalResource=ID ']' )
)
;
And now, we change the Widget
s properties to use the ValueAccess
to specify its values:
comit 36acef1c9cbd42797182a2a2c22f323ed1886f06
TextView:
'textView' (name=ID)?
text=StringVA
'{'
'}'
;
Button:
'button' (name=ID)?
( text=StringVA | src=AnyDrawableVA )
'{'
'}'
;
ImageView:
'imageView' (name=ID)?
src=DrawableVA
'{'
'}'
;
Spinner:
'spinner' (name=ID)?
prompt=StringVA
'{'
'}'
;
EditText:
'editText' (name=ID)?
text=StringVA?
'{'
'}'
;
application "My First App" => org.xtext.example.droid.myfirstapp
title = "Welcome to First App"
name_label = "Put your name here"
button_label = "Continue"
screen Welcome {
show Default
}
layout Default {
# textView string(name_label) { }
# textView "Description" { }
# layout Sublayout {
# textView string(name_label) { }
# editText string[external_text] { }
}
# button string(button_label) { }
}
commit 2075220de651548dcf4050f6e531af88dcea65f5
Now that we have created the ValueAccess
we can start adding some properties to the View
(Layout and Widget).
Let's start with the layout-related properties, and group them in a LayoutProperties
concept.
First, create the concept:
LayoutProperties:
'layout:'
'{'
//LinearLayoutParams
('height:' layout_height=DimensionVA ';')?
('width:' layout_width=DimensionVA ';')?
('weight:' layout_weight=IntegerVA ';')?
('marginBottom:' layout_marginBottom=DimensionVA ';')?
('marginLeft:' layout_marginLeft=DimensionVA ';')?
('marginRight:' layout_marginRight=DimensionVA ';')?
('marginTop:' layout_marginTop=DimensionVA ';')?
//RelativeLayoutParams
('above:' layout_above=[View] ';')?
('alignBaseline:' layout_alignBaseline=[View] ';')?
('alignBottom:' layout_alignBottom=[View] ';')?
('alignLeft:' layout_alignLeft=[View] ';')?
('alignParentBottom:' layout_alignParentBottom=BooleanVA ';')?
('alignParentLeft:' layout_alignParentLeft=BooleanVA ';')?
('alignParentRight:' layout_alignParentRight=BooleanVA ';')?
('alignParentTop:' layout_alignParentTop=BooleanVA ';')?
('alignTop:' layout_alignTop=[View] ';')?
('alignWithParentIfMissing:' layout_alignWithParentIfMissing=BooleanVA ';')?
('below:' layout_below=[View] ';')?
('centerHorizontal:' layout_centerHorizontal=BooleanVA ';')?
('centerInParent:' layout_centerInParent=BooleanVA ';')?
('centerVertical:' layout_centerVertical=BooleanVA ';')?
('toLeftOf:' layout_toLeftOf=[View] ';')?
('toRightOf:' layout_toRightOf=[View] ';')?
'}'
;
And then, add it to the Layout
and Widgets
:
Layout:
'layout' name=ID
'{'
(layoutProperties=LayoutProperties)?
views=ViewCollection
'}'
;
TextView:
'textView' (name=ID)?
text=StringVA
'{'
(layoutProperties=LayoutProperties)?
'}'
;
Button:
'button' (name=ID)?
( text=StringVA | src=AnyDrawableVA )
'{'
(layoutProperties=LayoutProperties)?
'}'
;
ImageView:
'imageView' (name=ID)?
src=AnyDrawableVA
'{'
(layoutProperties=LayoutProperties)?
'}'
;
Spinner:
'spinner' (name=ID)?
prompt=StringVA
'{'
(layoutProperties=LayoutProperties)?
'}'
;
EditText:
'editText' (name=ID)?
text=StringVA?
'{'
(layoutProperties=LayoutProperties)?
'}'
;
application "My First App" => org.xtext.example.droid.myfirstapp
title = "Welcome to First App"
name_label = "Put your name here"
button_label = "Continue"
left_margin = 10px
screen Welcome {
show Default
}
layout Default {
layout: {
marginLeft: dimen(left_margin);
marginRight: 5px;
centerHorizontal: YES;
}
# textView string(name_label) { }
# textView "Description" { }
# layout Sublayout {
# textView string(name_label) { }
# editText string[external_text] { }
}
# button string(button_label) { }
}
But there are some problems with this implementation.
It restricts the way we can specify the layout properties of a View
: we can only define the properties following the order they were defined in the grammar file.
That is a big problem, because the DSL users wil not even know about the grammar.
For instance, this example is not valid and lead to parse errors:
application "My First App" => org.xtext.example.droid.myfirstapp
layout Default {
layout: {
marginRight: 5px;
marginLeft: 10px;
centerHorizontal: YES;
}
# button "Go" { }
}
It happens because, in the example, the rule to the marginLeft
feature is declared before the rule to the marginRight
feature.
So, once the parser reaches the rule to the marginRight
it already left the marginLeft
rule behind and it expects finding another rule that is after marginRight
.
Despite the features of LayoutProperties
are all optional, they are an ordered sequence of optional features.
commit 09eec79e39d063e5b845e4478c37609279db644b
One solution could be making this sequence a loop of any LayoutProperty
features.
LayoutProperties:
'layout:'
'{'
(
//LinearLayoutParams
('height:' layout_height=DimensionVA ';') |
('width:' layout_width=DimensionVA ';') |
('weight:' layout_weight=IntegerVA ';') |
('marginBottom:' layout_marginBottom=DimensionVA ';') |
('marginLeft:' layout_marginLeft=DimensionVA ';') |
('marginRight:' layout_marginRight=DimensionVA ';') |
('marginTop:' layout_marginTop=DimensionVA ';') |
//RelativeLayoutParams
('above:' layout_above=[View] ';') |
('alignBaseline:' layout_alignBaseline=[View] ';') |
('alignBottom:' layout_alignBottom=[View] ';') |
('alignLeft:' layout_alignLeft=[View] ';') |
('alignParentBottom:' layout_alignParentBottom=BooleanVA ';') |
('alignParentLeft:' layout_alignParentLeft=BooleanVA ';') |
('alignParentRight:' layout_alignParentRight=BooleanVA ';') |
('alignParentTop:' layout_alignParentTop=BooleanVA ';') |
('alignTop:' layout_alignTop=[View] ';') |
('alignWithParentIfMissing:' layout_alignWithParentIfMissing=BooleanVA ';') |
('below:' layout_below=[View] ';') |
('centerHorizontal:' layout_centerHorizontal=BooleanVA ';') |
('centerInParent:' layout_centerInParent=BooleanVA ';') |
('centerVertical:' layout_centerVertical=BooleanVA ';') |
('toLeftOf:' layout_toLeftOf=[View] ';') |
('toRightOf:' layout_toRightOf=[View] ';')
)+
'}'
;
Now the previous example is going to work.
But it leads to another problem: you are allowing duplicated feature declarations.
Xtext is going to warn you about it, for example with this warn message: The assigned value of feature 'layout_height' will possibly override itself because it is used inside of a loop.
For instance, the following example is going to be valid.
Note that the first centerHorizontal
is an useless declaration and, for this reason, to repeat this declaration should be an invalid situation.
application "My First App" => org.xtext.example.droid.myfirstapp
layout Default {
layout: {
centerHorizontal: YES;
marginRight: 5px;
marginLeft: 7px;
centerHorizontal: NO;
}
# button "Go" { }
}
So how can we solve the first problem without introducing the second one?
commit fd0037b794e25caca4260117d9c89fd5e66d49a1
What we need is to define an Unordered Group. From Xtext Manual:
The elements of an unordered group can occur in any order but each element must appear once. Unordered groups are separated by &.
LayoutProperties:
'layout:'
'{'
(
//LinearLayoutParams
('height:' layout_height=DimensionVA ';')?
& ('width:' layout_width=DimensionVA ';')?
& ('weight:' layout_weight=IntegerVA ';')?
& ('marginBottom:' layout_marginBottom=DimensionVA ';')?
& ('marginLeft:' layout_marginLeft=DimensionVA ';')?
& ('marginRight:' layout_marginRight=DimensionVA ';')?
& ('marginTop:' layout_marginTop=DimensionVA ';')?
//RelativeLayoutParams
& ('above:' layout_above=[View] ';')?
& ('alignBaseline:' layout_alignBaseline=[View] ';')?
& ('alignBottom:' layout_alignBottom=[View] ';')?
& ('alignLeft:' layout_alignLeft=[View] ';')?
& ('alignParentBottom:' layout_alignParentBottom=BooleanVA ';')?
& ('alignParentLeft:' layout_alignParentLeft=BooleanVA ';')?
& ('alignParentRight:' layout_alignParentRight=BooleanVA ';')?
& ('alignParentTop:' layout_alignParentTop=BooleanVA ';')?
& ('alignTop:' layout_alignTop=[View] ';')?
& ('alignWithParentIfMissing:' layout_alignWithParentIfMissing=BooleanVA ';')?
& ('below:' layout_below=[View] ';')?
& ('centerHorizontal:' layout_centerHorizontal=BooleanVA ';')?
& ('centerInParent:' layout_centerInParent=BooleanVA ';')?
& ('centerVertical:' layout_centerVertical=BooleanVA ';')?
& ('toLeftOf:' layout_toLeftOf=[View] ';')?
& ('toRightOf:' layout_toRightOf=[View] ';')?
)
'}'
;
Note about Unordered Groups: The language artifacts generation involving unordered groups is a memory intensive task. You may experience a delay in the generation process.
commit ea1fe99b347cc0a1433ff29319d8a652bc0ac57d
The properties height
and width
accept also contant values (look at ViewGroup.LayoutParams).
We can add this behaviour to our language by using Enum rules.
From Xtext manual:
Enum rules return enumeration literals from strings. They can be seen as a shortcut for data type rules with specific value converters. The main advantage of enum rules is their simplicity, type safety and therefore nice validation.
So, we are going to add a new ValueAccess
to represent this kind of "Dimension OR Constant" value:
ValueAccess:
(StringVA | IntegerVA | BooleanVA | AnyDrawableVA | DimensionVA | LayoutDimensionVA)
| ResourceAccess
;
LayoutDimensionVA:
DimensionVA | (const_value=LayoutDimensionKind)
;
enum LayoutDimensionKind:
fill_parent | match_parent | wrap_content
;
And then, change the LayoutProperties
:
LayoutProperties:
'layout:'
'{'
(
//LinearLayoutParams
('height:' layout_height=LayoutDimensionVA ';')?
& ('width:' layout_width=LayoutDimensionVA ';')?
& ('weight:' layout_weight=IntegerVA ';')?
& ('marginBottom:' layout_marginBottom=DimensionVA ';')?
& ('marginLeft:' layout_marginLeft=DimensionVA ';')?
& ('marginRight:' layout_marginRight=DimensionVA ';')?
& ('marginTop:' layout_marginTop=DimensionVA ';')?
//RelativeLayoutParams
& ('above:' layout_above=[View] ';')?
& ('alignBaseline:' layout_alignBaseline=[View] ';')?
& ('alignBottom:' layout_alignBottom=[View] ';')?
& ('alignLeft:' layout_alignLeft=[View] ';')?
& ('alignParentBottom:' layout_alignParentBottom=BooleanVA ';')?
& ('alignParentLeft:' layout_alignParentLeft=BooleanVA ';')?
& ('alignParentRight:' layout_alignParentRight=BooleanVA ';')?
& ('alignParentTop:' layout_alignParentTop=BooleanVA ';')?
& ('alignTop:' layout_alignTop=[View] ';')?
& ('alignWithParentIfMissing:' layout_alignWithParentIfMissing=BooleanVA ';')?
& ('below:' layout_below=[View] ';')?
& ('centerHorizontal:' layout_centerHorizontal=BooleanVA ';')?
& ('centerInParent:' layout_centerInParent=BooleanVA ';')?
& ('centerVertical:' layout_centerVertical=BooleanVA ';')?
& ('toLeftOf:' layout_toLeftOf=[View] ';')?
& ('toRightOf:' layout_toRightOf=[View] ';')?
)
'}'
;
commit 4ddfd29b3748cf35d5aea8a7e32b047533eacd27
A Layout can be either a LinearLayout
or RelativeLayout
, so to simplify things we are going to use just a flag to indicate if it is relative or linear (the default layout being a linear one).
Layout:
(isRelative?='relative')?
'layout' name=ID
'{'
(
//View Properties
('alpha:' alpha=FLOAT ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('minHeight:' minHeight=DimensionVA ';')?
& ('minWidth:' minWidth=DimensionVA ';')?
& ('nextFocusDown:' nextFocusDown=[View] ';')?
& ('nextFocusLeft:' nextFocusLeft=[View] ';')?
& ('nextFocusRight:' nextFocusRight=[View] ';')?
& ('nextFocusUp:' nextFocusUp=[View] ';')?
& ('padding:' padding=DimensionVA ';')?
& ('paddingBottom:' paddingBottom=DimensionVA';')?
& ('paddingLeft:' paddingLeft=DimensionVA';')?
& ('paddingRight:' paddingRight=DimensionVA';')?
& ('paddingTop:' paddingTop=DimensionVA';')?
& ('rotation:' rotation=DimensionVA';')?
& ('rotationX:' rotationX=DimensionVA';')?
& ('rotationY:' rotationY=DimensionVA';')?
& ('saveEnabled:' saveEnabled=BooleanVA';')?
& ('scaleX:' scaleX=DimensionVA';')?
& ('scaleY:' scaleY=DimensionVA';')?
& ('scrollX:' scrollX=DimensionVA';')?
& ('scrollY:' scrollY=DimensionVA';')?
& ('scrollbars:' scrollbars=BooleanVA';')?
& ('transformPivotX:' transformPivotX=DimensionVA';')?
& ('transformPivotY:' transformPivotY=DimensionVA';')?
& ('translationX:' translationX=DimensionVA';')?
& ('translationY:' translationY=DimensionVA';')?
& ('visibility:' visibility=LayoutVisibilityKind';')?
& (layoutProperties=LayoutProperties)?
//Layout Properties
& ('gravity:' gravity+=LayoutGravityKind ('|' gravity+=LayoutGravityKind)* ';')?
& ('orientation:' orientation=LayoutOrientationKind ';')?
)
views=ViewCollection
'}'
;
enum LayoutVisibilityKind:
visible | invisible | gone
;
enum LayoutGravityKind :
top | bottom | left | right |
center | center_vertical | center_horizontal |
fill | fill_vertical | fill_horizontal |
clip_vertical | clip_horizontal
;
enum LayoutOrientationKind:
horizontal | vertical
;
Here we are using another unordered group (a big one). That is a useful (but expensive) feature. Your DSL artifacts is going to take some more time to be generated. At this point yout should start experiencing some unexpected errors when you try to generate the language artifacts.
The first one is something like this:
warning(205): ../org.xtext.example.droid/src-gen/org/xtext/example/droid/parser/antlr/internal/InternalDroid.g:1:8: ANTLR could not analyze this decision in rule Tokens; often this is because of recursive rule references visible from the left edge of alternatives. ANTLR will re-analyze the decision with a fixed lookahead of k=1. Consider using "options {k=1;}" for that decision and possibly adding a syntactic predicate.
This error is caused by a timeout in the ANTLR parser generator, and can be solved by changing your generation workflow to increase the timeout:
Add this to your parser.antlr.XtextAntlrGeneratorFragment
inside the GenerateDroid.mwe2 file:
// The antlr parser generator fragment.
fragment = parser.antlr.XtextAntlrGeneratorFragment {
// options = {
// backtrack = true
// }
//http://www.antlr.org/pipermail/antlr-interest/2007-March/020014.html
//http://20000frames.blogspot.com/2010/09/dealing-with-could-not-even-do-k1-for.html
antlrParam = "-Xconversiontimeout" antlrParam = "10000"
}
After this, you can start having java.lang.OutOfMemoryError: Java heap space
errors.
To solve this, you need to increase the allowed Java VM memory to the generator.
Right-click the GenerateDroid.mwe2, and select Run As > Run Configurations...
.
Under the Arguments
tab place -Xmx512m
in the VM Arguments text box.
Now you should be ready to start to working with this heavy DSL.
TODO: EXAMPLE IMAGE
20338cfc43934a3d72b1b131dc99ad794de215e5
TextView:
'textView' (name=ID)?
text=StringVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
//TextView attributes
& ('autoLink:' autoLink=AutoLinkKind ';')?
& ('autoText:' autoText=BooleanVA ';')?
& ('capitalize:' capitalize=CapitalizeKind ';')?
& ('digits:' digits=StringVA ';')?
& ('editable:' editable=BooleanVA ';')?
& ('gravity:' gravity=LayoutGravityKind';')?
& ('hint:' hint=StringVA ';')?
& ('numeric:' numeric=BooleanVA ';')?
& ('password:' password=BooleanVA ';')?
& ('phoneNumber:' phoneNumber=BooleanVA ';')?
& ('singleLine:' singleLine=BooleanVA ';')?
& ('textColor:' textColor=ColorVA ';')?
& ('typeface:' typeface=TypefaceKind ';')?
& ('textSize:' textSize=DimensionVA ';')?
& ('textStyle:' textStyle+=TextStyleKind ('|' textStyle+=TextStyleKind)* ';')?
)
'}'
;
Button:
'button' (name=ID)?
( text=StringVA | src=AnyDrawableVA )
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
// Specific properties
& ('hint:' hint=StringVA ';')?
)
'}'
;
ImageView:
'imageView' (name=ID)?
src=AnyDrawableVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
)
'}'
;
Spinner:
'spinner' (name=ID)?
prompt=StringVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
)
'}'
;
EditText:
'editText' (name=ID)?
text=StringVA?
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
//TextView attributes
& ('autoLink:' autoLink=AutoLinkKind ';')?
& ('autoText:' autoText=BooleanVA ';')?
& ('capitalize:' capitalize=CapitalizeKind ';')?
& ('digits:' digits=StringVA ';')?
& ('editable:' editable=BooleanVA ';')?
& ('gravity:' gravity=LayoutGravityKind ';')?
& ('hint:' hint=StringVA ';')?
& ('numeric:' numeric=BooleanVA ';')?
& ('password:' password=BooleanVA ';')?
& ('phoneNumber:' phoneNumber=BooleanVA ';')?
& ('singleLine:' singleLine=BooleanVA ';')?
& ('textColor:' textColor=ColorVA';')?
& ('typeface:' typeface=TypefaceKind ';')?
& ('textSize:' textSize=DimensionVA ';')?
& ('textStyle:' textStyle+=TextStyleKind ('|' textStyle+=TextStyleKind)* ';')?
)
'}'
;
enum AutoLinkKind:
none | web | email | phone | map | all
;
enum CapitalizeKind:
none | sentences | words | characters
;
enum TypefaceKind:
normal | sans | serif | monospace
;
enum TextStyleKind:
normal | bold | italic
;
3bad2f0a79484bcff429bfed4238b77fb26332cf
These are some changes with the intent of making the DSL a bit more useful.
Application:
'application' name=STRING
'=>' packageName=PackageName
(
('version:' versionCode=INT '=>' versionName=STRING)?
& (sdkVersion=ApplicationUsesSDK)?
)
(screens+=Screen | layouts+=Layout | resources+=Resource)+
;
ApplicationUsesSDK:
'sdk:'
'{'
(
('min:' minSdkVersion=INT ';')?
& ('max:' maxSdkVersion=INT ';')?
& ('target:' targetSdkVersion=INT ';')?
)
'}'
;
Screen:
'screen' name=ID
'{'
(
('show' layout=[Layout])
| widgets=ViewCollection
)
'}'
;
3d7336df27c3d4c82004e46ce487f42f1fae2348
Add the Action
concept.
Action:
(GoToURLAction | ShowLayoutAction | InvokeScreenAction)
;
GoToURLAction:
'goTo' url=STRING
;
ShowLayoutAction:
'show' layout=[Layout]
;
InvokeScreenAction:
'invoke' activity=[Screen]
;
ButtonTarget returns InvokeScreenAction:
'to' screen=[Screen]
;
Then, add the onClick
property to the each View
(Layout
, and every Widget
):
Layout:
(isRelative?='relative')?
'layout' name=ID
'{'
(
//View Properties
('alpha:' alpha=FLOAT ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('minHeight:' minHeight=DimensionVA ';')?
& ('minWidth:' minWidth=DimensionVA ';')?
& ('nextFocusDown:' nextFocusDown=[View] ';')?
& ('nextFocusLeft:' nextFocusLeft=[View] ';')?
& ('nextFocusRight:' nextFocusRight=[View] ';')?
& ('nextFocusUp:' nextFocusUp=[View] ';')?
& ('onClick:' onClick=Action';')?
& ('padding:' padding=DimensionVA ';')?
& ('paddingBottom:' paddingBottom=DimensionVA ';')?
& ('paddingLeft:' paddingLeft=DimensionVA ';')?
& ('paddingRight:' paddingRight=DimensionVA ';')?
& ('paddingTop:' paddingTop=DimensionVA ';')?
& ('rotation:' rotation=DimensionVA ';')?
& ('rotationX:' rotationX=DimensionVA ';')?
& ('rotationY:' rotationY=DimensionVA ';')?
& ('saveEnabled:' saveEnabled=BooleanVA ';')?
& ('scaleX:' scaleX=DimensionVA ';')?
& ('scaleY:' scaleY=DimensionVA ';')?
& ('scrollX:' scrollX=DimensionVA ';')?
& ('scrollY:' scrollY=DimensionVA ';')?
& ('scrollbars:' scrollbars=BooleanVA ';')?
& ('transformPivotX:' transformPivotX=DimensionVA ';')?
& ('transformPivotY:' transformPivotY=DimensionVA ';')?
& ('translationX:' translationX=DimensionVA ';')?
& ('translationY:' translationY=DimensionVA ';')?
& ('visibility:' visibility=LayoutVisibilityKind ';')?
& (layoutProperties=LayoutProperties)?
//Layout Properties
& ('gravity:' gravity+=LayoutGravityKind ('|' gravity+=LayoutGravityKind)* ';')?
& ('orientation:' orientation=LayoutOrientationKind ';')?
)
views=ViewCollection
'}'
;
TextView:
'textView' (name=ID)?
text=StringVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
& ('onClick:' onClick=Action ';')?
//TextView attributes
& ('autoLink:' autoLink=AutoLinkKind ';')?
& ('autoText:' autoText=BooleanVA ';')?
& ('capitalize:' capitalize=CapitalizeKind ';')?
& ('digits:' digits=StringVA ';')?
& ('editable:' editable=BooleanVA ';')?
& ('gravity:' gravity=LayoutGravityKind';')?
& ('hint:' hint=StringVA ';')?
& ('numeric:' numeric=BooleanVA ';')?
& ('password:' password=BooleanVA ';')?
& ('phoneNumber:' phoneNumber=BooleanVA ';')?
& ('singleLine:' singleLine=BooleanVA ';')?
& ('textColor:' textColor=ColorVA ';')?
& ('typeface:' typeface=TypefaceKind ';')?
& ('textSize:' textSize=DimensionVA ';')?
& ('textStyle:' textStyle+=TextStyleKind ('|' textStyle+=TextStyleKind)* ';')?
)
'}'
;
Button:
'button' (name=ID)?
( text=StringVA | src=AnyDrawableVA )
( onClick=ButtonTarget )?
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('onClick:' onClick=Action ';')?
// Specific properties
& ('hint:' hint=StringVA ';')?
)
'}'
;
ImageView:
'imageView' (name=ID)?
src=AnyDrawableVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
& ('onClick:' onClick=Action ';')?
)
'}'
;
Spinner:
'spinner' (name=ID)?
prompt=StringVA
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
& ('onClick:' onClick=Action ';')?
)
'}'
;
EditText:
'editText' (name=ID)?
text=StringVA?
'{'
(
(layoutProperties=LayoutProperties)?
& ('top:' top=DimensionVA ';')?
& ('left:' left=DimensionVA ';')?
& ('width:' width=DimensionVA ';')?
& ('height:' height=DimensionVA ';')?
& ('background:' background=AnyDrawableVA ';')?
& ('clickable:' clickable=BooleanVA ';')?
& ('fadeScrollBars:' fadeScrollBars=BooleanVA ';')?
& ('isScrollContainer:' isScrollContainer=BooleanVA ';')?
& ('onClick:' onClick=Action ';')?
//TextView attributes
& ('autoLink:' autoLink=AutoLinkKind ';')?
& ('autoText:' autoText=BooleanVA ';')?
& ('capitalize:' capitalize=CapitalizeKind ';')?
& ('digits:' digits=StringVA ';')?
& ('editable:' editable=BooleanVA ';')?
& ('gravity:' gravity=LayoutGravityKind ';')?
& ('hint:' hint=StringVA ';')?
& ('numeric:' numeric=BooleanVA ';')?
& ('password:' password=BooleanVA ';')?
& ('phoneNumber:' phoneNumber=BooleanVA ';')?
& ('singleLine:' singleLine=BooleanVA ';')?
& ('textColor:' textColor=ColorVA';')?
& ('typeface:' typeface=TypefaceKind ';')?
& ('textSize:' textSize=DimensionVA ';')?
& ('textStyle:' textStyle+=TextStyleKind ('|' textStyle+=TextStyleKind)* ';')?
)
'}'
;
- File > Export ...
- Choose
Plug-in Development > Deployable plug-ins and fragments
- Select the projects
org.xtext.example.droid
,org.xtext.example.droid.generator
andorg.xtext.example.droid.ui
- Choose a destination folder.
- Copy the generated plugins (
org.xtext.example.droid_1.0.0.jar
,org.xtext.example.droid.generator_1.0.0.jar
andorg.xtext.example.droid.ui_1.0.0.jar
) from the folder chosen in the previous step to theplugins
folder (inside your Eclipse install). - Restart Eclipse
ea0002493f6f169a7e5321afe6776fad443dc114
This is an optional step and is useful, for example, if you want to develop several plugins and don't want they to be depending on Xtext. We are going to do it in order to use the same Ecore metamodel to the EEF.edit tutorial.
- File > New > Project...
- Eclipse Modeling Framework > Empty EMF Project
- Project name: "org.emf.example.droid"
- Finish the wizard.
Make sure you have generated the Xtext language artifacts (by running the MWE2 Workflow). It will update the generated Ecore metamodel.
- Locate the file
Droid.ecore
in theorg.xtext.example.droid
project (the Xtext project). It is located at thesrc-gen
folder, inside theorg.xtext.example.droid
package. - Move the file to the
org.emf.example.droid
project (the EMF project). Place it under themodel
folder. - Create a new Genmodel file in the EMF project
- In the
model
folder, select theDroid.ecore
file and go to menu File > New > Other... - Eclipse Modeling Framework > EMF Generator Model
- Name the file
Droid.genmodel
and place it inside themodel
folder - Select
Ecore model
-
Browse workspace
and chose theDroid.ecore
file. - Open the created
Droid.genmodel
file. - Select its root item
Droid
and click the menuGenerator > Generate Model Code
NOTE: To avoiding errors you must remove all the content from the src-gen
folder in the org.xtext.example.droid
project (the Xtext project) and regenerate the language artifacts.
- Right-click the EMF project (org.emf.example.droid) and select
Export
- Choose
Plug-in Development > Deployable plug-ins and fragments
- Choose a destination folder.
- Copy the generated plugin (
org.emf.example.droid_1.0.0.jar
) from the folder chosen in the previous step to theplugins
folder (inside your Eclipse install). - Restart Eclipse
-
Open the
plugin.xml
file inside your Xtext project (org.example.xtext.droid
) -
Under the
Dependencies
tab, clickAdd
to add a dependency to your Xtext plugin. -
Chose the EMF plugin
org.emf.example.droid
-
Select the added dependency and click
Properties
-
Check the
Reexport this dependency
option. -
Go to the
Extensions
tab and remove theorg.eclipse.emf.ecore.generated_package
extension. -
Go to the
Runtime
tab and remove theorg.xtext.example.droid.droid
,org.xtext.example.droid.droid.impl
andorg.xtext.example.droid.droid.util
packages from theExported Packages
list. -
Open the
GenerateDroid.mwe2
file -
Add the
registerGeneratedEPackage = "droid.DroidPackage"
to thebean = StandaloneSetup { ... }
insideWorkflow { ... }
-
Open the
Droid.xtext
and changegenerate droid "http://www.xtext.org/example/droid/Droid"
toimport "http://www.xtext.org/example/droid/Droid"
-
Regenerate the language artifacts and run the generated editor.