In my Kivy app, one of the text inputs triggers the opening of a DropDown widget when on_focus
. The textinput is part of a custom BoxLayout IngredientRow
which I dinamically add to the screen on the press of a button.
What I want is to fill the textinput with the text of the button selected from the DropDown. This works for the first IngredientRow
. However, when I add new rows, selecting an item from the DropDown in a row different from the first, will fill the textinput from the first row. See below a minimal working example:
The py file:
from kivy.app import App
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.textinput import TextInput
class DelIngButton(Button):
pass
class DropListButton(Button):
def __init__(self, **kwargs):
super(DropListButton, self).__init__(**kwargs)
self.bind(on_release=lambda x: self.parent.parent.select(self.text))
class IngredientRow(BoxLayout):
pass
class MeasureDropDown(DropDown):
pass
####################################
class AddWindow(Screen):
def __init__(self, **kwargs):
super(AddWindow, self).__init__(**kwargs)
self.DropDown = MeasureDropDown()
def addIngredient(self, instance): #adds a new IngredientRow
row = instance.parent
row.remove_widget(row.children[0])
row.add_widget(Factory.DelIngButton(), index=0)
self.ingsGrid.add_widget(Factory.IngredientRow(), index=0)
class WMan(ScreenManager):
def __init__(self, **kwargs):
super(WMan, self).__init__(**kwargs)
kv = Builder.load_file("ui/layout.kv")
class RecipApp(App):
def build(self):
return kv
if __name__ == "__main__":
RecipApp().run()
and the kv file:
#:set text_color 0,0,0,.8
#:set row_height '35sp'
#:set main_padding ['10sp', '10sp']
#:set small_padding ['5sp', '5sp']
<DropListButton>: # Button for custom DropDown
color: text_color
background_normal: ''
<DelIngButton>: # Button to delete row
text: '-'
size_hint: None, None
height: row_height
width: row_height
on_release: self.parent.parent.remove_widget(self.parent)
<MeasureDropDown>:
id: dropDown
DropListButton:
size_hint: 1, None
height: row_height
text: "g"
DropListButton:
size_hint: 1, None
height: row_height
text: "Kg"
TextInput:
size_hint: 1, None
height: row_height
hint_text: 'new'
<IngredientRow>:
orientation: 'horizontal'
size_hint: 1, None
height: row_height
spacing: '5sp'
TextInput:
id: ing
hint_text: 'Ingredient'
multiline: False
size_hint: .6, None
height: row_height
TextInput:
id: quant
hint_text: 'Quantity'
multiline: False
size_hint: .2, None
height: row_height
TextInput:
id: measure
hint_text: 'measure'
size_hint: .2, None
height: row_height
on_focus:
app.root.ids.add.DropDown.open(self) if self.focus else app.root.ids.add.DropDown.dismiss(self)
app.root.ids.add.DropDown.bind(on_select=lambda self, x: setattr(app.root.ids.add.ingredientRow.children[1], 'text', x))
Button:
id: addIng
text: "+"
size_hint: None, None
height: row_height
width: row_height
on_release: app.root.ids.add.addIngredient(self)
<MainScrollView@ScrollView>:
size_hint: 1, None
scroll_type: ['bars', 'content']
##################
# Windows
##################
WMan:
AddWindow:
id: add
<AddWindow>:
name: 'add'
ingsGrid: ingsGrid
ingredientRow: ingredientRow
MainScrollView:
height: self.parent.size[1]
GridLayout:
cols:1
size_hint: 1, None
pos_hint: {"top": 1}
height: self.minimum_height
padding: main_padding
StackLayout:
id: ingsGrid
size_hint: 1, None
height: self.minimum_height
orientation: 'lr-tb'
padding: small_padding
IngredientRow:
id: ingredientRow
I understand the problem is with the following part of the code:
on_select=lambda self, x: setattr(app.root.ids.add.ingredientRow.children[1], 'text', x)
as this will always call the first IngredientRow. However, I could not figure out how to refer to the IngredientRow where the DropDown is called.
Combining my first answer with code to handle the TextInput
in the MeasureDropDown
:
from kivy.app import App
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.properties import BooleanProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.textinput import TextInput
class DelIngButton(Button):
pass
class DropListButton(Button):
def __init__(self, **kwargs):
super(DropListButton, self).__init__(**kwargs)
self.bind(on_release=lambda x: self.parent.parent.select(self.text))
class DropListTextInput(TextInput):
# Provides a couple needed behaviors
def on_focus(self, *args):
if self.focus:
self.dropDown.selection_is_DLTI = True
else:
self.dropDown.selection_is_DLTI = False
def on_text_validate(self, *args):
self.dropDown.selection_is_DLTI = False
# put the text from this widget into the TextInput that the DropDown is attached to
self.dropDown.attach_to.text = self.text
# dismiss the DropDown
self.dropDown.dismiss()
class IngredientRow(BoxLayout):
def __init__(self, **kwargs):
super(IngredientRow, self).__init__(**kwargs)
self.dropdown = MeasureDropDown()
def handle_focus(self, ti):
# handle on_focus event for the measure TextInput
if ti.focus:
# open DropDown if the TextInput gets focus
self.dropdown.open(ti)
else:
# ti has lost focus
if self.dropdown.selection_is_DLTI:
# do not dismiss if a DropListTextInput is the selection
return
# dismiss DropDown
self.dropdown.dismiss(ti)
self.dropdown.unbind_all()
self.dropdown.fbind('on_select', lambda self, x: setattr(ti, 'text', x))
class MeasureDropDown(DropDown):
# set to True if the selection is a DropListTextInput
selection_is_DLTI = BooleanProperty(False)
def unbind_all(self):
for callBack in self.get_property_observers('on_select'):
self.funbind('on_select', callBack)
####################################
class AddWindow(Screen):
def addIngredient(self, instance): #adds a new IngredientRow
row = instance.parent
row.remove_widget(row.children[0])
row.add_widget(Factory.DelIngButton(), index=0)
self.ingsGrid.add_widget(Factory.IngredientRow(), index=0)
class WMan(ScreenManager):
def __init__(self, **kwargs):
super(WMan, self).__init__(**kwargs)
# kv = Builder.load_file("ui/layout.kv")
kv = Builder.load_string('''
#:set text_color 0,0,0,.8
#:set row_height '35sp'
#:set main_padding ['10sp', '10sp']
#:set small_padding ['5sp', '5sp']
<DropListButton>: # Button for custom DropDown
color: text_color
background_normal: ''
<DelIngButton>: # Button to delete row
text: '-'
size_hint: None, None
height: row_height
width: row_height
on_release: self.parent.parent.remove_widget(self.parent)
<MeasureDropDown>:
id: dropDown
DropListButton:
size_hint: 1, None
height: row_height
text: "g"
DropListButton:
size_hint: 1, None
height: row_height
text: "Kg"
DropListTextInput: # CustomTextInput instead of standard TextInput
dropDown: dropDown # provide easy access to the DropDown
size_hint: 1, None
height: row_height
hint_text: 'new'
multiline: False # needed to trigger on_text_validate
<IngredientRow>:
orientation: 'horizontal'
size_hint: 1, None
height: row_height
spacing: '5sp'
TextInput:
id: ing
hint_text: 'Ingredient'
multiline: False
size_hint: .6, None
height: row_height
TextInput:
id: quant
hint_text: 'Quantity'
multiline: False
size_hint: .2, None
height: row_height
TextInput:
id: measure
hint_text: 'measure'
size_hint: .2, None
height: row_height
on_focus:
root.handle_focus(self) # focus event is now handled in the IngredientRow class
Button:
id: addIng
text: "+"
size_hint: None, None
height: row_height
width: row_height
on_release: app.root.ids.add.addIngredient(self)
<MainScrollView@ScrollView>:
size_hint: 1, None
scroll_type: ['bars', 'content']
##################
# Windows
##################
WMan:
AddWindow:
id: add
<AddWindow>:
name: 'add'
ingsGrid: ingsGrid
ingredientRow: ingredientRow
MainScrollView:
height: self.parent.size[1]
GridLayout:
cols:1
size_hint: 1, None
pos_hint: {"top": 1}
height: self.minimum_height
padding: main_padding
StackLayout:
id: ingsGrid
size_hint: 1, None
height: self.minimum_height
orientation: 'lr-tb'
padding: small_padding
IngredientRow:
id: ingredientRow
''')
class RecipApp(App):
def build(self):
return kv
if __name__ == "__main__":
RecipApp().run()
I have added a DropListTextInput
class for use in the MeasureDropDown
and added a handle_focus()
method to the IngredientRow
class.
I have also added a selection_is_DLTI
BooleanProperty
to the MeasureDropDown
class which keeps track of whether the selected widget is a DropListTextInput
.
The new handle_focus()
method does not dismiss the MeasureDropDown
if the selected widget is a DropListTextInput
.
The DropListTextInput
is limited to a single line, so that hitting Enter
in it will trigger the on_text_validate()
method, which sets the text in the measure
TextInput
and dismisses the MeasureDropDown
.
I used Builder.load_string()
just for my own convenience.