Update kivy recycleview self.data from textinput change - python

I haven't been able to figure out how to do update the data from a change in the text input on the UI. I've been trying to base my code on the following: https://github.com/kivy/kivy/issues/5318
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
import re
string_to_build_recycleview = '''
<rowTest#BoxLayout>:
#canvas:
# Rectangle:
# size: self.size
# pos: self.pos
name_text: 'Someone Else'
stroke_text: 'Butterfly'
classification_text: '3'
time_text: '9:99.99'
BoxLayout:
orientation: 'horizontal'
Label:
id: name
text: root.name_text
Label:
id: stroke
text: root.stroke_text
ClassificationTI:
id: classification
padding: (8, 1, 2, 1)
halign: 'center'
size: (20,20)
multiline: 'False'
input_filter: 'int'
text: root.classification_text
on_text: root.classification_text = self.text
TimeTI:
padding: (8, 1, 2, 1)
halign: 'center'
size: (20,20)
multiline: 'False'
text: root.time_text
#on_text: root.time_text = self.text
<RecycleViewTEST#RecycleView>:
id: myListToTest
scroll_type: ['bars', 'content']
#scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'rowTest'
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
'''
class ClassificationTI(TextInput):
def insert_text(self,substr,from_undo=False):
if substr.isnumeric():
return super(ClassificationTI, self).insert_text(substr,from_undo=from_undo)
return super(ClassificationTI, self)
class TimeTI(TextInput):
def insert_text(self,substr,from_undo=False):
if substr.isnumeric() or substr == ":" or substr == ".":
return super(TimeTI, self).insert_text(substr,from_undo=from_undo)
return super(TimeTI, self)
class RecycleViewTEST(RecycleView):
def __init__(self, **kwargs):
super(RecycleViewTEST, self).__init__(**kwargs)
Builder.load_string(string_to_build_recycleview)
self.initial_populate()
def on_enter(self, *args):
self.get_real_data()
def initial_populate(self):
data = (('TheFirstOne', 'free', '9', '1:01.60'),('TheSecondOne', 'free', '9', '1:01.60'))
self.data = [{'name_text':n,'stroke_text':s,'classification_text':c,'time_text':t} for n,s,c,t in data]
print(self.data)
def validate_classification(self,classification):
if classification.isnumeric() and int(classification) > 14 and int(classification) < 1:
return "Not Valid"
return classification
def validate_time(self,time):
if re.match(r"([0-9]?[0-9]:)?([0-5][0-9])\.([0-9][0-9])", time) is None:
return "Not Valid"
return time
def do_stuff(self):
for i in range(len(self.data)):
print(self.data[i])
self.data[i]['classification_text'] = self.validate_classification(self.data[i]['classification_text'])
self.data[i]['time_text'] = self.validate_time(self.data[i]['time_text'])
print(self.data[i])
self.refresh_from_data()
string_to_build_the_form = '''
<TheForm>:
BoxLayout:
orientation: 'vertical'
BoxLayout:
orientation: 'horizontal'
Label:
size_hint: (None, None)
size: (200, 27)
text: '[u]Name[/u]'
markup: True
Label:
size_hint: (None, None)
size: (200, 27)
text: '[u]Stroke[/u]'
markup: True
Label:
size_hint: (None, None)
size: (190, 27)
text: '[u]Class[/u]'
markup: True
Label:
size_hint: (None, None)
size: (200, 27)
text: '[u]Time (mm:ss.hh)[/u]'
markup: True
BoxLayout:
size_hint: (1,None)
RecycleViewTEST:
id: myListToTest
BoxLayout:
orientation: 'horizontal'
Button:
size_hint: (None,None)
size: (150,50)
text: "Validate"
on_release: root.validate()
Button:
id: save_recalc
size_hint: (None,None)
size: (150,50)
text: "Save & Recalculate"
disabled: True
'''
class TheForm(BoxLayout):
def __init__(self, **kwargs):
super(TheForm, self).__init__(**kwargs)
Builder.load_string(string_to_build_the_form)
def validate(self, *args):
self.ids.myListToTest.do_stuff()
self.ids.save_recalc.disabled = False
class TheTEST(App):
def build(self):
sm = TheForm()
return sm
if __name__ == '__main__':
TheTEST().run()
This is after I change the values text input boxes. The before refers to validating the classification and time then changing the data. After executing validate(), the textinput text returns to the original numbers:
BEFORE: {'name_text': 'TheFirstOne', 'stroke_text': 'free', 'classification_text': '9', 'time_text': '1:01.60'}
AFTER: {'name_text': 'TheFirstOne', 'stroke_text': 'free', 'classification_text': '9', 'time_text': '1:01.60'}
BEFORE: {'name_text': 'TheSecondOne', 'stroke_text': 'free', 'classification_text': '9', 'time_text': '1:01.60'}
AFTER: {'name_text': 'TheSecondOne', 'stroke_text': 'free', 'classification_text': '9', 'time_text': '1:01.60'}
What am I missing here? Thanks for the help!

