0

In a previous question, I asked how to have a row of text inputs which is dinamically added on press of a button, all from a py script.

I am now attempting at moving all my layout code to a kv file. While this was pretty straightforward for the widgets which appear on screen by default, I am not really sure of how to define the dinamically added text inputs from the kv file.

My solution, at present, is to create the 'default' widgets in the kv file, and to add the other ones from the py script through the addIngredient method. Below a minimal working version.

The kv file:

WMan:
    AddWindow:

<AddWindow>:
    name: 'add'
    ingsGrid: ingsGrid
    ing1: ing1
    quant1: quant1
    addIng: addIng
    saveButton: saveButton

    StackLayout:
        id: ingsGrid
        size_hint: .9, None
        height: self.minimum_height
        orientation: 'lr-tb'
        spacing: '5sp', '5sp'


        TextInput:
            id: ing1
            multiline: False
            size_hint: .65, None
            height: self.minimum_height
        TextInput:
            id: quant1
            multiline: False
            size_hint: .25, None
            height: self.minimum_height
        Button:
            id: addIng
            text: "+"
            size_hint: .1, None
            height: ing1.height
            on_release: root.addIngredient(self)
        Button:
            id: saveButton
            text: "Save"
            size_hint: .3, None
            on_release:
                root.saveRec(self)

The py script reads:

from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.stacklayout import StackLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder


class AddWindow(Screen):
    def __init__(self, **kwargs):
        super(AddWindow, self).__init__(**kwargs)

        recipeName = ObjectProperty(None)
        ingsGrid = ObjectProperty(None)
        ing1 = ObjectProperty(None)
        quant1 = ObjectProperty(None)

        self.i = 1
        self.ingsList = {}
        self.ingsList[0] = ing1
        self.quants = {}
        self.quants[0] = quant1

    def addIngredient(self, instance):
        tmp_children_list = self.ingsGrid.children[:]

        self.ingsGrid.clear_widgets()

        # range(start,stop[, step])
        for index in range(len(tmp_children_list)-1, -1, -1):
            # the last item, then last-1, etc
            child = tmp_children_list[index]
            # add the last first (new ones will be added on top)
            self.ingsGrid.add_widget(child)
            # if child is the pressed button
            if child == instance:
                self.ing = TextInput(
                    size_hint=(.65, None),
                    height='30sp')
                self.ingsGrid.add_widget(self.ing)
                self.ingsList[self.i] = self.ing

                self.quant = TextInput(
                    size_hint=(0.25, None),
                    height='30sp')
                self.ingsGrid.add_widget(self.quant)
                self.quants[self.i] = self.quant
                self.i += 1

                self.addNext = Button(
                    text="+",
                    size_hint=(0.1, None),
                    height='30sp')
                self.addNext.bind(on_press=self.addIngredient)
                self.ingsGrid.add_widget(self.addNext)

    def saveRec(self, instance): # grab all inputs and send to SQLite db
        print(self.ingsList)
        print(self.ingsList[0].text)
        print(self.ingsList[1].text)


class WMan(ScreenManager):
    pass

kv = Builder.load_file("test.kv")

class TestApp(App):
    def build(self):
        return kv

if __name__ == "__main__":
    TestApp().run()

My problem here is twofold: first, while this way of dinamically adding rows works as it should, it is to me a bit messy to have half of the layout defined on the kv file, and the other half defined in the py script. So my first question is:

1. Is there a way to move the entire layout to the kv file?

Second questions is:

2. How do I access the content of textInput 'ing1' (the one created in the kv file)?

when I run print(self.ingsList), I get:

{0: <ObjectProperty name=>, 1: <kivy.uix.textinput.TextInput object at 0x000002077FB89C88>}

So while I can easily do print(self.ingsList[1].text), running print(self.ingsList[0].text) will give error:

AttributeError: 'kivy.properties.ObjectProperty' object has no attribute 'text'
gicanzo
  • 69
  • 8

1 Answers1

0

Here is a modified version of your code that does what I think you want:

from kivy.app import App
from kivy.properties import ObjectProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder


class AddWindow(Screen):
    def __init__(self, **kwargs):
        super(AddWindow, self).__init__(**kwargs)

        recipeName = ObjectProperty(None)
        ingsGrid = ObjectProperty(None)
        ing1 = ObjectProperty(None)
        quant1 = ObjectProperty(None)

        self.i = 1
        self.ingsList = {}
        self.ingsList[0] = ing1
        self.quants = {}
        self.quants[0] = quant1

    def saveRec(self, instance): # grab all inputs and send to SQLite db
        for child in self.ids.ingsGrid.children:
            if isinstance(child, OneRow):
                print('ingedient name:', child.ids.ing1.text)
                print('quantity:', child.ids.quant1.text)
                print('')


class WMan(ScreenManager):
    pass


class OneRow(BoxLayout):
    inst_count = NumericProperty(0)
    count = 0
    def __init__(self, **kwargs):
        OneRow.count += 1
        self.inst_count = OneRow.count
        super(OneRow, self).__init__(**kwargs)

    def get_index(self):
        par = self.parent
        if par is None:
            return None
        index = 0
        for index in range(len(par.children) - 1, -1, -1):
            child = par.children[index]
            if child == self:
                return index

kv_str = '''
#:import Factory kivy.factory.Factory
WMan:
    AddWindow:
        id: add

#:set row_height 30
<OneRow>:
    orientation: 'horizontal'
    size_hint_y: None
    height: row_height
    TextInput:
        id: ing1
        multiline: False
        size_hint: .65, None
        height: row_height
    TextInput:
        id: quant1
        multiline: False
        text: str(root.inst_count)
        size_hint: .25, None
        height: row_height
    Button:
        id: addIng
        text: "+"
        size_hint: .1, None
        height: row_height
        on_release: app.root.ids.add.ids.ingsGrid.add_widget(Factory.OneRow(), index=root.get_index())

<AddWindow>:
    name: 'add'
    ingsGrid: ingsGrid
    saveButton: saveButton

    StackLayout:
        id: ingsGrid
        size_hint: .9, None
        height: self.minimum_height
        orientation: 'lr-tb'
        spacing: '5sp', '5sp'

        OneRow:
        Button:
            id: saveButton
            text: "Save"
            size_hint: .3, None
            on_release:
                root.saveRec(self)
'''

# kv = Builder.load_file("test.kv")
kv = Builder.load_string(kv_str)

class TestApp(App):
    def build(self):
        return kv

if __name__ == "__main__":
    TestApp().run()

I used Builder.load_string() instead of load_file() just for my own convenience.

I have created a class named OneRow that holds a single row from the ingredients list, and the + Button now just adds an instance of that class. The get_index() method of that class is only used to position new ingredients below the row with the Button that was pressed. And the other code in that class is just to add some identifying info. If those things are not important to you, you can eliminate the OneRow class definition from the python, and just replace <OneRow>: in the kv with <OneRow@BoxLayout>: and where the OneRow is added you can just set index=1.

John Anderson
  • 35,991
  • 4
  • 13
  • 36