Ajout du GUI
This commit is contained in:
883
kivy/lang/__init__.py
Normal file
883
kivy/lang/__init__.py
Normal file
@@ -0,0 +1,883 @@
|
||||
'''Kivy Language
|
||||
=============
|
||||
|
||||
The Kivy language is a language dedicated to describing user interface and
|
||||
interactions. You could compare this language to Qt's QML
|
||||
(http://qt.nokia.com), but we included new concepts such as rule definitions
|
||||
(which are somewhat akin to what you may know from CSS), templating and so on.
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
|
||||
The Builder doesn't execute canvas expressions in realtime anymore. It will
|
||||
pack all the expressions that need to be executed first and execute them
|
||||
after dispatching input, just before drawing the frame. If you want to
|
||||
force the execution of canvas drawing, just call
|
||||
:meth:`Builder.sync <BuilderBase.sync>`.
|
||||
|
||||
An experimental profiling tool for the kv lang is also included. You can
|
||||
activate it by setting the environment variable `KIVY_PROFILE_LANG=1`.
|
||||
It will then generate an html file named `builder_stats.html`.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The language consists of several constructs that you can use:
|
||||
|
||||
Rules
|
||||
A rule is similar to a CSS rule. A rule applies to specific widgets (or
|
||||
classes thereof) in your widget tree and modifies them in a
|
||||
certain way.
|
||||
You can use rules to specify interactive behaviour or use them to add
|
||||
graphical representations of the widgets they apply to.
|
||||
You can target a specific class of widgets (similar to the CSS
|
||||
concept of a *class*) by using the ``cls`` attribute (e.g.
|
||||
``cls=MyTestWidget``).
|
||||
|
||||
A Root Widget
|
||||
You can use the language to create your entire user interface.
|
||||
A kv file must contain only one root widget at most.
|
||||
|
||||
Dynamic Classes
|
||||
*(introduced in version 1.7.0)*
|
||||
Dynamic classes let you create new widgets and rules on-the-fly,
|
||||
without any Python declaration.
|
||||
|
||||
Templates (deprecated)
|
||||
*(introduced in version 1.0.5, deprecated from version 1.7.0)*
|
||||
Templates were used to populate parts of an application, such as
|
||||
styling the content of a list (e.g. icon on the left, text on the
|
||||
right). They are now deprecated by dynamic classes.
|
||||
|
||||
|
||||
Syntax of a kv File
|
||||
-------------------
|
||||
|
||||
.. highlight:: kv
|
||||
|
||||
A Kivy language file must have ``.kv`` as filename extension.
|
||||
|
||||
The content of the file should always start with the Kivy header, where
|
||||
`version` must be replaced with the Kivy language version you're using.
|
||||
For now, use 1.0::
|
||||
|
||||
#:kivy `1.0`
|
||||
|
||||
# content here
|
||||
|
||||
The `content` can contain rule definitions, a root widget, dynamic class
|
||||
definitions and templates::
|
||||
|
||||
# Syntax of a rule definition. Note that several Rules can share the same
|
||||
# definition (as in CSS). Note the braces: they are part of the definition.
|
||||
<Rule1,Rule2>:
|
||||
# .. definitions ..
|
||||
|
||||
<Rule3>:
|
||||
# .. definitions ..
|
||||
|
||||
# Syntax for creating a root widget
|
||||
RootClassName:
|
||||
# .. definitions ..
|
||||
|
||||
# Syntax for creating a dynamic class
|
||||
<NewWidget@BaseClass>:
|
||||
# .. definitions ..
|
||||
|
||||
# Syntax for create a template
|
||||
[TemplateName@BaseClass1,BaseClass2]:
|
||||
# .. definitions ..
|
||||
|
||||
Regardless of whether it's a rule, root widget, dynamic class or
|
||||
template you're defining, the definition should look like this::
|
||||
|
||||
# With the braces it's a rule. Without them, it's a root widget.
|
||||
<ClassName>:
|
||||
prop1: value1
|
||||
prop2: value2
|
||||
|
||||
canvas:
|
||||
CanvasInstruction1:
|
||||
canvasprop1: value1
|
||||
CanvasInstruction2:
|
||||
canvasprop2: value2
|
||||
|
||||
AnotherClass:
|
||||
prop3: value1
|
||||
|
||||
Here `prop1` and `prop2` are the properties of `ClassName` and `prop3` is the
|
||||
property of `AnotherClass`. If the widget doesn't have a property with
|
||||
the given name, an :class:`~kivy.properties.ObjectProperty` will be
|
||||
automatically created and added to the widget.
|
||||
|
||||
`AnotherClass` will be created and added as a child of the `ClassName`
|
||||
instance.
|
||||
|
||||
- The indentation is important and must be consistent. The spacing must be a
|
||||
multiple of the number of spaces used on the first indented line. Spaces
|
||||
are encouraged: mixing tabs and spaces is not recommended.
|
||||
- The value of a property must be given on a single line (for now at least).
|
||||
- Keep class names capitalized to avoid syntax errors.
|
||||
- The `canvas` property is special: you can put graphics instructions in it
|
||||
to create a graphical representation of the current class.
|
||||
|
||||
|
||||
Here is a simple example of a kv file that contains a root widget::
|
||||
|
||||
#:kivy 1.0
|
||||
|
||||
Button:
|
||||
text: 'Hello world'
|
||||
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
|
||||
The indentation is not limited to 4 spaces anymore. The spacing must be a
|
||||
multiple of the number of spaces used on the first indented line.
|
||||
|
||||
Both the :meth:`~BuilderBase.load_file` and the
|
||||
:meth:`~BuilderBase.load_string` methods
|
||||
return the root widget defined in your kv file/string. They will also add any
|
||||
class and template definitions to the :class:`~kivy.factory.Factory` for later
|
||||
usage.
|
||||
|
||||
Value Expressions, on_property Expressions, ids, and Reserved Keywords
|
||||
---------------------------------------------------------------------
|
||||
|
||||
When you specify a property's value, the value is evaluated as a Python
|
||||
expression. This expression can be static or dynamic, which means that
|
||||
the value can use the values of other properties using reserved keywords.
|
||||
|
||||
self
|
||||
The keyword self references the "current widget instance"::
|
||||
|
||||
Button:
|
||||
text: 'My state is %s' % self.state
|
||||
|
||||
root
|
||||
This keyword is available only in rule definitions and represents the
|
||||
root widget of the rule (the first instance of the rule)::
|
||||
|
||||
<MyWidget>:
|
||||
custom: 'Hello world'
|
||||
Button:
|
||||
text: root.custom
|
||||
|
||||
app
|
||||
This keyword always refers to your app instance. It's equivalent
|
||||
to a call to :meth:`kivy.app.App.get_running_app` in Python. ::
|
||||
|
||||
Label:
|
||||
text: app.name
|
||||
|
||||
args
|
||||
This keyword is available in on_<action> callbacks. It refers to the
|
||||
arguments passed to the callback. ::
|
||||
|
||||
TextInput:
|
||||
on_focus: self.insert_text("Focus" if args[1] else "No focus")
|
||||
|
||||
|
||||
.. versionchanged:: 2.1.0
|
||||
|
||||
f-strings are now parsed in value expressions, allowing to bind to the
|
||||
properties that they contain.
|
||||
|
||||
.. kv-lang-ids:
|
||||
|
||||
ids
|
||||
~~~
|
||||
|
||||
Class definitions may contain ids which can be used as a keywords:::
|
||||
|
||||
<MyWidget>:
|
||||
Button:
|
||||
id: btn1
|
||||
Button:
|
||||
text: 'The state of the other button is %s' % btn1.state
|
||||
|
||||
Please note that the `id` will not be available in the widget instance:
|
||||
it is used exclusively for external references. `id` is a weakref to the
|
||||
widget, and not the widget itself. The widget itself can be accessed
|
||||
with `<id>.__self__` (`btn1.__self__` in this case).
|
||||
|
||||
When the kv file is processed, weakrefs to all the widgets tagged with ids are
|
||||
added to the root widget's `ids` dictionary. In other words, following on from
|
||||
the example above, the buttons state could also be accessed as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
widget = MyWidget()
|
||||
state = widget.ids["btn1"].state
|
||||
|
||||
# Or, as an alternative syntax,
|
||||
state = widget.ids.btn1.state
|
||||
|
||||
Note that the outermost widget applies the kv rules to all its inner widgets
|
||||
before any other rules are applied. This means if an inner widget contains ids,
|
||||
these ids may not be available during the inner widget's `__init__` function.
|
||||
|
||||
Valid expressions
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are two places that accept python statements in a kv file:
|
||||
after a property, which assigns to the property the result of the expression
|
||||
(such as the text of a button as shown above) and after a on_property, which
|
||||
executes the statement when the property is updated (such as on_state).
|
||||
|
||||
In the former case, the
|
||||
`expression <http://docs.python.org/2/reference/expressions.html>`_ can only
|
||||
span a single line, cannot be extended to multiple lines using newline
|
||||
escaping, and must return a value. An example of a valid expression is
|
||||
``text: self.state and ('up' if self.state == 'normal' else 'down')``.
|
||||
|
||||
In the latter case, multiple single line statements are valid, including
|
||||
those that escape their newline, as long as they don't add an indentation
|
||||
level.
|
||||
|
||||
Examples of valid statements are:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
on_press: if self.state == 'normal': print('normal')
|
||||
on_state:
|
||||
if self.state == 'normal': print('normal')
|
||||
else: print('down')
|
||||
if self.state == 'normal': \\
|
||||
print('multiline normal')
|
||||
for i in range(10): print(i)
|
||||
print([1,2,3,4,
|
||||
5,6,7])
|
||||
|
||||
An example of a invalid statement:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
on_state:
|
||||
if self.state == 'normal':
|
||||
print('normal')
|
||||
|
||||
Relation Between Values and Properties
|
||||
--------------------------------------
|
||||
|
||||
When you use the Kivy language, you might notice that we do some work
|
||||
behind the scenes to automatically make things work properly. You should
|
||||
know that :doc:`api-kivy.properties` implement the
|
||||
`Observer Design Pattern <http://en.wikipedia.org/wiki/Observer_pattern>`_.
|
||||
That means that you can bind your own function to be
|
||||
called when the value of a property changes (i.e. you passively
|
||||
`observe` the property for potential changes).
|
||||
|
||||
The Kivy language detects properties in your `value` expression and will create
|
||||
callbacks to automatically update the property via your expression when changes
|
||||
occur.
|
||||
|
||||
Here's a simple example that demonstrates this behaviour::
|
||||
|
||||
Button:
|
||||
text: str(self.state)
|
||||
|
||||
In this example, the parser detects that `self.state` is a dynamic value (a
|
||||
property). The :attr:`~kivy.uix.button.Button.state` property of the button
|
||||
can change at any moment (when the user touches it).
|
||||
We now want this button to display its own state as text, even as the state
|
||||
changes. To do this, we use the state property of the Button and use it in the
|
||||
value expression for the button's `text` property, which controls what text is
|
||||
displayed on the button (We also convert the state to a string representation).
|
||||
Now, whenever the button state changes, the text property will be updated
|
||||
automatically.
|
||||
|
||||
Remember: The value is a python expression! That means that you can do
|
||||
something more interesting like::
|
||||
|
||||
Button:
|
||||
text: 'Plop world' if self.state == 'normal' else 'Release me!'
|
||||
|
||||
The Button text changes with the state of the button. By default, the button
|
||||
text will be 'Plop world', but when the button is being pressed, the text will
|
||||
change to 'Release me!'.
|
||||
|
||||
More precisely, the kivy language parser detects all substrings of the form
|
||||
`X.a.b` where `X` is `self` or `root` or `app` or a known id, and `a` and `b`
|
||||
are properties: it then adds the appropriate dependencies to cause the
|
||||
the constraint to be reevaluated whenever something changes. For example,
|
||||
this works exactly as expected::
|
||||
|
||||
<IndexedExample>:
|
||||
beta: self.a.b[self.c.d]
|
||||
|
||||
However, due to limitations in the parser which hopefully may be lifted in the
|
||||
future, the following doesn't work::
|
||||
|
||||
<BadExample>:
|
||||
beta: self.a.b[self.c.d].e.f
|
||||
|
||||
indeed the `.e.f` part is not recognized because it doesn't follow the expected
|
||||
pattern, and so, does not result in an appropriate dependency being setup.
|
||||
Instead, an intermediate property should be introduced to allow the following
|
||||
constraint::
|
||||
|
||||
<GoodExample>:
|
||||
alpha: self.a.b[self.c.d]
|
||||
beta: self.alpha.e.f
|
||||
|
||||
In addition, properties in python f-strings are also not yet supported::
|
||||
|
||||
<FStringExample>:
|
||||
text: f"I want to use {self.a} in property"
|
||||
|
||||
Instead, the ``format()`` method should be used::
|
||||
|
||||
<FormatStringExample>:
|
||||
text: "I want to use {} in property".format(self.a)
|
||||
|
||||
|
||||
Graphical Instructions
|
||||
----------------------
|
||||
|
||||
The graphical instructions are a special part of the Kivy language. They are
|
||||
handled by the 'canvas' property definition::
|
||||
|
||||
Widget:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (1, 1, 1)
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
|
||||
All the classes added inside the canvas property must be derived from the
|
||||
:class:`~kivy.graphics.Instruction` class. You cannot put any Widget class
|
||||
inside the canvas property (as that would not make sense because a
|
||||
widget is not a graphics instruction).
|
||||
|
||||
If you want to do theming, you'll have the same question as in CSS: which rules
|
||||
have been executed first? In our case, the rules are executed
|
||||
in processing order (i.e. top-down).
|
||||
|
||||
If you want to change how Buttons are rendered, you can create your own kv file
|
||||
and add something like this::
|
||||
|
||||
<Button>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (1, 0, 0)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.texture_size
|
||||
texture: self.texture
|
||||
|
||||
This will result in buttons having a red background with the label in the
|
||||
bottom left, in addition to all the preceding rules.
|
||||
You can clear all the previous instructions by using the `Clear` command::
|
||||
|
||||
<Button>:
|
||||
canvas:
|
||||
Clear
|
||||
Color:
|
||||
rgb: (1, 0, 0)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.size
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: self.texture_size
|
||||
texture: self.texture
|
||||
|
||||
Then, only your rules that follow the `Clear` command will be taken into
|
||||
consideration.
|
||||
|
||||
.. _dynamic_classes:
|
||||
|
||||
Dynamic classes
|
||||
---------------
|
||||
|
||||
Dynamic classes allow you to create new widgets on-the-fly, without any python
|
||||
declaration in the first place. The syntax of the dynamic classes is similar to
|
||||
the Rules, but you need to specify the base classes you want to
|
||||
subclass.
|
||||
|
||||
The syntax looks like:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
# Simple inheritance
|
||||
<NewWidget@Button>:
|
||||
# kv code here ...
|
||||
|
||||
# Multiple inheritance
|
||||
<NewWidget@ButtonBehavior+Label>:
|
||||
# kv code here ...
|
||||
|
||||
The `@` character is used to separate your class name from the classes you want
|
||||
to subclass. The Python equivalent would have been:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Simple inheritance
|
||||
class NewWidget(Button):
|
||||
pass
|
||||
|
||||
# Multiple inheritance
|
||||
class NewWidget(ButtonBehavior, Label):
|
||||
pass
|
||||
|
||||
Any new properties, usually added in python code, should be declared
|
||||
first. If the property doesn't exist in the dynamic class, it will be
|
||||
automatically created as an :class:`~kivy.properties.ObjectProperty`
|
||||
(pre 1.8.0) or as an appropriate typed property (from version
|
||||
1.8.0).
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
|
||||
If the property value is an expression that can be evaluated right away (no
|
||||
external binding), then the value will be used as default value of the
|
||||
property, and the type of the value will be used for the specialization of
|
||||
the Property class. In other terms: if you declare `hello: "world"`, a new
|
||||
:class:`~kivy.properties.StringProperty` will be instantiated, with the
|
||||
default value `"world"`. Lists, tuples, dictionaries and strings are
|
||||
supported.
|
||||
|
||||
Let's illustrate the usage of these dynamic classes with an
|
||||
implementation of a basic Image button. We could derive our classes from
|
||||
the Button and just add a property for the image filename:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<ImageButton@Button>:
|
||||
source: None
|
||||
|
||||
Image:
|
||||
source: root.source
|
||||
pos: root.pos
|
||||
size: root.size
|
||||
|
||||
# let's use the new classes in another rule:
|
||||
<MainUI>:
|
||||
BoxLayout:
|
||||
ImageButton:
|
||||
source: 'hello.png'
|
||||
on_press: root.do_something()
|
||||
ImageButton:
|
||||
source: 'world.png'
|
||||
on_press: root.do_something_else()
|
||||
|
||||
In Python, you can create an instance of the dynamic class as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.factory import Factory
|
||||
button_inst = Factory.ImageButton()
|
||||
|
||||
.. note::
|
||||
|
||||
Using dynamic classes, a child class can be declared before its parent.
|
||||
This however, leads to the unintuitive situation where the parent
|
||||
properties/methods override those of the child. Be careful if you choose
|
||||
to do this.
|
||||
|
||||
.. _template_usage:
|
||||
|
||||
Templates
|
||||
---------
|
||||
|
||||
.. versionchanged:: 1.7.0
|
||||
|
||||
Template usage is now deprecated. Please use Dynamic classes instead.
|
||||
|
||||
Syntax of templates
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Using a template in Kivy requires 2 things :
|
||||
|
||||
#. a context to pass for the context (will be ctx inside template).
|
||||
#. a kv definition of the template.
|
||||
|
||||
Syntax of a template:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
# With only one base class
|
||||
[ClassName@BaseClass]:
|
||||
# .. definitions ..
|
||||
|
||||
# With more than one base class
|
||||
[ClassName@BaseClass1,BaseClass2]:
|
||||
# .. definitions ..
|
||||
|
||||
For example, for a list, you'll need to create a entry with a image on
|
||||
the left, and a label on the right. You can create a template for making
|
||||
that definition easier to use.
|
||||
So, we'll create a template that uses 2 entries in the context: an image
|
||||
filename and a title:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
[IconItem@BoxLayout]:
|
||||
Image:
|
||||
source: ctx.image
|
||||
Label:
|
||||
text: ctx.title
|
||||
|
||||
Then in Python, you can instantiate the template using:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from kivy.lang import Builder
|
||||
|
||||
# create a template with hello world + an image
|
||||
# the context values should be passed as kwargs to the Builder.template
|
||||
# function
|
||||
icon1 = Builder.template('IconItem', title='Hello world',
|
||||
image='myimage.png')
|
||||
|
||||
# create a second template with other information
|
||||
ctx = {'title': 'Another hello world',
|
||||
'image': 'myimage2.png'}
|
||||
icon2 = Builder.template('IconItem', **ctx)
|
||||
# and use icon1 and icon2 as other widget.
|
||||
|
||||
|
||||
Template example
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Most of time, when you are creating a screen in the kv lang, you use a lot of
|
||||
redefinitions. In our example, we'll create a Toolbar, based on a
|
||||
BoxLayout, and put in a few :class:`~kivy.uix.image.Image` widgets that
|
||||
will react to the *on_touch_down* event.
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyToolbar>:
|
||||
BoxLayout:
|
||||
Image:
|
||||
source: 'data/text.png'
|
||||
size: self.texture_size
|
||||
size_hint: None, None
|
||||
on_touch_down: self.collide_point(*args[1].pos) and\
|
||||
root.create_text()
|
||||
|
||||
Image:
|
||||
source: 'data/image.png'
|
||||
size: self.texture_size
|
||||
size_hint: None, None
|
||||
on_touch_down: self.collide_point(*args[1].pos) and\
|
||||
root.create_image()
|
||||
|
||||
Image:
|
||||
source: 'data/video.png'
|
||||
size: self.texture_size
|
||||
size_hint: None, None
|
||||
on_touch_down: self.collide_point(*args[1].pos) and\
|
||||
root.create_video()
|
||||
|
||||
We can see that the size and size_hint attribute are exactly the same.
|
||||
More than that, the callback in on_touch_down and the image are changing.
|
||||
These can be the variable part of the template that we can put into a context.
|
||||
Let's try to create a template for the Image:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
[ToolbarButton@Image]:
|
||||
|
||||
# This is the same as before
|
||||
size: self.texture_size
|
||||
size_hint: None, None
|
||||
|
||||
# Now, we are using the ctx for the variable part of the template
|
||||
source: 'data/%s.png' % ctx.image
|
||||
on_touch_down: self.collide_point(*args[1].pos) and ctx.callback()
|
||||
|
||||
The template can be used directly in the MyToolbar rule:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyToolbar>:
|
||||
BoxLayout:
|
||||
ToolbarButton:
|
||||
image: 'text'
|
||||
callback: root.create_text
|
||||
ToolbarButton:
|
||||
image: 'image'
|
||||
callback: root.create_image
|
||||
ToolbarButton:
|
||||
image: 'video'
|
||||
callback: root.create_video
|
||||
|
||||
That's all :)
|
||||
|
||||
|
||||
Template limitations
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When you are creating a context:
|
||||
|
||||
#. you cannot use references other than "root":
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyRule>:
|
||||
Widget:
|
||||
id: mywidget
|
||||
value: 'bleh'
|
||||
Template:
|
||||
ctxkey: mywidget.value # << fail, this references the id
|
||||
# mywidget
|
||||
|
||||
#. not all of the dynamic parts will be understood:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyRule>:
|
||||
Template:
|
||||
ctxkey: 'value 1' if root.prop1 else 'value2' # << even if
|
||||
# root.prop1 is a property, if it changes value, ctxkey
|
||||
# will not be updated
|
||||
|
||||
Template definitions also replace any similarly named definitions in their
|
||||
entirety and thus do not support inheritance.
|
||||
|
||||
.. _redefining-style:
|
||||
|
||||
Redefining a widget's style
|
||||
---------------------------
|
||||
|
||||
Sometimes we would like to inherit from a widget in order to use its Python
|
||||
properties without also using its .kv defined style. For example, we would
|
||||
like to inherit from a Label, but we would also like to define our own
|
||||
canvas instructions instead of automatically using the canvas instructions
|
||||
inherited from the Label. We can achieve this by prepending a dash (-) before
|
||||
the class name in the .kv style definition.
|
||||
|
||||
In myapp.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(Label):
|
||||
pass
|
||||
|
||||
and in my.kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<-MyWidget>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: 1, 1, 1
|
||||
Rectangle:
|
||||
size: (32, 32)
|
||||
|
||||
MyWidget will now have a Color and Rectangle instruction in its canvas
|
||||
without any of the instructions inherited from the Label.
|
||||
|
||||
Redefining a widget's property style
|
||||
------------------------------------
|
||||
|
||||
Similar to :ref:`redefining style <redefining-style>`, sometimes we
|
||||
would like to inherit from a widget, keep all its KV defined styles, except for
|
||||
the style applied to a specific property. For example, we would
|
||||
like to inherit from a :class:`~kivy.uix.button.Button`, but we would also
|
||||
like to set our own `state_image`, rather then relying on the
|
||||
`background_normal` and `background_down` values. We can achieve this by
|
||||
prepending a dash (-) before the `state_image` property name in the .kv style
|
||||
definition.
|
||||
|
||||
In myapp.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyWidget(Button):
|
||||
new_background = StringProperty('my_background.png')
|
||||
|
||||
and in my.kv:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyWidget>:
|
||||
-state_image: self.new_background
|
||||
|
||||
MyWidget will now have a `state_image` background set only by `new_background`,
|
||||
and not by any previous styles that may have set `state_image`.
|
||||
|
||||
.. note::
|
||||
|
||||
Although the previous rules are cleared, they are still applied during
|
||||
widget construction and are only removed when the new rule with the dash
|
||||
is reached. This means that initially, previous rules could be used to set
|
||||
the property.
|
||||
|
||||
Order of kwargs and KV rule application
|
||||
---------------------------------------
|
||||
|
||||
Properties can be initialized in KV as well as in python. For example, in KV:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
<MyRule@Widget>:
|
||||
text: 'Hello'
|
||||
ramp: 45.
|
||||
order: self.x + 10
|
||||
|
||||
Then `MyRule()` would initialize all three kivy properties to
|
||||
the given KV values. Separately in python, if the properties already exist as
|
||||
kivy properties one can do for example `MyRule(line='Bye', side=55)`.
|
||||
|
||||
However, what will be the final values of the properties when
|
||||
`MyRule(text='Bye', order=55)` is executed? The quick rule is that python
|
||||
initialization is stronger than KV initialization only for constant rules.
|
||||
|
||||
Specifically, the `kwargs` provided to the python initializer are always
|
||||
applied first. So in the above example, `text` is set to
|
||||
`'Bye'` and `order` is set to `55`. Then, all the KV rules are applied, except
|
||||
those constant rules that overwrite a python initializer provided value.
|
||||
|
||||
That is, the KV rules that do not creates bindings such as `text: 'Hello'`
|
||||
and `ramp: 45.`, if a value for that property has been provided in python, then
|
||||
that rule will not be applied.
|
||||
|
||||
So in the `MyRule(text='Bye', order=55)` example, `text` will be `'Bye'`,
|
||||
`ramp` will be `45.`, and `order`, which creates a binding, will first be set
|
||||
to `55`, but then when KV rules are applied will end up being whatever
|
||||
`self.x + 10` is.
|
||||
|
||||
.. versionchanged:: 1.9.1
|
||||
|
||||
Before, KV rules always overwrote the python values, now, python values
|
||||
are not overwritten by constant rules.
|
||||
|
||||
|
||||
Lang Directives
|
||||
---------------
|
||||
|
||||
You can use directives to add declarative commands, such as imports or constant
|
||||
definitions, to the lang files. Directives are added as comments in the
|
||||
following format:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:<directivename> <options>
|
||||
|
||||
import <package>
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 1.0.5
|
||||
|
||||
Syntax:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:import <alias> <package>
|
||||
|
||||
You can import a package by writing:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:import os os
|
||||
|
||||
<Rule>:
|
||||
Button:
|
||||
text: os.getcwd()
|
||||
|
||||
Or more complex:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:import ut kivy.utils
|
||||
|
||||
<Rule>:
|
||||
canvas:
|
||||
Color:
|
||||
rgba: ut.get_random_color()
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
|
||||
You can directly import classes from a module:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#: import Animation kivy.animation.Animation
|
||||
<Rule>:
|
||||
on_prop: Animation(x=.5).start(self)
|
||||
|
||||
set <key> <expr>
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 1.0.6
|
||||
|
||||
Syntax:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:set <key> <expr>
|
||||
|
||||
Set a key that will be available anywhere in the kv. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:set my_color (.4, .3, .4)
|
||||
#:set my_color_hl (.5, .4, .5)
|
||||
|
||||
<Rule>:
|
||||
state: 'normal'
|
||||
canvas:
|
||||
Color:
|
||||
rgb: my_color if self.state == 'normal' else my_color_hl
|
||||
|
||||
include <file>
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
Syntax:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:include [force] <file>
|
||||
|
||||
Includes an external kivy file. This allows you to split complex
|
||||
widgets into their own files. If the include is forced, the file
|
||||
will first be unloaded and then reloaded again. For example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
# Test.kv
|
||||
#:include mycomponent.kv
|
||||
#:include force mybutton.kv
|
||||
|
||||
<Rule>:
|
||||
state: 'normal'
|
||||
MyButton:
|
||||
MyComponent:
|
||||
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
# mycomponent.kv
|
||||
#:include mybutton.kv
|
||||
|
||||
<MyComponent>:
|
||||
MyButton:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
# mybutton.kv
|
||||
|
||||
<MyButton>:
|
||||
canvas:
|
||||
Color:
|
||||
rgb: (1.0, 0.0, 0.0)
|
||||
Rectangle:
|
||||
pos: self.pos
|
||||
size: (self.size[0]/4, self.size[1]/4)
|
||||
|
||||
'''
|
||||
|
||||
|
||||
from kivy.lang.builder import (Observable, Builder, BuilderBase,
|
||||
BuilderException)
|
||||
from kivy.lang.parser import Parser, ParserException, global_idmap
|
||||
|
||||
__all__ = ('Observable', 'Builder', 'BuilderBase', 'BuilderException',
|
||||
'Parser', 'ParserException', 'global_idmap')
|
||||
BIN
kivy/lang/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
kivy/lang/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/lang/__pycache__/builder.cpython-310.pyc
Normal file
BIN
kivy/lang/__pycache__/builder.cpython-310.pyc
Normal file
Binary file not shown.
BIN
kivy/lang/__pycache__/parser.cpython-310.pyc
Normal file
BIN
kivy/lang/__pycache__/parser.cpython-310.pyc
Normal file
Binary file not shown.
999
kivy/lang/builder.py
Normal file
999
kivy/lang/builder.py
Normal file
@@ -0,0 +1,999 @@
|
||||
'''
|
||||
Builder
|
||||
======
|
||||
|
||||
Class used for the registering and application of rules for specific widgets.
|
||||
'''
|
||||
import sys
|
||||
from os import environ
|
||||
from os.path import join
|
||||
from copy import copy
|
||||
from types import CodeType
|
||||
from functools import partial
|
||||
|
||||
from kivy.factory import Factory
|
||||
from kivy.lang.parser import Parser, ParserException, _handlers, global_idmap,\
|
||||
ParserRuleProperty
|
||||
from kivy.logger import Logger
|
||||
from kivy.utils import QueryDict
|
||||
from kivy.cache import Cache
|
||||
from kivy import kivy_data_dir
|
||||
from kivy.context import register_context
|
||||
from kivy.resources import resource_find
|
||||
from kivy._event import Observable, EventDispatcher
|
||||
|
||||
__all__ = ('Observable', 'Builder', 'BuilderBase', 'BuilderException')
|
||||
|
||||
|
||||
trace = Logger.trace
|
||||
|
||||
# late import
|
||||
Instruction = None
|
||||
|
||||
# delayed calls are canvas expression triggered during an loop. It is one
|
||||
# directional linked list of args to call call_fn with. Each element is a list
|
||||
# whose last element points to the next list of args to execute when
|
||||
# Builder.sync is called.
|
||||
_delayed_start = None
|
||||
|
||||
|
||||
class BuilderException(ParserException):
|
||||
'''Exception raised when the Builder failed to apply a rule on a widget.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
def get_proxy(widget):
|
||||
try:
|
||||
return widget.proxy_ref
|
||||
except AttributeError:
|
||||
return widget
|
||||
|
||||
|
||||
def custom_callback(__kvlang__, idmap, *largs, **kwargs):
|
||||
idmap['args'] = largs
|
||||
exec(__kvlang__.co_value, idmap)
|
||||
|
||||
|
||||
def call_fn(args, instance, v):
|
||||
element, key, value, rule, idmap = args
|
||||
if __debug__:
|
||||
trace('Lang: call_fn %s, key=%s, value=%r, %r' % (
|
||||
element, key, value, rule.value))
|
||||
rule.count += 1
|
||||
e_value = eval(value, idmap)
|
||||
if __debug__:
|
||||
trace('Lang: call_fn => value=%r' % (e_value, ))
|
||||
setattr(element, key, e_value)
|
||||
|
||||
|
||||
def delayed_call_fn(args, instance, v):
|
||||
# it's already on the list
|
||||
if args[-1] is not None:
|
||||
return
|
||||
|
||||
global _delayed_start
|
||||
if _delayed_start is None:
|
||||
_delayed_start = args
|
||||
args[-1] = StopIteration
|
||||
else:
|
||||
args[-1] = _delayed_start
|
||||
_delayed_start = args
|
||||
|
||||
|
||||
def update_intermediates(base, keys, bound, s, fn, args, instance, value):
|
||||
''' Function that is called when an intermediate property is updated
|
||||
and `rebind` of that property is True. In that case, we unbind
|
||||
all bound funcs that were bound to attrs of the old value of the
|
||||
property and rebind to the new value of the property.
|
||||
|
||||
For example, if the rule is `self.a.b.c.d`, then when b is changed, we
|
||||
unbind from `b`, `c` and `d`, if they were bound before (they were not
|
||||
None and `rebind` of the respective properties was True) and we rebind
|
||||
to the new values of the attrs `b`, `c``, `d` that are not None and
|
||||
`rebind` is True.
|
||||
|
||||
:Parameters:
|
||||
`base`
|
||||
A (proxied) ref to the base widget, `self` in the example
|
||||
above.
|
||||
`keys`
|
||||
A list of the name off the attrs of `base` being watched. In
|
||||
the example above it'd be `['a', 'b', 'c', 'd']`.
|
||||
`bound`
|
||||
A list 4-tuples, each tuple being (widget, attr, callback, uid)
|
||||
representing callback functions bound to the attributed `attr`
|
||||
of `widget`. `uid` is returned by `fbind` when binding.
|
||||
The callback may be None, in which case the attr
|
||||
was not bound, but is there to be able to walk the attr tree.
|
||||
E.g. in the example above, if `b` was not an eventdispatcher,
|
||||
`(_b_ref_, `c`, None)` would be added to the list so we can get
|
||||
to `c` and `d`, which may be eventdispatchers and their attrs.
|
||||
`s`
|
||||
The index in `keys` of the of the attr that needs to be
|
||||
updated. That is all the keys from `s` and further will be
|
||||
rebound, since the `s` key was changed. In bound, the
|
||||
corresponding index is `s - 1`. If `s` is None, we start from
|
||||
1 (first attr).
|
||||
`fn`
|
||||
The function to be called args, `args` on bound callback.
|
||||
'''
|
||||
# first remove all the old bound functions from `s` and down.
|
||||
for f, k, fun, uid in bound[s:]:
|
||||
if fun is None:
|
||||
continue
|
||||
try:
|
||||
f.unbind_uid(k, uid)
|
||||
except ReferenceError:
|
||||
pass
|
||||
del bound[s:]
|
||||
|
||||
# find the first attr from which we need to start rebinding.
|
||||
f = getattr(*bound[-1][:2])
|
||||
if f is None:
|
||||
fn(args, None, None)
|
||||
return
|
||||
s += 1
|
||||
append = bound.append
|
||||
|
||||
# bind all attrs, except last to update_intermediates
|
||||
for val in keys[s:-1]:
|
||||
# if we need to dynamically rebind, bindm otherwise just
|
||||
# add the attr to the list
|
||||
if isinstance(f, (EventDispatcher, Observable)):
|
||||
prop = f.property(val, True)
|
||||
if prop is not None and getattr(prop, 'rebind', False):
|
||||
# fbind should not dispatch, otherwise
|
||||
# update_intermediates might be called in the middle
|
||||
# here messing things up
|
||||
uid = f.fbind(
|
||||
val, update_intermediates, base, keys, bound, s, fn, args)
|
||||
append([f.proxy_ref, val, update_intermediates, uid])
|
||||
else:
|
||||
append([f.proxy_ref, val, None, None])
|
||||
else:
|
||||
append([getattr(f, 'proxy_ref', f), val, None, None])
|
||||
|
||||
f = getattr(f, val, None)
|
||||
if f is None:
|
||||
break
|
||||
s += 1
|
||||
|
||||
# for the last attr we bind directly to the setting function,
|
||||
# because that attr sets the value of the rule.
|
||||
if isinstance(f, (EventDispatcher, Observable)):
|
||||
uid = f.fbind(keys[-1], fn, args)
|
||||
if uid:
|
||||
append([f.proxy_ref, keys[-1], fn, uid])
|
||||
# when we rebind we have to update the
|
||||
# rule with the most recent value, otherwise, the value might be wrong
|
||||
# and wouldn't be updated since we might not have tracked it before.
|
||||
# This only happens for a callback when rebind was True for the prop.
|
||||
fn(args, None, None)
|
||||
|
||||
|
||||
def create_handler(iself, element, key, value, rule, idmap, delayed=False):
|
||||
idmap = copy(idmap)
|
||||
idmap.update(global_idmap)
|
||||
idmap['self'] = iself.proxy_ref
|
||||
bound_list = _handlers[iself.uid][key]
|
||||
handler_append = bound_list.append
|
||||
|
||||
# we need a hash for when delayed, so we don't execute duplicate canvas
|
||||
# callbacks from the same handler during a sync op
|
||||
if delayed:
|
||||
fn = delayed_call_fn
|
||||
args = [element, key, value, rule, idmap, None] # see _delayed_start
|
||||
else:
|
||||
fn = call_fn
|
||||
args = (element, key, value, rule, idmap)
|
||||
|
||||
# bind every key.value
|
||||
if rule.watched_keys is not None:
|
||||
for keys in rule.watched_keys:
|
||||
base = idmap.get(keys[0])
|
||||
if base is None:
|
||||
continue
|
||||
f = base = getattr(base, 'proxy_ref', base)
|
||||
bound = []
|
||||
was_bound = False
|
||||
append = bound.append
|
||||
|
||||
# bind all attrs, except last to update_intermediates
|
||||
k = 1
|
||||
for val in keys[1:-1]:
|
||||
# if we need to dynamically rebind, bindm otherwise
|
||||
# just add the attr to the list
|
||||
if isinstance(f, (EventDispatcher, Observable)):
|
||||
prop = f.property(val, True)
|
||||
if prop is not None and getattr(prop, 'rebind', False):
|
||||
# fbind should not dispatch, otherwise
|
||||
# update_intermediates might be called in the middle
|
||||
# here messing things up
|
||||
uid = f.fbind(
|
||||
val, update_intermediates, base, keys, bound, k,
|
||||
fn, args)
|
||||
append([f.proxy_ref, val, update_intermediates, uid])
|
||||
was_bound = True
|
||||
else:
|
||||
append([f.proxy_ref, val, None, None])
|
||||
elif not isinstance(f, type):
|
||||
append([getattr(f, 'proxy_ref', f), val, None, None])
|
||||
else:
|
||||
append([f, val, None, None])
|
||||
f = getattr(f, val, None)
|
||||
if f is None:
|
||||
break
|
||||
k += 1
|
||||
|
||||
# for the last attr we bind directly to the setting
|
||||
# function, because that attr sets the value of the rule.
|
||||
if isinstance(f, (EventDispatcher, Observable)):
|
||||
uid = f.fbind(keys[-1], fn, args) # f is not None
|
||||
if uid:
|
||||
append([f.proxy_ref, keys[-1], fn, uid])
|
||||
was_bound = True
|
||||
if was_bound:
|
||||
handler_append(bound)
|
||||
|
||||
try:
|
||||
return eval(value, idmap), bound_list
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
raise BuilderException(rule.ctx, rule.line,
|
||||
'{}: {}'.format(e.__class__.__name__, e),
|
||||
cause=tb)
|
||||
|
||||
|
||||
class BuilderBase(object):
|
||||
'''The Builder is responsible for creating a :class:`Parser` for parsing a
|
||||
kv file, merging the results into its internal rules, templates, etc.
|
||||
|
||||
By default, :class:`Builder` is a global Kivy instance used in widgets
|
||||
that you can use to load other kv files in addition to the default ones.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
super(BuilderBase, self).__init__()
|
||||
self._match_cache = {}
|
||||
self._match_name_cache = {}
|
||||
self.files = []
|
||||
self.dynamic_classes = {}
|
||||
self.templates = {}
|
||||
self.rules = []
|
||||
self.rulectx = {}
|
||||
|
||||
@classmethod
|
||||
def create_from(cls, builder):
|
||||
"""Creates a instance of the class, and initializes to the state of
|
||||
``builder``.
|
||||
|
||||
:param builder: The builder to initialize from.
|
||||
:return: A new instance of this class.
|
||||
"""
|
||||
obj = cls()
|
||||
obj._match_cache = copy(builder._match_cache)
|
||||
obj._match_name_cache = copy(builder._match_name_cache)
|
||||
obj.files = copy(builder.files)
|
||||
obj.dynamic_classes = copy(builder.dynamic_classes)
|
||||
obj.templates = copy(builder.templates)
|
||||
obj.rules = list(builder.rules)
|
||||
assert not builder.rulectx
|
||||
obj.rulectx = dict(builder.rulectx)
|
||||
|
||||
return obj
|
||||
|
||||
def load_file(self, filename, encoding='utf8', **kwargs):
|
||||
'''Insert a file into the language builder and return the root widget
|
||||
(if defined) of the kv file.
|
||||
|
||||
:parameters:
|
||||
`rulesonly`: bool, defaults to False
|
||||
If True, the Builder will raise an exception if you have a root
|
||||
widget inside the definition.
|
||||
|
||||
`encoding`: File character encoding. Defaults to utf-8,
|
||||
'''
|
||||
|
||||
filename = resource_find(filename) or filename
|
||||
if __debug__:
|
||||
trace('Lang: load file %s, using %s encoding', filename, encoding)
|
||||
|
||||
kwargs['filename'] = filename
|
||||
with open(filename, 'r', encoding=encoding) as fd:
|
||||
data = fd.read()
|
||||
return self.load_string(data, **kwargs)
|
||||
|
||||
def unload_file(self, filename):
|
||||
'''Unload all rules associated with a previously imported file.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
.. warning::
|
||||
|
||||
This will not remove rules or templates already applied/used on
|
||||
current widgets. It will only effect the next widgets creation or
|
||||
template invocation.
|
||||
'''
|
||||
# remove rules and templates
|
||||
filename = resource_find(filename) or filename
|
||||
self.rules = [x for x in self.rules if x[1].ctx.filename != filename]
|
||||
self._clear_matchcache()
|
||||
templates = {}
|
||||
for x, y in self.templates.items():
|
||||
if y[2] != filename:
|
||||
templates[x] = y
|
||||
self.templates = templates
|
||||
|
||||
if filename in self.files:
|
||||
self.files.remove(filename)
|
||||
|
||||
# unregister all the dynamic classes
|
||||
Factory.unregister_from_filename(filename)
|
||||
|
||||
def load_string(self, string, **kwargs):
|
||||
'''Insert a string into the Language Builder and return the root widget
|
||||
(if defined) of the kv string.
|
||||
|
||||
:Parameters:
|
||||
`rulesonly`: bool, defaults to False
|
||||
If True, the Builder will raise an exception if you have a root
|
||||
widget inside the definition.
|
||||
`filename`: str, defaults to None
|
||||
If specified, the filename used to index the kv rules.
|
||||
|
||||
The filename parameter can be used to unload kv strings in the same way
|
||||
as you unload kv files. This can be achieved using pseudo file names
|
||||
e.g.::
|
||||
|
||||
Build.load_string("""
|
||||
<MyRule>:
|
||||
Label:
|
||||
text="Hello"
|
||||
""", filename="myrule.kv")
|
||||
|
||||
can be unloaded via::
|
||||
|
||||
Build.unload_file("myrule.kv")
|
||||
|
||||
'''
|
||||
|
||||
kwargs.setdefault('rulesonly', False)
|
||||
self._current_filename = fn = kwargs.get('filename', None)
|
||||
|
||||
# put a warning if a file is loaded multiple times
|
||||
if fn in self.files:
|
||||
Logger.warning(
|
||||
'Lang: The file {} is loaded multiples times, '
|
||||
'you might have unwanted behaviors.'.format(fn))
|
||||
|
||||
try:
|
||||
# parse the string
|
||||
parser = Parser(content=string, filename=fn)
|
||||
|
||||
# merge rules with our rules
|
||||
self.rules.extend(parser.rules)
|
||||
self._clear_matchcache()
|
||||
|
||||
# add the template found by the parser into ours
|
||||
for name, cls, template in parser.templates:
|
||||
self.templates[name] = (cls, template, fn)
|
||||
Factory.register(name,
|
||||
cls=partial(self.template, name),
|
||||
is_template=True, warn=True)
|
||||
|
||||
# register all the dynamic classes
|
||||
for name, baseclasses in parser.dynamic_classes.items():
|
||||
Factory.register(name, baseclasses=baseclasses, filename=fn,
|
||||
warn=True)
|
||||
|
||||
# create root object is exist
|
||||
if kwargs['rulesonly'] and parser.root:
|
||||
filename = kwargs.get('rulesonly', '<string>')
|
||||
raise Exception('The file <%s> contain also non-rules '
|
||||
'directives' % filename)
|
||||
|
||||
# save the loaded files only if there is a root without
|
||||
# template/dynamic classes
|
||||
if fn and (parser.templates or
|
||||
parser.dynamic_classes or parser.rules):
|
||||
self.files.append(fn)
|
||||
|
||||
if parser.root:
|
||||
widget = Factory.get(parser.root.name)(__no_builder=True)
|
||||
rule_children = []
|
||||
widget.apply_class_lang_rules(
|
||||
root=widget, rule_children=rule_children)
|
||||
self._apply_rule(
|
||||
widget, parser.root, parser.root,
|
||||
rule_children=rule_children)
|
||||
|
||||
for child in rule_children:
|
||||
child.dispatch('on_kv_post', widget)
|
||||
widget.dispatch('on_kv_post', widget)
|
||||
return widget
|
||||
finally:
|
||||
self._current_filename = None
|
||||
|
||||
def template(self, *args, **ctx):
|
||||
'''Create a specialized template using a specific context.
|
||||
|
||||
.. versionadded:: 1.0.5
|
||||
|
||||
With templates, you can construct custom widgets from a kv lang
|
||||
definition by giving them a context. Check :ref:`Template usage
|
||||
<template_usage>`.
|
||||
'''
|
||||
# Prevent naming clash with whatever the user might be putting into the
|
||||
# ctx as key.
|
||||
name = args[0]
|
||||
if name not in self.templates:
|
||||
raise Exception('Unknown <%s> template name' % name)
|
||||
baseclasses, rule, fn = self.templates[name]
|
||||
key = '%s|%s' % (name, baseclasses)
|
||||
cls = Cache.get('kv.lang', key)
|
||||
if cls is None:
|
||||
rootwidgets = []
|
||||
for basecls in baseclasses.split('+'):
|
||||
rootwidgets.append(Factory.get(basecls))
|
||||
cls = type(name, tuple(rootwidgets), {})
|
||||
Cache.append('kv.lang', key, cls)
|
||||
widget = cls()
|
||||
# in previous versions, ``ctx`` is passed as is as ``template_ctx``
|
||||
# preventing widgets in it from be collected by the GC. This was
|
||||
# especially relevant to AccordionItem's title_template.
|
||||
proxy_ctx = {k: get_proxy(v) for k, v in ctx.items()}
|
||||
self._apply_rule(widget, rule, rule, template_ctx=proxy_ctx)
|
||||
return widget
|
||||
|
||||
def apply_rules(
|
||||
self, widget, rule_name, ignored_consts=set(), rule_children=None,
|
||||
dispatch_kv_post=False):
|
||||
'''Search all the rules that match the name `rule_name`
|
||||
and apply them to `widget`.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:Parameters:
|
||||
|
||||
`widget`: :class:`~kivy.uix.widget.Widget`
|
||||
The widget to whom the matching rules should be applied to.
|
||||
`ignored_consts`: set
|
||||
A set or list type whose elements are property names for which
|
||||
constant KV rules (i.e. those that don't create bindings) of
|
||||
that widget will not be applied. This allows e.g. skipping
|
||||
constant rules that overwrite a value initialized in python.
|
||||
`rule_children`: list
|
||||
If not ``None``, it should be a list that will be populated
|
||||
with all the widgets created by the kv rules being applied.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
|
||||
`dispatch_kv_post`: bool
|
||||
Normally the class `Widget` dispatches the `on_kv_post` event
|
||||
to widgets created during kv rule application.
|
||||
But if the rules are manually applied by calling :meth:`apply`,
|
||||
that may not happen, so if this is `True`, we will dispatch the
|
||||
`on_kv_post` event where needed after applying the rules to
|
||||
`widget` (we won't dispatch it for `widget` itself).
|
||||
|
||||
Defaults to False.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
'''
|
||||
rules = self.match_rule_name(rule_name)
|
||||
if __debug__:
|
||||
trace('Lang: Found %d rules for %s' % (len(rules), rule_name))
|
||||
if not rules:
|
||||
return
|
||||
|
||||
if dispatch_kv_post:
|
||||
rule_children = rule_children if rule_children is not None else []
|
||||
for rule in rules:
|
||||
self._apply_rule(
|
||||
widget, rule, rule, ignored_consts=ignored_consts,
|
||||
rule_children=rule_children)
|
||||
if dispatch_kv_post:
|
||||
for w in rule_children:
|
||||
w.dispatch('on_kv_post', widget)
|
||||
|
||||
def apply(self, widget, ignored_consts=set(), rule_children=None,
|
||||
dispatch_kv_post=False):
|
||||
'''Search all the rules that match the widget and apply them.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`widget`: :class:`~kivy.uix.widget.Widget`
|
||||
The widget whose class rules should be applied to this widget.
|
||||
`ignored_consts`: set
|
||||
A set or list type whose elements are property names for which
|
||||
constant KV rules (i.e. those that don't create bindings) of
|
||||
that widget will not be applied. This allows e.g. skipping
|
||||
constant rules that overwrite a value initialized in python.
|
||||
`rule_children`: list
|
||||
If not ``None``, it should be a list that will be populated
|
||||
with all the widgets created by the kv rules being applied.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
|
||||
`dispatch_kv_post`: bool
|
||||
Normally the class `Widget` dispatches the `on_kv_post` event
|
||||
to widgets created during kv rule application.
|
||||
But if the rules are manually applied by calling :meth:`apply`,
|
||||
that may not happen, so if this is `True`, we will dispatch the
|
||||
`on_kv_post` event where needed after applying the rules to
|
||||
`widget` (we won't dispatch it for `widget` itself).
|
||||
|
||||
Defaults to False.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
'''
|
||||
rules = self.match(widget)
|
||||
if __debug__:
|
||||
trace('Lang: Found %d rules for %s' % (len(rules), widget))
|
||||
if not rules:
|
||||
return
|
||||
|
||||
if dispatch_kv_post:
|
||||
rule_children = rule_children if rule_children is not None else []
|
||||
for rule in rules:
|
||||
self._apply_rule(
|
||||
widget, rule, rule, ignored_consts=ignored_consts,
|
||||
rule_children=rule_children)
|
||||
if dispatch_kv_post:
|
||||
for w in rule_children:
|
||||
w.dispatch('on_kv_post', widget)
|
||||
|
||||
def _clear_matchcache(self):
|
||||
self._match_cache.clear()
|
||||
self._match_name_cache.clear()
|
||||
|
||||
def _apply_rule(self, widget, rule, rootrule, template_ctx=None,
|
||||
ignored_consts=set(), rule_children=None):
|
||||
# widget: the current instantiated widget
|
||||
# rule: the current rule
|
||||
# rootrule: the current root rule (for children of a rule)
|
||||
|
||||
# will collect reference to all the id in children
|
||||
assert(rule not in self.rulectx)
|
||||
self.rulectx[rule] = rctx = {
|
||||
'ids': {'root': widget.proxy_ref},
|
||||
'set': [], 'hdl': []}
|
||||
|
||||
# extract the context of the rootrule (not rule!)
|
||||
assert(rootrule in self.rulectx)
|
||||
rctx = self.rulectx[rootrule]
|
||||
|
||||
# if a template context is passed, put it as "ctx"
|
||||
if template_ctx is not None:
|
||||
rctx['ids']['ctx'] = QueryDict(template_ctx)
|
||||
|
||||
# if we got an id, put it in the root rule for a later global usage
|
||||
if rule.id:
|
||||
# use only the first word as `id` discard the rest.
|
||||
rule.id = rule.id.split('#', 1)[0].strip()
|
||||
rctx['ids'][rule.id] = widget.proxy_ref
|
||||
# set id name as a attribute for root widget so one can in python
|
||||
# code simply access root_widget.id_name
|
||||
_ids = dict(rctx['ids'])
|
||||
_root = _ids.pop('root')
|
||||
_new_ids = _root.ids
|
||||
for _key, _value in _ids.items():
|
||||
if _value == _root:
|
||||
# skip on self
|
||||
continue
|
||||
_new_ids[_key] = _value
|
||||
_root.ids = _new_ids
|
||||
|
||||
# first, ensure that the widget have all the properties used in
|
||||
# the rule if not, they will be created as ObjectProperty.
|
||||
rule.create_missing(widget)
|
||||
|
||||
# build the widget canvas
|
||||
if rule.canvas_before:
|
||||
with widget.canvas.before:
|
||||
self._build_canvas(widget.canvas.before, widget,
|
||||
rule.canvas_before, rootrule)
|
||||
if rule.canvas_root:
|
||||
with widget.canvas:
|
||||
self._build_canvas(widget.canvas, widget,
|
||||
rule.canvas_root, rootrule)
|
||||
if rule.canvas_after:
|
||||
with widget.canvas.after:
|
||||
self._build_canvas(widget.canvas.after, widget,
|
||||
rule.canvas_after, rootrule)
|
||||
|
||||
# create children tree
|
||||
Factory_get = Factory.get
|
||||
Factory_is_template = Factory.is_template
|
||||
for crule in rule.children:
|
||||
cname = crule.name
|
||||
|
||||
if cname in ('canvas', 'canvas.before', 'canvas.after'):
|
||||
raise ParserException(
|
||||
crule.ctx, crule.line,
|
||||
'Canvas instructions added in kv must '
|
||||
'be declared before child widgets.')
|
||||
|
||||
# depending if the child rule is a template or not, we are not
|
||||
# having the same approach
|
||||
cls = Factory_get(cname)
|
||||
|
||||
if Factory_is_template(cname):
|
||||
# we got a template, so extract all the properties and
|
||||
# handlers, and push them in a "ctx" dictionary.
|
||||
ctx = {}
|
||||
idmap = copy(global_idmap)
|
||||
idmap.update({'root': rctx['ids']['root']})
|
||||
if 'ctx' in rctx['ids']:
|
||||
idmap.update({'ctx': rctx['ids']['ctx']})
|
||||
try:
|
||||
for prule in crule.properties.values():
|
||||
value = prule.co_value
|
||||
if type(value) is CodeType:
|
||||
value = eval(value, idmap)
|
||||
ctx[prule.name] = value
|
||||
for prule in crule.handlers:
|
||||
value = eval(prule.value, idmap)
|
||||
ctx[prule.name] = value
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
raise BuilderException(
|
||||
prule.ctx, prule.line,
|
||||
'{}: {}'.format(e.__class__.__name__, e), cause=tb)
|
||||
|
||||
# create the template with an explicit ctx
|
||||
child = cls(**ctx)
|
||||
widget.add_widget(child)
|
||||
|
||||
# reference it on our root rule context
|
||||
if crule.id:
|
||||
rctx['ids'][crule.id] = child
|
||||
|
||||
else:
|
||||
# we got a "normal" rule, construct it manually
|
||||
# we can't construct it without __no_builder=True, because the
|
||||
# previous implementation was doing the add_widget() before
|
||||
# apply(), and so, we could use "self.parent".
|
||||
child = cls(__no_builder=True)
|
||||
widget.add_widget(child)
|
||||
child.apply_class_lang_rules(
|
||||
root=rctx['ids']['root'], rule_children=rule_children)
|
||||
self._apply_rule(
|
||||
child, crule, rootrule, rule_children=rule_children)
|
||||
|
||||
if rule_children is not None:
|
||||
rule_children.append(child)
|
||||
|
||||
# append the properties and handlers to our final resolution task
|
||||
if rule.properties:
|
||||
rctx['set'].append((widget.proxy_ref,
|
||||
list(rule.properties.values())))
|
||||
for key, crule in rule.properties.items():
|
||||
# clear previously applied rules if asked
|
||||
if crule.ignore_prev:
|
||||
Builder.unbind_property(widget, key)
|
||||
if rule.handlers:
|
||||
rctx['hdl'].append((widget.proxy_ref, rule.handlers))
|
||||
|
||||
# if we are applying another rule that the root one, then it's done for
|
||||
# us!
|
||||
if rootrule is not rule:
|
||||
del self.rulectx[rule]
|
||||
return
|
||||
|
||||
# normally, we can apply a list of properties with a proper context
|
||||
try:
|
||||
rule = None
|
||||
for widget_set, rules in reversed(rctx['set']):
|
||||
for rule in rules:
|
||||
assert(isinstance(rule, ParserRuleProperty))
|
||||
key = rule.name
|
||||
value = rule.co_value
|
||||
if type(value) is CodeType:
|
||||
value, bound = create_handler(
|
||||
widget_set, widget_set, key, value, rule,
|
||||
rctx['ids'])
|
||||
# if there's a rule
|
||||
if (widget_set != widget or bound or
|
||||
key not in ignored_consts):
|
||||
setattr(widget_set, key, value)
|
||||
else:
|
||||
if (widget_set != widget or
|
||||
key not in ignored_consts):
|
||||
setattr(widget_set, key, value)
|
||||
|
||||
except Exception as e:
|
||||
if rule is not None:
|
||||
tb = sys.exc_info()[2]
|
||||
raise BuilderException(rule.ctx, rule.line,
|
||||
'{}: {}'.format(e.__class__.__name__,
|
||||
e), cause=tb)
|
||||
raise e
|
||||
|
||||
# build handlers
|
||||
try:
|
||||
crule = None
|
||||
for widget_set, rules in rctx['hdl']:
|
||||
for crule in rules:
|
||||
assert(isinstance(crule, ParserRuleProperty))
|
||||
assert(crule.name.startswith('on_'))
|
||||
key = crule.name
|
||||
if not widget_set.is_event_type(key):
|
||||
key = key[3:]
|
||||
idmap = copy(global_idmap)
|
||||
idmap.update(rctx['ids'])
|
||||
idmap['self'] = widget_set.proxy_ref
|
||||
if not widget_set.fbind(key, custom_callback, crule,
|
||||
idmap):
|
||||
raise AttributeError(key)
|
||||
# hack for on_parent
|
||||
if crule.name == 'on_parent':
|
||||
Factory.Widget.parent.dispatch(widget_set.__self__)
|
||||
except Exception as e:
|
||||
if crule is not None:
|
||||
tb = sys.exc_info()[2]
|
||||
raise BuilderException(
|
||||
crule.ctx, crule.line,
|
||||
'{}: {}'.format(e.__class__.__name__, e), cause=tb)
|
||||
raise e
|
||||
|
||||
# rule finished, forget it
|
||||
del self.rulectx[rootrule]
|
||||
|
||||
def match(self, widget):
|
||||
'''Return a list of :class:`ParserRule` objects matching the widget.
|
||||
'''
|
||||
cache = self._match_cache
|
||||
k = (widget.__class__, tuple(widget.cls))
|
||||
if k in cache:
|
||||
return cache[k]
|
||||
rules = []
|
||||
for selector, rule in self.rules:
|
||||
if selector.match(widget):
|
||||
if rule.avoid_previous_rules:
|
||||
del rules[:]
|
||||
rules.append(rule)
|
||||
cache[k] = rules
|
||||
return rules
|
||||
|
||||
def match_rule_name(self, rule_name):
|
||||
'''Return a list of :class:`ParserRule` objects matching the widget.
|
||||
'''
|
||||
cache = self._match_name_cache
|
||||
rule_name = str(rule_name)
|
||||
k = rule_name.lower()
|
||||
if k in cache:
|
||||
return cache[k]
|
||||
rules = []
|
||||
for selector, rule in self.rules:
|
||||
if selector.match_rule_name(rule_name):
|
||||
if rule.avoid_previous_rules:
|
||||
del rules[:]
|
||||
rules.append(rule)
|
||||
cache[k] = rules
|
||||
return rules
|
||||
|
||||
def sync(self):
|
||||
'''Execute all the waiting operations, such as the execution of all the
|
||||
expressions related to the canvas.
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
'''
|
||||
global _delayed_start
|
||||
next_args = _delayed_start
|
||||
if next_args is None:
|
||||
return
|
||||
|
||||
while next_args is not StopIteration:
|
||||
# is this try/except still needed? yes, in case widget died in this
|
||||
# frame after the call was scheduled
|
||||
try:
|
||||
call_fn(next_args[:-1], None, None)
|
||||
except ReferenceError:
|
||||
pass
|
||||
args = next_args
|
||||
next_args = args[-1]
|
||||
args[-1] = None
|
||||
_delayed_start = None
|
||||
|
||||
def unbind_widget(self, uid):
|
||||
'''Unbind all the handlers created by the KV rules of the
|
||||
widget. The :attr:`kivy.uix.widget.Widget.uid` is passed here
|
||||
instead of the widget itself, because Builder is using it in the
|
||||
widget destructor.
|
||||
|
||||
This effectively clears all the KV rules associated with this widget.
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> w = Builder.load_string(\'''
|
||||
... Widget:
|
||||
... height: self.width / 2. if self.disabled else self.width
|
||||
... x: self.y + 50
|
||||
... \''')
|
||||
>>> w.size
|
||||
[100, 100]
|
||||
>>> w.pos
|
||||
[50, 0]
|
||||
>>> w.width = 500
|
||||
>>> w.size
|
||||
[500, 500]
|
||||
>>> Builder.unbind_widget(w.uid)
|
||||
>>> w.width = 222
|
||||
>>> w.y = 500
|
||||
>>> w.size
|
||||
[222, 500]
|
||||
>>> w.pos
|
||||
[50, 500]
|
||||
|
||||
.. versionadded:: 1.7.2
|
||||
'''
|
||||
if uid not in _handlers:
|
||||
return
|
||||
for prop_callbacks in _handlers[uid].values():
|
||||
for callbacks in prop_callbacks:
|
||||
for f, k, fn, bound_uid in callbacks:
|
||||
if fn is None: # it's not a kivy prop.
|
||||
continue
|
||||
try:
|
||||
f.unbind_uid(k, bound_uid)
|
||||
except ReferenceError:
|
||||
# proxy widget is already gone, that's cool :)
|
||||
pass
|
||||
del _handlers[uid]
|
||||
|
||||
def unbind_property(self, widget, name):
|
||||
'''Unbind the handlers created by all the rules of the widget that set
|
||||
the name.
|
||||
|
||||
This effectively clears all the rules of widget that take the form::
|
||||
|
||||
name: rule
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> w = Builder.load_string(\'''
|
||||
... Widget:
|
||||
... height: self.width / 2. if self.disabled else self.width
|
||||
... x: self.y + 50
|
||||
... \''')
|
||||
>>> w.size
|
||||
[100, 100]
|
||||
>>> w.pos
|
||||
[50, 0]
|
||||
>>> w.width = 500
|
||||
>>> w.size
|
||||
[500, 500]
|
||||
>>> Builder.unbind_property(w, 'height')
|
||||
>>> w.width = 222
|
||||
>>> w.size
|
||||
[222, 500]
|
||||
>>> w.y = 500
|
||||
>>> w.pos
|
||||
[550, 500]
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
uid = widget.uid
|
||||
if uid not in _handlers:
|
||||
return
|
||||
|
||||
prop_handlers = _handlers[uid]
|
||||
if name not in prop_handlers:
|
||||
return
|
||||
|
||||
for callbacks in prop_handlers[name]:
|
||||
for f, k, fn, bound_uid in callbacks:
|
||||
if fn is None: # it's not a kivy prop.
|
||||
continue
|
||||
try:
|
||||
f.unbind_uid(k, bound_uid)
|
||||
except ReferenceError:
|
||||
# proxy widget is already gone, that's cool :)
|
||||
pass
|
||||
del prop_handlers[name]
|
||||
if not prop_handlers:
|
||||
del _handlers[uid]
|
||||
|
||||
def _build_canvas(self, canvas, widget, rule, rootrule):
|
||||
global Instruction
|
||||
if Instruction is None:
|
||||
Instruction = Factory.get('Instruction')
|
||||
idmap = copy(self.rulectx[rootrule]['ids'])
|
||||
for crule in rule.children:
|
||||
name = crule.name
|
||||
if name == 'Clear':
|
||||
canvas.clear()
|
||||
continue
|
||||
instr = Factory.get(name)()
|
||||
if not isinstance(instr, Instruction):
|
||||
raise BuilderException(
|
||||
crule.ctx, crule.line,
|
||||
'You can add only graphics Instruction in canvas.')
|
||||
try:
|
||||
for prule in crule.properties.values():
|
||||
key = prule.name
|
||||
value = prule.co_value
|
||||
if type(value) is CodeType:
|
||||
value, _ = create_handler(
|
||||
widget, instr.proxy_ref,
|
||||
key, value, prule, idmap, True)
|
||||
setattr(instr, key, value)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
raise BuilderException(
|
||||
prule.ctx, prule.line,
|
||||
'{}: {}'.format(e.__class__.__name__, e), cause=tb)
|
||||
|
||||
|
||||
#: Main instance of a :class:`BuilderBase`.
|
||||
Builder: BuilderBase = register_context('Builder', BuilderBase)
|
||||
Builder.load_file(join(kivy_data_dir, 'style.kv'), rulesonly=True)
|
||||
|
||||
if 'KIVY_PROFILE_LANG' in environ:
|
||||
import atexit
|
||||
from html import escape
|
||||
|
||||
def match_rule(fn, index, rule):
|
||||
if rule.ctx.filename != fn:
|
||||
return
|
||||
for prop, prp in rule.properties.items():
|
||||
if prp.line != index:
|
||||
continue
|
||||
yield prp
|
||||
for child in rule.children:
|
||||
for r in match_rule(fn, index, child):
|
||||
yield r
|
||||
if rule.canvas_root:
|
||||
for r in match_rule(fn, index, rule.canvas_root):
|
||||
yield r
|
||||
if rule.canvas_before:
|
||||
for r in match_rule(fn, index, rule.canvas_before):
|
||||
yield r
|
||||
if rule.canvas_after:
|
||||
for r in match_rule(fn, index, rule.canvas_after):
|
||||
yield r
|
||||
|
||||
def dump_builder_stats():
|
||||
html = [
|
||||
'<!doctype html>'
|
||||
'<html><body>',
|
||||
'<style type="text/css">\n',
|
||||
'pre { margin: 0; }\n',
|
||||
'</style>']
|
||||
files = {x[1].ctx.filename for x in Builder.rules}
|
||||
for fn in files:
|
||||
try:
|
||||
with open(fn) as f:
|
||||
lines = f.readlines()
|
||||
except (IOError, TypeError) as e:
|
||||
continue
|
||||
html += ['<h2>', fn, '</h2>', '<table>']
|
||||
count = 0
|
||||
for index, line in enumerate(lines):
|
||||
line = line.rstrip()
|
||||
line = escape(line)
|
||||
matched_prp = []
|
||||
for psn, rule in Builder.rules:
|
||||
matched_prp.extend(match_rule(fn, index, rule))
|
||||
|
||||
count = sum({x.count for x in matched_prp})
|
||||
|
||||
color = (255, 155, 155) if count else (255, 255, 255)
|
||||
html += ['<tr style="background-color: rgb{}">'.format(color),
|
||||
'<td>', str(index + 1), '</td>',
|
||||
'<td>', str(count), '</td>',
|
||||
'<td><pre>', line, '</pre></td>',
|
||||
'</tr>']
|
||||
html += ['</table>']
|
||||
html += ['</body></html>']
|
||||
with open('builder_stats.html', 'w', encoding='utf-8') as fd:
|
||||
fd.write(''.join(html))
|
||||
|
||||
print('Profiling written at builder_stats.html')
|
||||
|
||||
atexit.register(dump_builder_stats)
|
||||
817
kivy/lang/parser.py
Normal file
817
kivy/lang/parser.py
Normal file
@@ -0,0 +1,817 @@
|
||||
'''
|
||||
Parser
|
||||
======
|
||||
|
||||
Class used for the parsing of .kv files into rules.
|
||||
'''
|
||||
import os
|
||||
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import ast
|
||||
import importlib
|
||||
from re import sub, findall
|
||||
from types import CodeType
|
||||
from functools import partial
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import kivy.lang.builder # imported as absolute to avoid circular import
|
||||
from kivy.logger import Logger
|
||||
from kivy.cache import Cache
|
||||
from kivy import require
|
||||
from kivy.resources import resource_find
|
||||
from kivy.utils import rgba
|
||||
import kivy.metrics as Metrics
|
||||
|
||||
__all__ = ('Parser', 'ParserException')
|
||||
|
||||
|
||||
trace = Logger.trace
|
||||
global_idmap = {}
|
||||
|
||||
# register cache for creating new classtype (template)
|
||||
Cache.register('kv.lang')
|
||||
|
||||
# all previously included files
|
||||
__KV_INCLUDES__ = []
|
||||
|
||||
# precompile regexp expression
|
||||
str_re = (
|
||||
"(?:'''.*?''')|"
|
||||
"(?:(?:(?<!')|''')'(?:[^']|\\\\')+?'(?:(?!')|'''))|"
|
||||
'(?:""".*?""")|'
|
||||
'(?:(?:(?<!")|""")"(?:[^"]|\\\\")+?"(?:(?!")|"""))'
|
||||
)
|
||||
|
||||
lang_str = re.compile(f"({str_re})", re.DOTALL)
|
||||
lang_fstr = re.compile(f"([fF](?:{str_re}))", re.DOTALL)
|
||||
|
||||
lang_key = re.compile('([a-zA-Z_]+)')
|
||||
lang_keyvalue = re.compile(r'([a-zA-Z_][a-zA-Z0-9_.]*\.[a-zA-Z0-9_.]+)')
|
||||
lang_tr = re.compile(r'(_\()')
|
||||
lang_cls_split_pat = re.compile(', *')
|
||||
|
||||
# all the widget handlers, used to correctly unbind all the callbacks then the
|
||||
# widget is deleted
|
||||
_handlers = defaultdict(partial(defaultdict, list))
|
||||
|
||||
|
||||
class ProxyApp(object):
|
||||
# proxy app object
|
||||
# taken from http://code.activestate.com/recipes/496741-object-proxying/
|
||||
|
||||
__slots__ = ['_obj']
|
||||
|
||||
def __init__(self):
|
||||
object.__init__(self)
|
||||
object.__setattr__(self, '_obj', None)
|
||||
|
||||
def _ensure_app(self):
|
||||
app = object.__getattribute__(self, '_obj')
|
||||
if app is None:
|
||||
from kivy.app import App
|
||||
app = App.get_running_app()
|
||||
object.__setattr__(self, '_obj', app)
|
||||
# Clear cached application instance, when it stops
|
||||
app.bind(on_stop=lambda instance:
|
||||
object.__setattr__(self, '_obj', None))
|
||||
return app
|
||||
|
||||
def __getattribute__(self, name):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
return getattr(object.__getattribute__(self, '_obj'), name)
|
||||
|
||||
def __delattr__(self, name):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
delattr(object.__getattribute__(self, '_obj'), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
setattr(object.__getattribute__(self, '_obj'), name, value)
|
||||
|
||||
def __bool__(self):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
return bool(object.__getattribute__(self, '_obj'))
|
||||
|
||||
def __str__(self):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
return str(object.__getattribute__(self, '_obj'))
|
||||
|
||||
def __repr__(self):
|
||||
object.__getattribute__(self, '_ensure_app')()
|
||||
return repr(object.__getattribute__(self, '_obj'))
|
||||
|
||||
|
||||
global_idmap['app'] = ProxyApp()
|
||||
global_idmap['pt'] = Metrics.pt
|
||||
global_idmap['inch'] = Metrics.inch
|
||||
global_idmap['cm'] = Metrics.cm
|
||||
global_idmap['mm'] = Metrics.mm
|
||||
global_idmap['dp'] = Metrics.dp
|
||||
global_idmap['sp'] = Metrics.sp
|
||||
global_idmap['rgba'] = rgba
|
||||
|
||||
|
||||
class ParserException(Exception):
|
||||
'''Exception raised when something wrong happened in a kv file.
|
||||
'''
|
||||
|
||||
def __init__(self, context, line, message, cause=None):
|
||||
self.filename = context.filename or '<inline>'
|
||||
self.line = line
|
||||
sourcecode = context.sourcecode
|
||||
sc_start = max(0, line - 2)
|
||||
sc_stop = min(len(sourcecode), line + 3)
|
||||
sc = ['...']
|
||||
for x in range(sc_start, sc_stop):
|
||||
if x == line:
|
||||
sc += ['>> %4d:%s' % (line + 1, sourcecode[line][1])]
|
||||
else:
|
||||
sc += [' %4d:%s' % (x + 1, sourcecode[x][1])]
|
||||
sc += ['...']
|
||||
sc = '\n'.join(sc)
|
||||
|
||||
message = 'Parser: File "%s", line %d:\n%s\n%s' % (
|
||||
self.filename, self.line + 1, sc, message)
|
||||
if cause:
|
||||
message += '\n' + ''.join(traceback.format_tb(cause))
|
||||
|
||||
super(ParserException, self).__init__(message)
|
||||
|
||||
|
||||
class ParserRuleProperty(object):
|
||||
'''Represent a property inside a rule.
|
||||
'''
|
||||
|
||||
__slots__ = ('ctx', 'line', 'name', 'value', 'co_value',
|
||||
'watched_keys', 'mode', 'count', 'ignore_prev')
|
||||
|
||||
def __init__(self, ctx, line, name, value, ignore_prev=False):
|
||||
super(ParserRuleProperty, self).__init__()
|
||||
#: Associated parser
|
||||
self.ctx = ctx
|
||||
#: Line of the rule
|
||||
self.line = line
|
||||
#: Name of the property
|
||||
self.name = name
|
||||
#: Value of the property
|
||||
self.value = value
|
||||
#: Compiled value
|
||||
self.co_value = None
|
||||
#: Compilation mode
|
||||
self.mode = None
|
||||
#: Watched keys
|
||||
self.watched_keys = None
|
||||
#: Stats
|
||||
self.count = 0
|
||||
#: whether previous rules targeting name should be cleared
|
||||
self.ignore_prev = ignore_prev
|
||||
|
||||
def precompile(self):
|
||||
name = self.name
|
||||
value = self.value
|
||||
|
||||
# first, remove all the string from the value
|
||||
tmp = sub(lang_str, '', self.value)
|
||||
|
||||
# detecting how to handle the value according to the key name
|
||||
mode = self.mode
|
||||
if self.mode is None:
|
||||
self.mode = mode = 'exec' if name[:3] == 'on_' else 'eval'
|
||||
if mode == 'eval':
|
||||
# if we don't detect any string/key in it, we can eval and give the
|
||||
# result
|
||||
if re.search(lang_key, tmp) is None:
|
||||
value = '\n' * self.line + value
|
||||
self.co_value = eval(
|
||||
compile(value, self.ctx.filename or '<string>', 'eval')
|
||||
)
|
||||
return
|
||||
|
||||
# ok, we can compile.
|
||||
value = '\n' * self.line + value
|
||||
self.co_value = compile(value, self.ctx.filename or '<string>', mode)
|
||||
|
||||
# for exec mode, we don't need to watch any keys.
|
||||
if mode == 'exec':
|
||||
return
|
||||
|
||||
# now, detect obj.prop
|
||||
# find all the fstrings in the value
|
||||
fstrings = lang_fstr.findall(value)
|
||||
wk = set()
|
||||
for s in fstrings:
|
||||
expression = ast.parse(s)
|
||||
wk |= set(self.get_names_from_expression(expression.body[0].value))
|
||||
|
||||
# first, remove all the string from the value
|
||||
tmp = sub(lang_str, '', value)
|
||||
idx = tmp.find('#')
|
||||
if idx != -1:
|
||||
tmp = tmp[:idx]
|
||||
# detect key.value inside value, and split them
|
||||
wk |= set(findall(lang_keyvalue, tmp))
|
||||
if wk:
|
||||
self.watched_keys = [x.split('.') for x in wk]
|
||||
if findall(lang_tr, tmp):
|
||||
if self.watched_keys:
|
||||
self.watched_keys += [['_']]
|
||||
else:
|
||||
self.watched_keys = [['_']]
|
||||
|
||||
@classmethod
|
||||
def get_names_from_expression(cls, node):
|
||||
"""
|
||||
Look for all the symbols used in an ast node.
|
||||
"""
|
||||
if isinstance(node, ast.Name):
|
||||
yield node.id
|
||||
|
||||
if isinstance(node, (ast.JoinedStr, ast.BoolOp)):
|
||||
for n in node.values:
|
||||
if isinstance(n, ast.Str):
|
||||
# NOTE: required for python3.6
|
||||
yield from cls.get_names_from_expression(n.s)
|
||||
else:
|
||||
yield from cls.get_names_from_expression(n.value)
|
||||
|
||||
if isinstance(node, ast.BinOp):
|
||||
yield from cls.get_names_from_expression(node.right)
|
||||
yield from cls.get_names_from_expression(node.left)
|
||||
|
||||
if isinstance(node, ast.IfExp):
|
||||
yield from cls.get_names_from_expression(node.test)
|
||||
yield from cls.get_names_from_expression(node.body)
|
||||
yield from cls.get_names_from_expression(node.orelse)
|
||||
|
||||
if isinstance(node, ast.Subscript):
|
||||
yield from cls.get_names_from_expression(node.value)
|
||||
yield from cls.get_names_from_expression(node.slice)
|
||||
|
||||
if isinstance(node, ast.Slice):
|
||||
yield from cls.get_names_from_expression(node.lower)
|
||||
yield from cls.get_names_from_expression(node.upper)
|
||||
yield from cls.get_names_from_expression(node.step)
|
||||
|
||||
if isinstance(
|
||||
node,
|
||||
(ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp)
|
||||
):
|
||||
for g in node.generators:
|
||||
yield from cls.get_names_from_expression(g.iter)
|
||||
|
||||
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
||||
for elt in node.elts:
|
||||
yield from cls.get_names_from_expression(elt)
|
||||
|
||||
if isinstance(node, ast.Dict):
|
||||
for val in node.values:
|
||||
yield from cls.get_names_from_expression(val)
|
||||
|
||||
if isinstance(node, ast.UnaryOp):
|
||||
yield from cls.get_names_from_expression(node.operand)
|
||||
|
||||
if isinstance(node, ast.comprehension):
|
||||
yield from cls.get_names_from_expression(node.iter.value)
|
||||
|
||||
if isinstance(node, ast.Attribute):
|
||||
if isinstance(node.value, ast.Name):
|
||||
yield f'{node.value.id}.{node.attr}'
|
||||
|
||||
if isinstance(node, ast.Call):
|
||||
yield from cls.get_names_from_expression(node.func)
|
||||
|
||||
for arg in node.args:
|
||||
yield from cls.get_names_from_expression(arg)
|
||||
for keyword in node.keywords:
|
||||
yield from cls.get_names_from_expression(keyword.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<ParserRuleProperty name=%r filename=%s:%d ' \
|
||||
'value=%r watched_keys=%r>' % (
|
||||
self.name, self.ctx.filename, self.line + 1,
|
||||
self.value, self.watched_keys)
|
||||
|
||||
|
||||
class ParserRule(object):
|
||||
'''Represents a rule, in terms of the Kivy internal language.
|
||||
'''
|
||||
|
||||
__slots__ = ('ctx', 'line', 'name', 'children', 'id', 'properties',
|
||||
'canvas_before', 'canvas_root', 'canvas_after',
|
||||
'handlers', 'level', 'cache_marked', 'avoid_previous_rules')
|
||||
|
||||
def __init__(self, ctx, line, name, level):
|
||||
super(ParserRule, self).__init__()
|
||||
#: Level of the rule in the kv
|
||||
self.level = level
|
||||
#: Associated parser
|
||||
self.ctx = ctx
|
||||
#: Line of the rule
|
||||
self.line = line
|
||||
#: Name of the rule
|
||||
self.name = name
|
||||
#: List of children to create
|
||||
self.children = []
|
||||
#: Id given to the rule
|
||||
self.id = None
|
||||
#: Properties associated to the rule
|
||||
self.properties = OrderedDict()
|
||||
#: Canvas normal
|
||||
self.canvas_root = None
|
||||
#: Canvas before
|
||||
self.canvas_before = None
|
||||
#: Canvas after
|
||||
self.canvas_after = None
|
||||
#: Handlers associated to the rule
|
||||
self.handlers = []
|
||||
#: Properties cache list: mark which class have already been checked
|
||||
self.cache_marked = []
|
||||
#: Indicate if any previous rules should be avoided.
|
||||
self.avoid_previous_rules = False
|
||||
|
||||
if level == 0:
|
||||
self._detect_selectors()
|
||||
else:
|
||||
self._forbid_selectors()
|
||||
|
||||
def precompile(self):
|
||||
for x in self.properties.values():
|
||||
x.precompile()
|
||||
for x in self.handlers:
|
||||
x.precompile()
|
||||
for x in self.children:
|
||||
x.precompile()
|
||||
if self.canvas_before:
|
||||
self.canvas_before.precompile()
|
||||
if self.canvas_root:
|
||||
self.canvas_root.precompile()
|
||||
if self.canvas_after:
|
||||
self.canvas_after.precompile()
|
||||
|
||||
def create_missing(self, widget):
|
||||
# check first if the widget class already been processed by this rule
|
||||
cls = widget.__class__
|
||||
if cls in self.cache_marked:
|
||||
return
|
||||
self.cache_marked.append(cls)
|
||||
for name in self.properties:
|
||||
if hasattr(widget, name):
|
||||
continue
|
||||
value = self.properties[name].co_value
|
||||
if type(value) is CodeType:
|
||||
value = None
|
||||
widget.create_property(name, value, default_value=False)
|
||||
|
||||
def _forbid_selectors(self):
|
||||
c = self.name[0]
|
||||
if c == '<' or c == '[':
|
||||
raise ParserException(
|
||||
self.ctx, self.line,
|
||||
'Selectors rules are allowed only at the first level')
|
||||
|
||||
def _detect_selectors(self):
|
||||
c = self.name[0]
|
||||
if c == '<':
|
||||
self._build_rule()
|
||||
elif c == '[':
|
||||
self._build_template()
|
||||
else:
|
||||
if self.ctx.root is not None:
|
||||
raise ParserException(
|
||||
self.ctx, self.line,
|
||||
'Only one root object is allowed by .kv')
|
||||
self.ctx.root = self
|
||||
|
||||
def _build_rule(self):
|
||||
name = self.name
|
||||
if __debug__:
|
||||
trace('Builder: build rule for %s' % name)
|
||||
if name[0] != '<' or name[-1] != '>':
|
||||
raise ParserException(self.ctx, self.line,
|
||||
'Invalid rule (must be inside <>)')
|
||||
|
||||
# if the very first name start with a -, avoid previous rules
|
||||
name = name[1:-1]
|
||||
if name[:1] == '-':
|
||||
self.avoid_previous_rules = True
|
||||
name = name[1:]
|
||||
|
||||
for rule in re.split(lang_cls_split_pat, name):
|
||||
crule = None
|
||||
|
||||
if not rule:
|
||||
raise ParserException(self.ctx, self.line,
|
||||
'Empty rule detected')
|
||||
|
||||
if '@' in rule:
|
||||
# new class creation ?
|
||||
# ensure the name is correctly written
|
||||
rule, baseclasses = rule.split('@', 1)
|
||||
if not re.match(lang_key, rule):
|
||||
raise ParserException(self.ctx, self.line,
|
||||
'Invalid dynamic class name')
|
||||
|
||||
# save the name in the dynamic classes dict.
|
||||
self.ctx.dynamic_classes[rule] = baseclasses
|
||||
crule = ParserSelectorName(rule)
|
||||
|
||||
else:
|
||||
# classical selectors.
|
||||
|
||||
if rule[0] == '.':
|
||||
crule = ParserSelectorClass(rule[1:])
|
||||
else:
|
||||
crule = ParserSelectorName(rule)
|
||||
|
||||
self.ctx.rules.append((crule, self))
|
||||
|
||||
def _build_template(self):
|
||||
name = self.name
|
||||
exception = ParserException(
|
||||
self.ctx, self.line,
|
||||
'Deprecated Kivy lang template syntax used "{}". Templates will '
|
||||
'be removed in a future version'.format(name))
|
||||
if name not in ('[FileListEntry@FloatLayout+TreeViewNode]',
|
||||
'[FileIconEntry@Widget]',
|
||||
'[AccordionItemTitle@Label]'):
|
||||
Logger.warning(exception)
|
||||
|
||||
if __debug__:
|
||||
trace('Builder: build template for %s' % name)
|
||||
if name[0] != '[' or name[-1] != ']':
|
||||
raise ParserException(self.ctx, self.line,
|
||||
'Invalid template (must be inside [])')
|
||||
item_content = name[1:-1]
|
||||
if '@' not in item_content:
|
||||
raise ParserException(self.ctx, self.line,
|
||||
'Invalid template name (missing @)')
|
||||
template_name, template_root_cls = item_content.split('@')
|
||||
self.ctx.templates.append((template_name, template_root_cls, self))
|
||||
|
||||
def __repr__(self):
|
||||
return '<ParserRule name=%r>' % (self.name, )
|
||||
|
||||
|
||||
class Parser(object):
|
||||
'''Create a Parser object to parse a Kivy language file or Kivy content.
|
||||
'''
|
||||
|
||||
PROP_ALLOWED = ('canvas.before', 'canvas.after')
|
||||
CLASS_RANGE = list(range(ord('A'), ord('Z') + 1))
|
||||
PROP_RANGE = (
|
||||
list(range(ord('A'), ord('Z') + 1)) +
|
||||
list(range(ord('a'), ord('z') + 1)) +
|
||||
list(range(ord('0'), ord('9') + 1)) + [ord('_')])
|
||||
|
||||
__slots__ = ('rules', 'templates', 'root', 'sourcecode',
|
||||
'directives', 'filename', 'dynamic_classes')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Parser, self).__init__()
|
||||
self.rules = []
|
||||
self.templates = []
|
||||
self.root = None
|
||||
self.sourcecode = []
|
||||
self.directives = []
|
||||
self.dynamic_classes = {}
|
||||
self.filename = kwargs.get('filename', None)
|
||||
content = kwargs.get('content', None)
|
||||
if content is None:
|
||||
raise ValueError('No content passed')
|
||||
self.parse(content)
|
||||
|
||||
def execute_directives(self):
|
||||
global __KV_INCLUDES__
|
||||
for ln, cmd in self.directives:
|
||||
cmd = cmd.strip()
|
||||
if __debug__:
|
||||
trace('Parser: got directive <%s>' % cmd)
|
||||
if cmd[:5] == 'kivy ':
|
||||
version = cmd[5:].strip()
|
||||
if len(version.split('.')) == 2:
|
||||
version += '.0'
|
||||
require(version)
|
||||
elif cmd[:4] == 'set ':
|
||||
try:
|
||||
name, value = cmd[4:].strip().split(' ', 1)
|
||||
except:
|
||||
Logger.exception('')
|
||||
raise ParserException(self, ln, 'Invalid directive syntax')
|
||||
try:
|
||||
value = eval(value, global_idmap)
|
||||
except:
|
||||
Logger.exception('')
|
||||
raise ParserException(self, ln, 'Invalid value')
|
||||
global_idmap[name] = value
|
||||
elif cmd[:8] == 'include ':
|
||||
ref = cmd[8:].strip()
|
||||
force_load = False
|
||||
|
||||
if ref[:6] == 'force ':
|
||||
ref = ref[6:].strip()
|
||||
force_load = True
|
||||
|
||||
# if #:include [force] "path with quotes around"
|
||||
if ref[0] == ref[-1] and ref[0] in ('"', "'"):
|
||||
c = ref[:3].count(ref[0])
|
||||
ref = ref[c:-c] if c != 2 else ref
|
||||
|
||||
if ref[-3:] != '.kv':
|
||||
Logger.warning('Lang: {0} does not have a valid Kivy'
|
||||
'Language extension (.kv)'.format(ref))
|
||||
break
|
||||
if ref in __KV_INCLUDES__:
|
||||
if not os.path.isfile(resource_find(ref) or ref):
|
||||
raise ParserException(self, ln,
|
||||
'Invalid or unknown file: {0}'
|
||||
.format(ref))
|
||||
if not force_load:
|
||||
Logger.warning('Lang: {0} has already been included!'
|
||||
.format(ref))
|
||||
continue
|
||||
else:
|
||||
Logger.debug('Lang: Reloading {0} '
|
||||
'because include was forced.'
|
||||
.format(ref))
|
||||
kivy.lang.builder.Builder.unload_file(ref)
|
||||
kivy.lang.builder.Builder.load_file(ref)
|
||||
continue
|
||||
Logger.debug('Lang: Including file: {0}'.format(0))
|
||||
__KV_INCLUDES__.append(ref)
|
||||
kivy.lang.builder.Builder.load_file(ref)
|
||||
elif cmd[:7] == 'import ':
|
||||
package = cmd[7:].strip()
|
||||
z = package.split()
|
||||
if len(z) != 2:
|
||||
raise ParserException(self, ln, 'Invalid import syntax')
|
||||
alias, package = z
|
||||
try:
|
||||
if package not in sys.modules:
|
||||
try:
|
||||
mod = importlib.__import__(package)
|
||||
except ImportError:
|
||||
module_name = '.'.join(package.split('.')[:-1])
|
||||
mod = importlib.__import__(module_name)
|
||||
# resolve the whole thing
|
||||
for part in package.split('.')[1:]:
|
||||
mod = getattr(mod, part)
|
||||
else:
|
||||
mod = sys.modules[package]
|
||||
global_idmap[alias] = mod
|
||||
except ImportError:
|
||||
Logger.exception('')
|
||||
raise ParserException(self, ln,
|
||||
'Unable to import package %r' %
|
||||
package)
|
||||
else:
|
||||
raise ParserException(self, ln, 'Unknown directive')
|
||||
|
||||
def parse(self, content):
|
||||
'''Parse the contents of a Parser file and return a list
|
||||
of root objects.
|
||||
'''
|
||||
# Read and parse the lines of the file
|
||||
lines = content.splitlines()
|
||||
if not lines:
|
||||
return
|
||||
num_lines = len(lines)
|
||||
lines = list(zip(list(range(num_lines)), lines))
|
||||
self.sourcecode = lines[:]
|
||||
|
||||
if __debug__:
|
||||
trace('Parser: parsing %d lines' % num_lines)
|
||||
|
||||
# Strip all comments
|
||||
self.strip_comments(lines)
|
||||
|
||||
# Execute directives
|
||||
self.execute_directives()
|
||||
|
||||
# Get object from the first level
|
||||
objects, remaining_lines = self.parse_level(0, lines)
|
||||
|
||||
# Precompile rules tree
|
||||
for rule in objects:
|
||||
rule.precompile()
|
||||
|
||||
# After parsing, there should be no remaining lines
|
||||
# or there's an error we did not catch earlier.
|
||||
if remaining_lines:
|
||||
ln, content = remaining_lines[0]
|
||||
raise ParserException(self, ln, 'Invalid data (not parsed)')
|
||||
|
||||
def strip_comments(self, lines):
|
||||
'''Remove all comments from all lines in-place.
|
||||
Comments need to be on a single line and not at the end of a line.
|
||||
i.e. a comment line's first non-whitespace character must be a #.
|
||||
'''
|
||||
# extract directives
|
||||
for ln, line in lines[:]:
|
||||
stripped = line.strip()
|
||||
if stripped[:2] == '#:':
|
||||
self.directives.append((ln, stripped[2:]))
|
||||
if stripped[:1] == '#':
|
||||
lines.remove((ln, line))
|
||||
if not stripped:
|
||||
lines.remove((ln, line))
|
||||
|
||||
def parse_level(self, level, lines, spaces=0):
|
||||
'''Parse the current level (level * spaces) indentation.
|
||||
'''
|
||||
indent = spaces * level if spaces > 0 else 0
|
||||
objects = []
|
||||
|
||||
current_object = None
|
||||
current_property = None
|
||||
current_propobject = None
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
ln, content = line
|
||||
|
||||
# Get the number of space
|
||||
tmp = content.lstrip(' \t')
|
||||
|
||||
# Replace any tab with 4 spaces
|
||||
tmp = content[:len(content) - len(tmp)]
|
||||
tmp = tmp.replace('\t', ' ')
|
||||
|
||||
# first indent designates the indentation
|
||||
if spaces == 0:
|
||||
spaces = len(tmp)
|
||||
|
||||
count = len(tmp)
|
||||
|
||||
if spaces > 0 and count % spaces != 0:
|
||||
raise ParserException(self, ln,
|
||||
'Invalid indentation, '
|
||||
'must be a multiple of '
|
||||
'%s spaces' % spaces)
|
||||
content = content.strip()
|
||||
rlevel = count // spaces if spaces > 0 else 0
|
||||
|
||||
# Level finished
|
||||
if count < indent:
|
||||
return objects, lines[i - 1:]
|
||||
|
||||
# Current level, create an object
|
||||
elif count == indent:
|
||||
x = content.split(':', 1)
|
||||
if not x[0]:
|
||||
raise ParserException(self, ln, 'Identifier missing')
|
||||
if (len(x) == 2 and len(x[1]) and
|
||||
not x[1].lstrip().startswith('#')):
|
||||
raise ParserException(self, ln,
|
||||
'Invalid data after declaration')
|
||||
name = x[0].rstrip()
|
||||
# if it's not a root rule, then we got some restriction
|
||||
# aka, a valid name, without point or everything else
|
||||
if count != 0:
|
||||
if False in [ord(z) in Parser.PROP_RANGE for z in name]:
|
||||
raise ParserException(self, ln, 'Invalid class name')
|
||||
|
||||
current_object = ParserRule(self, ln, name, rlevel)
|
||||
current_property = None
|
||||
objects.append(current_object)
|
||||
|
||||
# Next level, is it a property or an object ?
|
||||
elif count == indent + spaces:
|
||||
x = content.split(':', 1)
|
||||
if not x[0]:
|
||||
raise ParserException(self, ln, 'Identifier missing')
|
||||
|
||||
# It's a class, add to the current object as a children
|
||||
current_property = None
|
||||
name = x[0].rstrip()
|
||||
ignore_prev = name[0] == '-'
|
||||
if ignore_prev:
|
||||
name = name[1:]
|
||||
|
||||
if ord(name[0]) in Parser.CLASS_RANGE:
|
||||
if ignore_prev:
|
||||
raise ParserException(
|
||||
self, ln, 'clear previous, `-`, not allowed here')
|
||||
_objects, _lines = self.parse_level(
|
||||
level + 1, lines[i:], spaces)
|
||||
current_object.children = _objects
|
||||
lines = _lines
|
||||
i = 0
|
||||
|
||||
# It's a property
|
||||
else:
|
||||
if name not in Parser.PROP_ALLOWED:
|
||||
if not all(ord(z) in Parser.PROP_RANGE for z in name):
|
||||
raise ParserException(self, ln,
|
||||
'Invalid property name')
|
||||
if len(x) == 1:
|
||||
raise ParserException(self, ln, 'Syntax error')
|
||||
value = x[1].strip()
|
||||
if name == 'id':
|
||||
if len(value) <= 0:
|
||||
raise ParserException(self, ln, 'Empty id')
|
||||
if value in ('self', 'root'):
|
||||
raise ParserException(
|
||||
self, ln,
|
||||
'Invalid id, cannot be "self" or "root"')
|
||||
current_object.id = value
|
||||
elif len(value):
|
||||
rule = ParserRuleProperty(
|
||||
self, ln, name, value, ignore_prev)
|
||||
if name[:3] == 'on_':
|
||||
current_object.handlers.append(rule)
|
||||
else:
|
||||
ignore_prev = False
|
||||
current_object.properties[name] = rule
|
||||
else:
|
||||
current_property = name
|
||||
current_propobject = None
|
||||
|
||||
if ignore_prev: # it wasn't consumed
|
||||
raise ParserException(
|
||||
self, ln, 'clear previous, `-`, not allowed here')
|
||||
|
||||
# Two more levels?
|
||||
elif count == indent + 2 * spaces:
|
||||
if current_property in (
|
||||
'canvas', 'canvas.after', 'canvas.before'):
|
||||
_objects, _lines = self.parse_level(
|
||||
level + 2, lines[i:], spaces)
|
||||
rl = ParserRule(self, ln, current_property, rlevel)
|
||||
rl.children = _objects
|
||||
if current_property == 'canvas':
|
||||
current_object.canvas_root = rl
|
||||
elif current_property == 'canvas.before':
|
||||
current_object.canvas_before = rl
|
||||
else:
|
||||
current_object.canvas_after = rl
|
||||
current_property = None
|
||||
lines = _lines
|
||||
i = 0
|
||||
else:
|
||||
if current_propobject is None:
|
||||
current_propobject = ParserRuleProperty(
|
||||
self, ln, current_property, content)
|
||||
if current_property[:3] == 'on_':
|
||||
current_object.handlers.append(current_propobject)
|
||||
else:
|
||||
current_object.properties[current_property] = \
|
||||
current_propobject
|
||||
else:
|
||||
current_propobject.value += '\n' + content
|
||||
|
||||
# Too much indentation, invalid
|
||||
else:
|
||||
raise ParserException(self, ln,
|
||||
'Invalid indentation (too many levels)')
|
||||
|
||||
# Check the next line
|
||||
i += 1
|
||||
|
||||
return objects, []
|
||||
|
||||
|
||||
class ParserSelector(object):
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key.lower()
|
||||
|
||||
def match(self, widget):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s key=%s>' % (self.__class__.__name__, self.key)
|
||||
|
||||
|
||||
class ParserSelectorClass(ParserSelector):
|
||||
|
||||
def match(self, widget):
|
||||
return self.key in widget.cls
|
||||
|
||||
|
||||
class ParserSelectorName(ParserSelector):
|
||||
|
||||
parents = {}
|
||||
|
||||
def get_bases(self, cls):
|
||||
for base in cls.__bases__:
|
||||
if base.__name__ == 'object':
|
||||
break
|
||||
yield base
|
||||
if base.__name__ == 'Widget':
|
||||
break
|
||||
for cbase in self.get_bases(base):
|
||||
yield cbase
|
||||
|
||||
def match(self, widget):
|
||||
parents = ParserSelectorName.parents
|
||||
cls = widget.__class__
|
||||
if cls not in parents:
|
||||
classes = [x.__name__.lower() for x in
|
||||
[cls] + list(self.get_bases(cls))]
|
||||
parents[cls] = classes
|
||||
return self.key in parents[cls]
|
||||
|
||||
def match_rule_name(self, rule_name):
|
||||
return self.key == rule_name.lower()
|
||||
Reference in New Issue
Block a user