I got an answer from the Kivy forums on discord. The key is to pass a index to the row widget so they can change the data for their row. I think this should work for any complex widget in a recycleview object. Hope this helps someone.
from kivy.app import App
from kivy.properties import ObjectProperty, NumericProperty, ListProperty
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.textinput import TextInput
from kivy.lang.builder import Builder
KV = """
<RecycleItem>:
on_text: if root.owner != None: self.owner.data[self.index]['text'] = self.text
RecycleView:
data: app.data
viewclass: 'RecycleItem'
RecycleBoxLayout:
spacing: 10
default_size: None, dp(80)
default_size_hint: 1, None
orientation: 'vertical'
size_hint_y: None
height: self.minimum_height
"""
class RecycleItem(RecycleDataViewBehavior,TextInput):
owner = ObjectProperty()
index = NumericProperty(0)
def refresh_view_attrs(self, rv, index, data):
self.index = index
print("INDEXXXXXX: ",self.index)
return super(RecycleItem, self).refresh_view_attrs(rv, index, data)
class Test(App):
data = ListProperty()
def build(self):
self.data = [{"text": "Label "+str(x), 'owner': self} for x in range(20)]
return Builder.load_string(KV)
Test().run()

Related

Kivy Recycleview updates from button but not from app build method

A sample Kivy app populates Recycleview upon a button click.
However, when I copy the code from the class functions into the App.build to populate the Recycleview upon app loading, it doesn't do it. Is it not referencing the Recycleview correctly? Why is this not working?
I saw some answers here that used "self.ids." to find the Recycleview data, and also "App.get_running_app()." but it didn't work.
(Note: requires Kivy 2.0.0rc4 to compile)
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
kv = """
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class TestApp(App):
def build(self):
t = Test()
t.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
t.rv.refresh_from_data()
return Test()
if __name__ == '__main__':
TestApp().run()
One solution is to perform the Recycleview update from the Test class.
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
kv = """
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.populate()
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()
It seems to be a question of the hierarchy and Kivy design. There's no instance of the RecycleView getting passed to the top object App.

How to populate recycleview from input on another 'screen'

