10

Can the kivy language access inherited layouts and widgets? I want to create one basic BoxLayout that contains the styling and title Label for my widget. I want to be able to inherit from this widget and add additional widgets in different positions.

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

Builder.load_string('''
<SimpleBar>:
    canvas.before:
        Color:
            rgba: 0, 0.5, 0.5, 1
        Rectangle:
            pos: self.pos
            size: self.size
    BoxLayout:
        id: my_layout
        Label:
            text: "hi"

<NewBar>:
    Label:
        text: "2"
''')

class SimpleBar(BoxLayout):
    def log(self, value):
        print(value)

class NewBar(SimpleBar):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print(dir(self))

class GeneralApp(App):
    def build(self):
        return NewBar()

if __name__ == '__main__':
    GeneralApp().run()

Above is my basic running widget.

I want NewBar's "2" Label to be located before SimpleBar's 'hi' Label like below.

<NewBar>:
     BoxLayout:
         id: my_layout
         Label:
             text: "2"
         Label:
             text: "hi"

I know that - can negate items. However, <-NewBar> removes all of my styling.

Is there any way to do this in the kivy language?

justengel
  • 6,132
  • 4
  • 26
  • 42

3 Answers3

6

Here's a fun thing: you don't need to specify all classes used in kv lang in the lang itself - you can also add them using Factory.register method later in code. Here's an example:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.lang import Builder
from kivy.factory import Factory

from functools import partial

Builder.load_string('''

<MyWidget>:
    Foo
    Bar
''')

class MyWidget(BoxLayout):
    pass

class MyApp(App):
    def build(self):
        Factory.register('Foo', cls=partial(Label, text='foo'))
        Factory.register('Bar', cls=partial(Label, text='bar'))
        return MyWidget()

if __name__ == '__main__':
    MyApp().run()

Let's use it to create a template base widget we later fill with various content. We use a placeholder that we later replace with another widget:

<BaseWidget>:
    orientation: 'vertical'
    Label:
        size_hint: None, 0.1
        text: 'title'
    Placeholder

In the Python code we register a placeholder class in the __init__ method of this base template class.

class BaseWidget(BoxLayout):
    def __init__(self, **args):
        # unregister if already registered...
        Factory.unregister('Placeholder')
        Factory.register('Placeholder', cls=self.placeholder)
        super(BaseWidget, self).__init__(**args)

Now let's define a content class.

<TwoButtonWidget>:
    Button:
        text: 'button 1'
    Button:
        text: 'button 2'

And finally create a customized class that use our base class as a template and replaces its placeholder with a content class. This class don't have its own kivy rules (these are moved into content class) so when we inherite from our base template, no extra widgets are inserted.

# content class
class TwoButtonWidget(BoxLayout):
    pass

# Base class subclass
class CustomizedWidget(BaseWidget):
    placeholder = TwoButtonWidget # set contetnt class

A full example:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.factory import Factory

Builder.load_string('''
<BaseWidget>:
    orientation: 'vertical'
    widget_title: widget_title
    placeholder: placeholder
    Label:
        size_hint: None, 0.1
        id: widget_title
    Placeholder
        id: placeholder

<TwoButtonWidget>:
    button1: button1
    Button:
        text: 'button 1'
        id: button1
    Button:
        text: 'button 2'

<ThreeButtonWidget>:
    orientation: 'vertical'
    Button:
        text: 'button a'
    Button:
        text: 'button b'
    Button:
        text: 'button c'
''')

class BaseWidget(BoxLayout):
    def __init__(self, **args):
        # unregister if already registered...
        Factory.unregister('Placeholder')
        Factory.register('Placeholder', cls=self.placeholder)
        super(BaseWidget, self).__init__(**args)

class TwoButtonWidget(BoxLayout):
    pass

class ThreeButtonWidget(BoxLayout):
    pass

class CustomizedWidget1(BaseWidget):
    placeholder = TwoButtonWidget

class CustomizedWidget2(BaseWidget):
    placeholder = ThreeButtonWidget

class MyApp(App):
    def build(self):
        layout = BoxLayout()
        c1 = CustomizedWidget1()
        # we can access base widget...
        c1.widget_title.text = 'First'
        # we can access placeholder
        c1.placeholder.button1.text = 'This was 1 before'

        c2 = CustomizedWidget2()
        c2.widget_title.text = 'Second'

        layout.add_widget(c1)
        layout.add_widget(c2)
        return layout

if __name__ == '__main__':
    MyApp().run()

You can easily extend it and for example, have multiple placeholders.

Applying this to your case:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.lang import Builder
from kivy.factory import Factory