good day
what im trying to accomplish is to input text on one screen then have it create a button on another screen within a recycleview where if I keep adding buttons the recycleview keeps getting populated. I would assume that the button1 function would update the rvs.rv.data and that the recycleview would use the it update itself. could you point me in the right direction please?
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = """
<custombutton#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Button:
text: root.value
<Root>:
RVScreen:
name: 'Rv'
InputScreen:
name: 'input_screen'
<InputScreen>:
name: "input_screen"
orientation: "vertical"
BoxLayout:
orientation: "vertical"
TextInput:
id: textinput
Button:
text: "hi"
on_press: print(app.rvs.rv.data)
on_press: app.rvs.button1('some_value')
Button:
text: 'rvscreen'
on_press: root.manager.current = 'Rv'
<RVScreen>:
name: 'Rv'
rv: rv
orientation: "vertical"
BoxLayout:
orientation: "vertical"
Button:
text: 'refresh'
on_press: root.rv.refresh_from_data()
on_press: print(app.rvs.rv.data)
Button:
text: "input page"
on_press: root.manager.current = 'input_screen'
RecycleView:
id: rv
viewclass: 'custombutton'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
"""
Builder.load_string(kv)
class Root(ScreenManager):
pass
class RVScreen(Screen):
def __init__(self, **kwargs):
super(RVScreen, self).__init__(**kwargs)
def button1(self, value):
self.rv.data.insert(0, {'value': value or 'default value'})
class InputScreen(Screen):
pass
class TestApp(App):
rvs = RVScreen()
def build(self):
return Root()
if __name__ == '__main__':
TestApp().run()
The problem is that you are populating the data of the app.rvs, but that is created by the line:
rvs = RVScreen()
which creates a new RVScreen instance that is entirely unrelated to the RVScreen that is displayed in your GUI. In order to update the RVScreen in your GUI, you need to get a reference to that RVScreen.
I like to that that through a method in the App, just because it makes the code simpler. So here is a modified version of your code that does it my way:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen, ScreenManager
kv = """
<custombutton#BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Button:
text: root.value
<Root>:
RVScreen:
name: 'Rv'
InputScreen:
name: 'input_screen'
<InputScreen>:
name: "input_screen"
orientation: "vertical"
BoxLayout:
orientation: "vertical"
TextInput:
id: textinput
Button:
text: "hi"
on_press: app.add_data(textinput.text)
Button:
text: 'rvscreen'
on_press: root.manager.current = 'Rv'
<RVScreen>:
name: 'Rv'
rv: rv
orientation: "vertical"
BoxLayout:
orientation: "vertical"
Button:
text: 'refresh'
on_press: rv.refresh_from_data()
Button:
text: "input page"
on_press: root.manager.current = 'input_screen'
RecycleView:
id: rv
viewclass: 'custombutton'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
"""
Builder.load_string(kv)
class Root(ScreenManager):
pass
class RVScreen(Screen):
pass
class InputScreen(Screen):
pass
class TestApp(App):
# rvs = RVScreen() # this code does nothing useful
def build(self):
return Root()
def add_data(self, value):
# get the `Screen` instance
rvs = self.root.get_screen('Rv')
# insert the new value into the data
rvs.ids.rv.data.insert(0, {'value': value or 'default value'})
if __name__ == '__main__':
TestApp().run()
I also made a small modification, so that the new value comes from the TextInput.

Kivy Multiple Column RecyclerView