from functools import partial

Builder.load_string('''

<SimpleBar>:
    canvas.before:
        Color:
            rgba: 0, 0.5, 0.5, 1
        Rectangle:
            pos: self.pos
            size: self.size
    BoxLayout:
        Placeholder
        Label:
            text: "hi"

<NewBarContent>:
    Label:
        text: "2"
''')

class SimpleBar(BoxLayout):
    def __init__(self, **args):
        # unregister if already registered...
        Factory.unregister('Placeholder')
        Factory.register('Placeholder', cls=self.placeholder)
        super(SimpleBar, self).__init__(**args)

class NewBarContent(BoxLayout):
    pass

class NewBar(SimpleBar):
    placeholder = NewBarContent

class MyApp(App):
    def build(self):
        return NewBar()

if __name__ == '__main__':
    MyApp().run()
Nykakin
  • 8,657
  • 2
  • 29
  • 42
  • **Outrageous** – both that this is even feasible *and* that this isn't actually documented anywhere. Seriously. This technique catapults the Kivy language into a full-blown widget-oriented object-oriented language complete with user-defined widget inheritance rules. That said, your last example is more than a little busted: the `SimpleBar` widget can no longer be instantiated, because it fails to define the `placeholder` property. Ideally, you'd default `placeholder` to an invisible null widget class. Does Kivy even provide an invisible null widget class, though? – Cecil Curry Mar 23 '23 at 06:04
  • @CecilCurry `kivy.uix.widget.Widget` is exactly this - a default empty base widget. If you add `placeholder = Widget` into `SimpleBar` you can use it directly. – Nykakin Mar 23 '23 at 19:04
3

If you want to do a composite widget, that accept new children and add them in one specific "container" widget, you have to do some python.

Basically the idea is to override add_widget so that once the basic structure of the widget is there, the new widgets are added using the new method.

Let's say you have this NestingWidget

class NestingWidget(BoxLayout):
     title = StringProperty()

     def activate(self):
         # do something
         pass

with this rule

<NestingWidget>:
    Label:
        text: root.title

    BoxLayout:

    Button:
        on_press: root.activate()

and you want to use it like this:

FloatLayout:
    NestingWidget:
        title: 'first item'
        Image:
            source: '../examples/demo/pictures/images/Ill1.jpg'

this won't work immediatly, because the Image will be added as a direct child of NestingWidget, so it'll be under the Button.

Howether, you probably remarked that some widgets in kivy can accept new nested widgets, while being already complex on their own.

The trick to do that is — as said before — to override add_widget.

First, let's add an id to our container.

<NestingWidget>:
    Label:
        text: root.title

    BoxLayout:
        id: container

    Button:
        on_press: root.activate()

then, let's use it in add_widget.

class NestingWidget(BoxLayout):
    …
    def add_widget(self, *args, **kwargs):
        if 'container' in self.ids:
            return self.ids.container.add_widget(*args, **kwargs)
        else:
            return super(NestingWidget, self).add_widget(*args, **kwargs)
Tshirtman
  • 5,859
  • 1
  • 19
  • 26
1

With simple kv no, because if you put something in the widget (e.g. Label: ...), it'll call <widget>.add_widget() method and when such a method is called without additional parameters, it will by default place the widget after whatever was already placed before it. Therefore you can either search through the kivy/lang/parser.py file and add such a functionality(PR welcome), or do it within python in hm... __init__ or wherever you'll like to add the widget (maybe after some event).

To do that within python you can call <widget (or self)>.add_widget(<child>, index=<where>) according to the docs. For example:

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty

Builder.load_string('''
#:import Factory kivy.factory.Factory
<Ninja@Label>:

<SimpleBar>:
    BoxLayout:
        id: my_layout
        Label:
            text: "hi"

<ChildWithBenefits>:
    placebefore:
        [(Factory.Label(text='I am the first!'), 0),
        (Factory.Ninja(text='No, I am!'), 2)]
''')

class SimpleBar(BoxLayout):
    def log(self, value):
        print(value)

class ChildWithBenefits(SimpleBar):
    placebefore = ListProperty([])
    def __init__(self, *args, **kwargs):
        super(ChildWithBenefits, self).__init__(*args, **kwargs)
        for child, index in self.placebefore:
            print child.text
            print type(child)
            self.add_widget(child, index=index)
        self.log('Hello!')

class GeneralApp(App):
    def build(self):
        return ChildWithBenefits()

if __name__ == '__main__':
    GeneralApp().run()
Peter Badida
  • 11,310
  • 10
  • 44
  • 90