i was just playing with Python and kivy , I've loaded my String data into a RecyclerView as per the kivy official documentation. but I've faced trouble on loading an object to multiple columns inside the list like a form data. for example i wanted to have name,family name and age to three columns with title headers row by row , I've also tried RecyclerGridLayout with 3 columns , but it can load just name into grids regardless of row by row requirement
<RV>:
viewclass: 'Label'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
Will appreciate any hint or sample code to learn how RecyclerView works on kivy
I was also looking for this and I could not find a specific example, so I have provided my solution. As el3ien has said, you will need to create a custom class which will represent each row of your selectable label.
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
label1_text: 'label 1 text' # I have included two methods of accessing the labels
label2_text: 'label 2 text' # This is method 1
label3_text: 'label 3 text'
pos: self.pos
size: self.size
Label:
id: id_label1 # method 2 uses the label id
text: root.label1_text
Label:
id: id_label2
text: root.label2_text
Label:
id: id_label3
text: root.label3_text
In applying your data into the RV, you will need to restructure the dictionary to reflect the label layout
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
paired_iter = zip(items_1, items_2) # items_1 and items_2 are defined elsewhere
self.data = []
for i1, i2 in paired_iter:
d = {'label2': {'text': i1}, 'label3': {'text': i2}}
self.data.append(d)
Finally in the refresh_view_attrs, you will specify .label_text which is bound to each label, or you can use label id's.
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.label1_text = str(index)
self.label2_text = data['label2']['text']
self.ids['id_label3'].text = data['label3']['text'] # As an alternate method of assignment
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
The entire code is below:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
Builder.load_string('''
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
Rectangle:
pos: self.pos
size: self.size
label1_text: 'label 1 text'
label2_text: 'label 2 text'
label3_text: 'label 3 text'
pos: self.pos
size: self.size
Label:
id: id_label1
text: root.label1_text
Label:
id: id_label2
text: root.label2_text
Label:
id: id_label3
text: root.label3_text
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
''')
items_1 = {'apple', 'banana', 'pear', 'pineapple'}
items_2 = {'dog', 'cat', 'rat', 'bat'}
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, GridLayout):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
cols = 3
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
self.label1_text = str(index)
self.label2_text = data['label2']['text']
self.ids['id_label3'].text = data['label3']['text'] # As an alternate method of assignment
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableLabel, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
if is_selected:
print("selection changed to {0}".format(rv.data[index]))
else:
print("selection removed for {0}".format(rv.data[index]))
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
paired_iter = zip(items_1, items_2)
self.data = []
for i1, i2 in paired_iter:
d = {'label2': {'text': i1}, 'label3': {'text': i2}}
self.data.append(d)
# can also be performed in a complicated one liner for those who like it tricky
# self.data = [{'label2': {'text': i1}, 'label3': {'text': i2}} for i1, i2 in zip(items_1, items_2)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Instead of using Label as viewclass, create a custom class. That could be a horizontal box layout with two boxes.
<CustomClass#BoxLayout>:
orientation: "horizontal"
Label:
Label:
I used the above idea from #el3ien. My code is below.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
Builder.load_string('''
<RV>:
viewclass: 'myView'
RecycleBoxLayout:
default_size: None, dp(200)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<myView#BoxLayout>:
BoxLayout:
orientation: 'horizontal'
BoxLayout:
orientation: 'vertical'
on_release:
Button:
size_hint: (1,1)
background_normal: 'C:/Users/Arsalan/Desktop/dummyImage2.jpg'
background_down: 'C:/Users/Arsalan/Desktop/dummyImage1.png'
text:
text_size: self.size
halign:
valign: 'middle'
Label:
size_hint: (1,0.3)
text: 'Product summary'
text_size: self.size
halign:
valign: 'middle'
canvas.before:
Color:
rgba: (0.6, 0.7, 0.4, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout:
size_hint :(1,0.01)
Label:
size_hint: (1,0.3)
text: 'Rs 600'
text_size: self.size
halign:
valign: 'middle'
BoxLayout:
orientation: 'vertical'
size_hint: (0.001,1)
BoxLayout:
orientation: 'vertical'
on_release:
Button:
size_hint: (1,1)
background_normal: 'C:/Users/Arsalan/Desktop/dummyImage2.jpg'
background_down: 'C:/Users/Arsalan/Desktop/dummyImage1.png'
text:
text_size: self.size
halign:
valign: 'middle'
Label:
size_hint: (1,0.3)
text: 'Product summary'
text_size: self.size
halign:
valign: 'middle'
canvas.before:
Color:
rgba: (0.6, 0.7, 0.4, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout:
size_hint :(1,0.01)
Label:
size_hint: (1,0.3)
text: 'Rs 600'
text_size: self.size
halign:
valign: 'middle'
''')
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
Try it out and let me know if you still have any question.
I would like to add a headers row to the table; and, I am trying the following:
<RV>:
BoxLayout:
orientation: "vertical"
BoxLayout:
orientation: 'horizontal'
size_hint: 1, None
size_hint_y: None
height: 25
Label:
text: "Item"
Label:
text: "User ID"
Label:
text: "User Name"
BoxLayout:
RecycleView:
id: review
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
But then the :SelectableRecycleBoxLayout moves deeper into the hierarchy and I no longer get the table, only the headers...how to fix this? I still don't quite get how to get to kv stuff from the python. :-(

Python/Kivy : ValueError: TreeViewLabelAccountGroup.text accept only str

I have written some code in Python (test.py) and kivy (test.kv).When i run test.py.then show Test Menu and user submenu.When i click on user then +Add button show.When click on +add then show Number TextBox Type.
I am using treeView in number textbox.when i click on select number then it gives error
File "kivy/_event.pyx", line 273, in kivy._event.EventDispatcher.init (kivy/_event.c:5375)
File "kivy/properties.pyx", line 478, in kivy.properties.Property.set (kivy/properties.c:5171)
File "kivy/properties.pyx", line 513, in kivy.properties.Property.set (kivy/properties.c:5882)
ValueError: TreeViewLabelAccountGroup.text accept only str
if i use rows = [(1, 'Male'), (2, 'Female'), (3, 'Dog')] instead of rows = [(1, 111), (2, 112), (3, 113)]
then its working fine.
I do not know where I'm making a mistake.
If i change text to int then its working but data not showing.
if parent is None:
tree_node = tree_view_account_group.add_node(TreeViewLabelAccountGroup(text=node['node_id'],
is_open=True))
else:
tree_node = tree_view_account_group.add_node(TreeViewLabelAccountGroup(text=node['node_id'],
is_open=True), parent)
test.py
import kivy
kivy.require('1.9.0') # replace with your current kivy version !
import sqlite3 as lite
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import BooleanProperty, ListProperty, StringProperty, ObjectProperty, NumericProperty
from kivy.lang import Builder
from kivy.uix.dropdown import DropDown
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.button import Button
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.popup import Popup
from kivy.core.window import Window
from kivy.uix.label import Label
Window.maximize()
from kivy.clock import Clock
from kivy.uix.treeview import TreeView, TreeViewLabel, TreeViewNode
from kivy.uix.image import AsyncImage
import os
import sys
#root.attributes("-toolwindow", 1)
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
def populate_tree_view_account_group(tree_view_account_group, parent, node):
if parent is None:
tree_node = tree_view_account_group.add_node(TreeViewLabelAccountGroup(text=node['node_id'],
is_open=True))
else:
tree_node = tree_view_account_group.add_node(TreeViewLabelAccountGroup(text=node['node_id'],
is_open=True), parent)
for child_node in node['children']:
populate_tree_view_account_group(tree_view_account_group, tree_node, child_node)
#this is retrieve from database
rows = [(1, 111), (2, 112), (3, 113)]
treeAccountGroup = []
for r in rows:
treeAccountGroup.append({'node_id': r[1], 'children': []})
class TreeviewAccountGroup(Popup):
treeviewAccount = ObjectProperty(None)
tv = ObjectProperty(None)
h = NumericProperty(0)
#ti = ObjectProperty()
popup = ObjectProperty()
def __init__(self, **kwargs):
super(TreeviewAccountGroup, self).__init__(**kwargs)
self.tv = TreeView(root_options=dict(text=""),
hide_root=False,
indent_level=4)
for branch in treeAccountGroup:
populate_tree_view_account_group(self.tv, None, branch)
#self.remove_widgets()
self.treeviewAccount.add_widget(self.tv)
Clock.schedule_once(self.update, 1)
def remove_widgets(self):
for child in [child for child in self.treeviewAccount.children]:
self.treeviewAccount.remove_widget(child)
def update(self, *args):
self.h = len([child for child in self.tv.children]) * 24
def filter(self, f):
self.treeviewAccount.clear_widgets()
self.tv = TreeView(root_options=dict(text=""),
hide_root=False,
indent_level=4)
new_tree = []
for n in treeAccountGroup:
if f.lower() in n['node_id'].lower():
new_tree.append(n)
for branch in new_tree:
populate_tree_view_account_group(self.tv, None, branch)
self.treeviewAccount.add_widget(self.tv)
class TreeViewLabelAccountGroup(Label, TreeViewNode):
pass
class AccountGroupPopup(Popup):
category_label = ObjectProperty(None)
category_text = ObjectProperty(None)
name_label = ObjectProperty(None)
name_txt = ObjectProperty(None)
popupGroupAccount = ObjectProperty(None)
popupEffect = ObjectProperty(None)
groupType = ObjectProperty(None)
mode = StringProperty("")
col_data = ListProperty(["?", "?", "?", "?","?"])
index = NumericProperty(0)
popup4 = ObjectProperty(None)
popup5 = ObjectProperty(None)
primary_check_button = BooleanProperty(False)
secondary_check_button = BooleanProperty(False)
def __init__(self, obj, **kwargs):
super(AccountGroupPopup, self).__init__(**kwargs)
self.mode = obj.mode
if obj.mode == "Add":
self.col_data[0] = ''
self.col_data[1] = ''
self.col_data[2] = ''
self.col_data[3] = 'Select Number'
def display_primary_treeview(self, instance):
if len(instance.text) > 0:
if self.popupGroupAccount is None:
self.popupGroupAccount = TreeviewAccountGroup()
self.popupGroupAccount.popup4 = self
self.popupGroupAccount.filter(instance.text)
self.popupGroupAccount.open()
def display_effect_type(self, instance):
if len(instance.text) > 0:
if self.popupEffect is None:
self.popupEffect = TreeviewEffectType()
self.popupEffect.popup5 = self
self.popupEffect.filter(instance.text)
self.popupEffect.open()
class SelectableButtonGroupAccount(RecycleDataViewBehavior, Button):
''' Add selection support to the Button '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
rv_data_account_group = ObjectProperty(None)
start_point = NumericProperty(0)
mode = StringProperty("Update")
def __init__(self, **kwargs):
super(SelectableButtonGroupAccount, self).__init__(**kwargs)
Clock.schedule_interval(self.update, .0005)
def update(self, *args):
self.text = self.rv_data_account_group[self.index][self.key]
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableButtonGroupAccount, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableButtonGroupAccount, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
self.rv_data_account_group = rv.data
#print("selection changed to {0}".format(rv.data[1]))
def on_press(self):
popup = AccountGroupPopup(self)
popup.open()
class RVACCOUNTGROUP(BoxLayout):
def add_account_group(self):
self.mode = "Add"
popup = AccountGroupPopup(self)
popup.open()
class CustDrop(DropDown):
def __init__(self, **kwargs):
super(CustDrop, self).__init__(**kwargs)
self.select('')
class MainMenu(BoxLayout):
content_area = ObjectProperty()
rv = ObjectProperty(None)
dropdown = ObjectProperty(None)
def display_account_group(self):
self.dropdown.dismiss()
self.remove_widgets()
self.rvarea = RVACCOUNTGROUP()
self.content_area.add_widget(self.rvarea)
def remove_widgets(self):
self.content_area.clear_widgets()
def insert_update(self, obj):
print('Test')
class TacApp(App):
title = "FactEx"
def build(self):
self.root = Builder.load_file('test.kv')
return MainMenu()
if __name__ == '__main__':
TacApp().run()
test.kv
#:kivy 1.10.0
#:import CoreImage kivy.core.image.Image
#:import os os
<TreeviewAccountGroup>:
id: treeviewAccount
treeviewAccount: treeviewAccount
title: ""
pos_hint: {'x': .65, 'y': .3}
size_hint: .2,.4
#size: 800, 800
auto_dismiss: False
BoxLayout
orientation: "vertical"
ScrollView:
size_hint: 1, .9
BoxLayout:
size_hint_y: None
id: treeviewAccount
height: root.h
rooot: root
TextInput:
id: treeview
size_hint_y: .1
on_text: root.filter(self.text)
#BoxLayout:
#id: treeview
#on_press: root.select_node(self.text)
Button:
size_hint: 1, 0.1
text: "Close"
on_release: root.dismiss()
<TreeviewEffectType>:
#id: treeviewAccount
treeviewEffectType: treeviewEffectType
title: ""
pos_hint: {'x': .65, 'y': .3}
size_hint: .2,.4
#size: 800, 800
auto_dismiss: False
BoxLayout
orientation: "vertical"
ScrollView:
size_hint: 1, .9
BoxLayout:
size_hint_y: None
id: treeviewEffectType
height: root.h
rooot: root
TextInput:
id: treeview
size_hint_y: .1
on_text: root.filter(self.text)
#BoxLayout:
#id: treeview
#on_press: root.select_node(self.text)
Button:
size_hint: 1, 0.1
text: "Close"
on_release: root.dismiss()
<TreeViewLabelEffectType>:
height: 24
on_touch_down:
root.parent.parent.rooot.popup5.col_data[4] = self.text
#app.root.effect_type_txt.text = self.text
#root.parent.parent.rooot.popup5.popup.dismiss()
<TreeViewLabelAccountGroup>:
height: 24
on_touch_down:
root.parent.parent.rooot.popup4.col_data[3] = self.text
#app.root.effect_type_txt.text = self.text
#app.root.popupEffect.dismiss()
<AccountGroupPopup>:
title: ""
size_hint: None, None
size: 500, 350
auto_dismiss: False
BoxLayout:
orientation: "vertical"
GridLayout:
cols: 2
padding : 30, 0
row_default_height: '30dp'
size_hint: 1, .1
pos_hint: {'x': .1, 'y': .06}
GridLayout:
cols: 2
padding: 10, 10
spacing: 20, 20
#row_default_height: '30dp'
size_hint: 1, .7
pos_hint: {'x': 0, 'y':.65}
Label:
id:category_label
text: 'Number'
text_size: self.size
valign: 'middle'
TextInput:
id: category_text
text: root.col_data[3]
on_focus: root.display_primary_treeview(self)
Button:
text: 'Ok'
on_release:
#root.package_changes(name_txt.text,primary_group_txt.text,effect_type_txt.text)
app.root.insert_update(root)
root.dismiss()
Button:
text: 'Cancel'
on_release: root.dismiss()
<AccountGroup#RecycleView>:
viewclass: 'SelectableButtonGroupAccount'
SelectableRecycleGridLayout:
cols: 1
default_size: None, dp(26)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
<RVACCOUNTGROUP>:
BoxLayout:
orientation: "vertical"
Button:
size_hint: .05, .03
text: "+Add"
on_press: root.add_account_group()
<DropdownButton#Button>:
border: (0, 16, 0, 16)
text_size: self.size
valign: "middle"
padding_x: 5
size_hint_y: None
height: '30dp'
#on_release: dropdown.select('')
#on_release: app.root.test
background_color: 90 , 90, 90, 90
color: 0, 0.517, 0.705, 1
<MenuButton#Button>:
text_size: self.size
valign: "middle"
padding_x: 5
size : (80,30)
size_hint : (None, None)
background_color: 90 , 90, 90, 90
background_normal: ''
color: 0, 0.517, 0.705, 1
border: (0, 10, 0, 0)
<MainMenu>:
content_area: content_area
dropdown: dropdown
BoxLayout:
orientation: 'vertical'
#spacing : 10
BoxLayout:
canvas.before:
Rectangle:
pos: self.pos
size: self.size
size_hint_y: 1
MenuButton:
id: btn
text: 'Test'
size : (60,30)
on_release: dropdown.open(self)
CustDrop:
id: dropdown
auto_width: False
width: 150
DropdownButton:
text: 'User'
size_hint_y: None
height: '32dp'
# on_release: dropdown3.open(self)
on_release: root.display_account_group()
BoxLayout:
id: content_area
size_hint_y: 30
Label:
size_hint_y: 1
Can someone help me?
You need to convert the node_id to str:
for r in rows:
treeAccountGroup.append({'node_id': str(r[1]), 'children': []})

Search Box with suggestions in kivy?

I am new to kivy wanted to know how we can bind textinput box to suggestion so that user and touch and select the suggestions.
I have long list of buttons out of which i want to select on the basis of name.
i am using kivymd i dont real know how to do it in kivy but here are the codes for kivymd
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivy.uix.screenmanager import Screen
from kivymd.icon_definitions import md_icons
from kivymd.app import MDApp
from kivymd.uix.list import OneLineIconListItem
Builder.load_string(
'''
#:import images_path kivymd.images_path
<CustomOneLineIconListItem>:
IconLeftWidget:
icon: root.icon
<PreviousMDIcons>:
BoxLayout:
orientation: 'vertical'
spacing: dp(10)
padding: dp(20)
BoxLayout:
size_hint_y: None
height: self.minimum_height
MDIconButton:
icon: 'magnify'
MDTextField:
id: search_field
hint_text: 'Search icon'
on_text: root.set_list_md_icons(self.text, True)
RecycleView:
id: rv
key_viewclass: 'viewclass'
key_size: 'height'
RecycleBoxLayout:
padding: dp(10)
default_size: None, dp(48)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
'''
)
class CustomOneLineIconListItem(OneLineIconListItem):
icon = StringProperty()
class PreviousMDIcons(Screen):
def set_list_md_icons(self, text="", search=False):
'''Builds a list of icons for the screen MDIcons.'''
def add_icon_item(name_icon):
self.ids.rv.data.append(
{
"viewclass": "CustomOneLineIconListItem",
"icon": name_icon,
"text": name_icon,
"callback": lambda x: x,
}
)
self.ids.rv.data = []
for name_icon in md_icons.keys():
if search:
if text in name_icon:
add_icon_item(name_icon)
else:
add_icon_item(name_icon)
class MainApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = PreviousMDIcons()
def build(self):
return self.screen
def on_start(self):
self.screen.set_list_md_icons()
MainApp().run()
some results
here is the screen shot result when your not searching for anything
here is a screen shot when i am trying to search
second
here is a result when the word am searching does not exist
Here is a simple example that will use the use text input to search in the option_list and will display the suggestion under the text input widget.
I used the kivymd widget to get a nice look design you replaced with the normal kivy widgets if you want
from kivy.properties import ListProperty
from kivymd.app import MDApp
from kivymd.uix.list import OneLineAvatarIconListItem
from kivymd.uix.textfield import MDTextField
kv = """
Screen:
BoxLayout:
orientation: 'vertical'
spacing: 1
BoxLayout:
size_hint_y: 1/5
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size[0], 2
MDIconButton:
icon: 'magnify'
size_hint_y: 1
SearchTextInput:
id: Search_TextInput_id
size_hint_y: .97
pos_hint:{ 'left':0 , 'top': 1}
hint_text: 'search'
hint_text_color: 1,1,1,1
icon_left: 'magnify'
mode: "fill"
helper_text_mode: "persistent"
helper_text: "Search"
line_color: [1,1,1,1]
color_normal: [1,1,1,1]
font_size: .35 * self.height
active_line: False
multiline: False
MDIconButton:
icon: 'close'
size_hint_y:1
text_color: 0,0,0,1
BoxLayout:
orientation: 'vertical'
padding: 4
RecycleView:
viewclass: 'Search_Select_Option'
data:app.rv_data
RecycleBoxLayout:
spacing: 15
padding : 10
default_size: None, None
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<Search_Select_Option>:
on_release: print(self.text)
IconRightWidget:
icon: "arrow-top-left"
"""
class Search_Select_Option(OneLineAvatarIconListItem):
pass
class SearchTextInput(MDTextField):
option_list = 'one1,two1,two2,three1,three2,three3,four1,four2,four3,four4,five1,five2,five3,five4,five5'.split(',')
def on_text(self, instance, value):
app = MDApp.get_running_app()
option_list = list(set(self.option_list + value[:value.rfind(' ')].split(' ')))
val = value[value.rfind(' ') + 1:]
if not val:
return
try:
app.option_data = []
for i in range(len(option_list)):
word = [word for word in option_list if word.startswith(val)][0][len(val):]
if not word:
return
if self.text + word in option_list:
if self.text + word not in app.option_data:
popped_suggest = option_list.pop(option_list.index(str(self.text + word)))
app.option_data.append(popped_suggest)
app.update_data(app.option_data)
except IndexError:
pass
class RVTestApp(MDApp):
rv_data = ListProperty()
def update_data(self, rv_data_list):
self.rv_data = [{'text': item} for item in rv_data_list]
print(self.rv_data, 'update')
def build(self):
return Builder.load_string(kv)
RVTestApp().run()
Incase anyone is wondering how can get the value of the selected item from the list, I have a little updated version here:
from kivymd.icon_definitions import md_icons
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.list import OneLineListItem
from kivy.uix.screenmanager import Screen
KV = '''
<ListSelect>
BoxLayout:
orientation: 'vertical'
spacing: dp(10)
padding: dp(20)
pos_hint:{'center_x': 0.5, 'y': 0.85}
BoxLayout:
size_hint_y: None
height: self.minimum_height
MDIconButton:
icon: 'magnify'
MDTextField:
id: search_field
hint_text: 'Search icon'
on_text:
root.set_list(self.text)
RecycleView:
pos_hint:{'center_x': 0.5, 'center_y': 0.4}
MDList:
id: container
'''
class ListSelect(Screen):
def pressed(self, value):
# value here is the OneLineListItem
self.ids.container.clear_widgets()
# set TextField text to selected list item
self.ids.search_field.text = value.text
print(value.text)
def set_list(self, text=" "):
# text defaults to blank space to not show any icons initally
# each OneLineListItem takes the pressed func on press
self.ids.container.clear_widgets() # refresh list
for icon in md_icons.keys():
# using casefold() to make the input case insensitve
if icon.startswith(text.casefold()):
self.ids.container.add_widget(
OneLineListItem(text=icon, on_press=self.pressed)
)
class Test(MDApp):
def build(self):
Builder.load_string(KV)
self.screen = ListSelect()
return self.screen
def on_start(self):
self.screen.set_list()
Test().run()
One catch: the search needs quite a long time to finish. If anyone has any hints on how to speed it up, you are very welcome!

Categories

Resources