Problem
I've been messing around with Kivy's RecycleViews in hopes of creating a list builder for one of my projects. I am working out of the second example on Kivy docs' RecycleView page, as it is already almost what I am trying to create. For reference, the example contains a list where multiple items can be selected or unselected.
My main issue is that I have been unable to find any way to get any sort of list containing all of the selected items in the RecycleView. I thought at the very least, I could have a separate list in the RecycleView containing all of the selected items using the apply_selection() method in the SelectableLabel class, however I am unable to distinguish unselecting a SelectableLabel from moving the label outside of the RecycleView's view.
Code
listBuilder.py
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.datamodel import RecycleDataModel
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
class TestScreen(Screen):
''' Screen for testing stuff '''
def pressed(self):
print(f'Selected: {self.ids.rv.data}')
self.ids.rv.data.append({'text': '200'})
class RV(RecycleView):
''' Recycle View '''
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout, RecycleDataModel):
''' Adds selection and focus behaviour to the view. '''
def on_data_changed(self, **kwargs):
print('Data changed: ', kwargs)
super(SelectableRecycleBoxLayout, self).on_data_changed(**kwargs)
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
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]))
Builder.load_file('listBuilder.kv')
class MainApp(App):
def build(self):
return TestScreen()
if __name__ == '__main__':
MainApp().run()
listBuilder.kv
#:kivy 1.11
#:import ScrollEffect kivy.effects.scroll.ScrollEffect
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (0.2, 0.2, 0.2, 1) if self.selected else (0.5, 0.5, 0.5, 1)
Rectangle:
pos: self.pos
size: self.size
<RV>:
viewclass: 'SelectableLabel'
# Scroll effects
effect_cls: ScrollEffect # Disable overscroll
scroll_type: ['bars', 'content']
bar_width: dp(15)
scroll_wheel_distance: dp(90)
# Content of recycle view
SelectableRecycleBoxLayout:
default_size: None, dp(30)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
<TestScreen>:
name: 'test'
FloatLayout:
canvas:
Color:
rgba: 1,1,1,1
Rectangle:
pos: 0,0
size: self.width, self.height
# Container for recycle view
RV:
id: rv
size_hint: 0.3, 0.5
pos_hint: {'center_x': 0.3, 'center_y': 0.5}
Button:
text: 'Print selected'
size_hint: 0.2, 0.05
pos_hint: {'center_x': 0.8, 'center_y': 0.1}
on_release:
root.pressed()
For anyone wondering why I am using screens in this example--it's because this is test code for a larger program that uses screens.
I'm using Kivy 1.11.1 and Python 3.7.8
Any help is appreciated as I do not really understand yet fully grasp the RecycleView data models.
Thanks!
I found another solution as I started to delve deeper into my project and thought I'd share it here.
There is a built-in way to get the selected nodes; it's accessed through RecycleView.layout_manger.selected_nodes. It returns a list of the indices selected nodes, though it should be noted that they are not in numerical order but in the order that the nodes were selected.
Here are the changes I made to the original code using the new method:
RV class:
class RV(RecycleView):
''' Recycle View '''
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
def get_selected(self):
''' Returns list of selected nodes dicts '''
return [self.data[idx] for idx in self.layout_manager.selected_nodes]
If you just care about the indices, you don't necessarily need a method, but I figured it'd be nice to get the actual dicts.
The pressed method then looks like:
def pressed(self):
print('Selected:')
for d in self.ids.rv.get_selected():
('\t', d)
The main reason I opted to switch to this method is that the selected dictionary key does not correspond to the selected state of the node. In the program, I had to remove certain items from the list and the new items at the old indices become selected. It's a bit odd, but it makes more sense when thinking about the selection as a list of indices rather than the individual items being selected or not.
For anyone who is having trouble with other list items becoming selected after the original ones were removed, I found this helpful: https://www.reddit.com/r/kivy/comments/6b0pfp/dhjh7q4
If you add selected as a key in your RecycleView data, then you can get what you want like this:
class RV(RecycleView):
''' Recycle View '''
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x), 'selected': False} for x in range(100)]
Then, in the SelectableLabel class:
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected
# change selected in data
rv.data[index]['selected'] = self.selected
if is_selected:
print("selection changed to {0}".format(rv.data[index]))
else:
print("selection removed for {0}".format(rv.data[index]))
And, finally, you can assemble the list in the pressed() method:
def pressed(self):
print('Selected:')
rv = self.ids.rv
for d in rv.data:
if d['selected']:
print('\t', d)
Related
Sorry if this is a ridiculous question. I'm new to programming and so far I've been able to lurk my way through problems that have already been answered, but I can't find anything that answers this particular issue, so here I am with my first ever question to the Stack community. Yay!
I'm writing a fresh produce stock management app in Python/Kivy, using an SQLite database. I've got a RecycleView list with SelectableButton viewclass, and have it contained within a GridLayout.
I have a class method (getItems) within ItemList that extracts the item(string) and quantity(real) from the database, and adds it to a ListProperty variable (data_all), which is where the SelectableButton should in turn get its labels from.
In my example, I've commented out this method, and populated the list in the format that the getItems should do. Actually, as I write this, I wonder if there is a critical difference between the way SQLite stores REAL values, and how python stores float values.
Anyway, ultimately, I'm having trouble displaying the item next to it's quantity in the Recycle view list.
The closest I've come to having it work is to make a 2-column GridLayout, with the item name on the left as a Button, and the quantity on the right as a Label. However, when the list exceeds the bottom of the screen, they scroll independently of one another, as per example 1 below. This is not what I want as the items and quanitities should be next to one another.
Python:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.properties import ObjectProperty, BooleanProperty, ListProperty, StringProperty, ObjectProperty, NumericProperty, DictProperty
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
import string
#import sqlite3
class EditItemPopup(Popup):
obj = ObjectProperty(None)
obj_text = StringProperty("")
#The full code also includes quantity data to be displayed here, but that's irrelevant to the question
def __init__(self, obj, **kwargs):
super(EditItemPopup, self).__init__(**kwargs)
self.obj = obj
self.obj_text = self.obj.text
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableButton(RecycleDataViewBehavior, Button):
''' Add selection support to the Button '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableButton, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableButton, 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
def on_press(self):
popup = EditItemPopup(self)
popup.open()
def update_changes(self, txt):
self.text = txt
class ItemList(Widget):
'''User selects, adds, edits, and deletes items and quanitities'''
#this is how the data is extracted from the SQLite db
data_all = ListProperty(['Apples', 3.0, 'Bananas', 12.0, 'Lettuce', 16.5, 'Asparagus', 3.0])
def __init__(self, **kwargs):
super(ItemList, self).__init__(**kwargs)
# self.getItems()
return
#This method, and the invocation for it in the __init__ methd is how I would typically pull data from the SQLite db. I've left it commented here and open to constructive criticism
# def getItems(self):
# c.execute("SELECT item, quantity FROM itemTable ORDER BY item ASC")
# rows = c.fetchall()
# for row in rows:
# for col in row:
# self.data_all.append(col)
class ColStock(App):
def build(self):
return ItemList()
if __name__ == "__main__":
ColStock().run()
Kivy:
<EditItemPopup>:
title: "Edit Item"
size_hint: None, None
size: 400, 500
auto_dismiss: False
BoxLayout:
orientation: "vertical"
GridLayout:
size_hint_y: None
height: 50
cols: 3
Label:
text: root.obj_text
Button:
size_hint_y: None
height: 50
text: "OK, thanks"
on_press: root.dismiss()
<SelectableButton>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected==True else (0, 0, 0, 0)
Rectangle:
pos: self.pos
size: self.size
<ItemList>:
GridLayout:
size: root.width,root.height
cols: 2
RecycleView:
viewclass: 'SelectableButton'
data: [{'text': str(x)} for x in root.data_all[0::2]]
SelectableRecycleGridLayout:
cols: 1
default_size: None, dp(30)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
RecycleView:
viewclass: 'Label'
data: [{'text': str(x)} for x in root.data_all[1::2]]
RecycleGridLayout:
cols: 1
default_size: None, dp(30)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
I tried working around this by instead creating one GridLayout column, with the item name, a string seperater, and quantity all on the same button, and an rpartition("......qty: ') in the EditItemPopup init method to extract only the item name.
However, everything I've tried returns an error, usually a TypeError.
Python:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.properties import ObjectProperty, BooleanProperty, ListProperty, StringProperty, ObjectProperty, NumericProperty, DictProperty
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
import string
#import sqlite3
class EditItemPopup(Popup):
obj = ObjectProperty(None)
obj_text = StringProperty("")
item_name = StringProperty("")
item_qty = StringProperty("")
#The full code also includes quantity data to be displayed here, but that's irrelevant to the question
def __init__(self, obj, **kwargs):
super(EditItemPopup, self).__init__(**kwargs)
self.obj = obj
self.obj_text = self.obj.text
self.item_tuple = self.obj_text.rpartition("......qty: ")
self.item_name = self.item_tuple[0]
self.item_qty = self.item_tuple[2]
class SelectableRecycleGridLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleGridLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableButton(RecycleDataViewBehavior, Button):
''' Add selection support to the Button '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableButton, self).refresh_view_attrs(rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(SelectableButton, 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
def on_press(self):
popup = EditItemPopup(self)
popup.open()
def update_changes(self, txt):
self.text = txt
class ItemList(Widget):
'''User selects, adds, edits, and deletes items and quanitities'''
#this is how the data is extracted from the SQLite db
data_all = ListProperty(['Apples', 3.0, 'Bananas', 12.0, 'Lettuce', 16.5, 'Asparagus', 3.0])
def __init__(self, **kwargs):
super(ItemList, self).__init__(**kwargs)
# self.getItems()
return
#This method, and the invocation for it in the __init__ methd is how I would typically pull data from the SQLite db. I've left it commented here and open to constructive criticism
# def getItems(self):
# c.execute("SELECT item, quantity FROM itemTable ORDER BY item ASC")
# rows = c.fetchall()
# for row in rows:
# for col in row:
# self.data_all.append(col)
class ColStock(App):
def build(self):
return ItemList()
if __name__ == "__main__":
ColStock().run()
Kivy:
<EditItemPopup>:
title: "Edit Item"
size_hint: None, None
size: 400, 500
auto_dismiss: False
BoxLayout:
orientation: "vertical"
GridLayout:
size_hint_y: None
height: 50
cols: 3
Label:
text: root.item_name
Label:
text: root.item_qty
Button:
size_hint_y: None
height: 50
text: "OK, thanks"
on_press: root.dismiss()
<SelectableButton>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: (.0, 0.9, .1, .3) if self.selected==True else (0, 0, 0, 0)
Rectangle:
pos: self.pos
size: self.size
<ItemList>:
GridLayout:
size: root.width,root.height
cols: 1
RecycleView:
viewclass: 'SelectableButton'
#added self.obj_text.rpartition("......qty: ") for the popup window to extract item name as string
data: [{'text': str(x) + "......qty: " + "Quantity goes here"} for x in root.data_all[0::2]]
SelectableRecycleGridLayout:
cols: 1
default_size: None, dp(30)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
I've probably violated every convention in programming, and I know it's a hack, but I'd be happy with anything that works. Thank you in advance to anyone who can shed some light on this. :)
So, I've been stuck on this for about 2 or 3 weeks, and the day I ask for help on S.E is the day I discover a solution through trial and error that works for my needs.
I can use the following syntax in the kivy file which adds both item and quantity to each SelectableButton by iterating each pair in the data_all list:
data: [{'text': str(x) + "......qty: " + str(y)} for x,y in zip(root.data_all[0::2],root.data_all[1::2])]
I then add the following to the Popup's init method (python file) to extract only the item name for the label:
self.obj_tuple = self.obj_text.rpartition("......qty: ")
self.item_name = self.obj_tuple[0]
The self.item_name can then also be used to reference the correct row in the SQLite db.
Other suggestions are welcome of course, but I hope this helps someone who is facing similar issues.
Based on the code that is posted on kivy docs about Recycle View, how do I change the data? how to change the size of the selectable labels? and especially if I want to have more widget on the screen, how do set the position of the list to be in the bottom side of the screen?
I've tried to change the position with GridLayout, BoxLayout and nothing happens.
'''
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.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
<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
''')
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
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)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()
'''
Question 1
How do I change the data?
Answer
Change the data by updating self.data
RecycleView » data
The RecycleView is generatad by processing the data (i.e.
self.data), essentially a list of dicts, and uses these dicts to
generate instances of the viewclass as required.
Question 2
How to change the size of the selectable labels?
Answer
The size especially the height of each selectable widgets can be change in either Python script or kv file.
With reference to this example, the height can be changed by setting default_size: None, dp(30) at SelectableRecycleBoxLayout. As for the width of each selectable widgets, it will change according to the number columns in a row of data, self.data
With a SelectableRecycleGridLayout, one can specify the minimum width for each column by using cols_minimum
Question 3
If I want to have more widget on the screen, how to set the position
of the list to be in the bottom side of the screen?
Answer
Declare a root widget with inheritance of BoxLayout
Snippets
<RootWidget>:
orientation: 'vertical'
BoxLayout:
size_hint: 1, 0.8
BoxLayout:
size_hint: 1, 0.2
RV:
Output
Example
How to add vertical scroll in RecycleView
I am trying to create a simple attendance app.
When program is launched all labels are in deselected list
Expected behavior: when any label is selected data moves to the selected list and now selected labels are in the end of the joined list. Then RecycleView refreshes to display this change.
So I managed to make the data to move from one list to another, but I can't make RecycleView to refresh
I tried using ids but failed
I hope someone can help me. I think this is a routine problem for people who are knowledgeable, but for noobs like me it is not.
I am asking question on this site for the first time so sorry in advance
here is the code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty
from kivy.properties import ListProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from datetime import datetime
import kivy
from kivy.config import Config
Config.set('graphics', 'width', '300')
Config.set('graphics', 'height', '500')
importedlist = ['Novella Varela', 'Caroll Faircloth', 'Douglas Schissler',
'Rolande Hassell', 'Hayley Rivero', 'Niesha Dungy', 'Winfred Dejonge', 'Venetta Milum']
deselected_list = importedlist[:]
selected_list = []
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
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.
and add/remove items from lists
'''
self.selected = is_selected
if self.selected and self.text in deselected_list:
selected_list.append(self.text)
deselected_list.remove(self.text)
print(selected_list)
elif not self.selected and self.text in selected_list:
deselected_list.append(self.text)
selected_list.remove(self.text)
print(deselected_list)
class RV(RecycleView):
# this needs to be updated every time any label is selected or deselected
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = ([{'text': str(row)} for row in sorted(deselected_list)]
+ [{'text': str(row)} for row in sorted(selected_list)])
class Screen(BoxLayout):
now = datetime.now()
def nowdate(self):
return self.now.strftime('%d')
def nowmonth(self):
return self.now.strftime('%m')
def nowyear(self):
return self.now.strftime('%y')
def nowhour(self):
return self.now.strftime('%H')
def nowminute(self):
return self.now.strftime('%M')
Builder.load_string('''
#:import datetime datetime
<Screen>:
orientation: 'vertical'
BoxLayout:
size_hint_y: None
height: 30
TextInput:
id: date
text: root.nowdate()
TextInput:
id: date
text: root.nowmonth()
TextInput:
id: date
text: root.nowyear()
TextInput:
id: date
text: root.nowhour()
TextInput:
id: date
text: root.nowminute()
Button:
RV:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(45)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
Button:
size_hint_y: None
height: 30
<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
''')
class TestApp(App):
def build(self):
return Screen()
if __name__ == '__main__':
TestApp().run()
Alright, so I did actually stumble a few times trying to sort this out but I think I've got it. What I did was create two recycle views and a CustomLabel that has access to ButtonBehaviors so you can use 'on_press' instead of 'on_touch_down' (which propagates through the entire tree rather than the interacted with element).
Example Video
The py file:
from kivy.app import App
from kivy.uix.recycleview import RecycleView
from kivy.uix.label import Label
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.floatlayout import FloatLayout
# Create a Custom ButtonLabel that can use on_press
class ButtonLabel(ButtonBehavior, Label):
# App.get_running_app() lets us traverse all the way through our app from
# the very top, which allows us access to any id. In this case we are accessing
# the content of our selected_list_view of our app
#property
def selected_list_content(self):
return App.get_running_app().root.ids.selected_list.ids.content
# And in this case, we're accessing the content of our deselected_list_view
#property
def deselected_list_content(self):
return App.get_running_app().root.ids.deselected_list.ids.content
# This is our callback that our Label's will call when pressed
def change_location(self):
# If the label's parent is equal* the selected list, we remove the label from its
# parent, and then we add it to the other list
if self.parent == self.selected_list_content:
self.parent.remove_widget(self)
self.deselected_list_content.add_widget(self)
# If the label's parent is not the selected list, then it is the deselected list
# so we remove it from its parent and add it to the selected list
else:
self.parent.remove_widget(self)
self.selected_list_content.add_widget(self)
#* Note: Kivy uses weak references. This is why we use ==, and not 'is'
# We create a CustomRecycleView that we will define in our kv file
class CustomRecycleView(RecycleView):
pass
class MainWindow(FloatLayout):
pass
class ExampleApp(App):
def build(self):
# We create an instance of the MainWindow class, so we can access its id
# to import our list. Otherwise we would have nothing to add the list too
main_window = MainWindow()
importedlist = ['Novella Varela', 'Caroll Faircloth', 'Douglas Schissler',
'Rolande Hassell', 'Hayley Rivero', 'Niesha Dungy', 'Winfred Dejonge', 'Venetta Milum']
# We create a Label for each Name in our imported list, and then add it
# to the content of selected list as a default
# I'm sure you'll be importing our own lists in a different manner
# This is just for the example
for name in importedlist:
NameLabel = ButtonLabel(text=(name))
main_window.ids.selected_list.ids.content.add_widget(NameLabel)
return main_window
if __name__ == '__main__':
ExampleApp().run()
The kv file:
#:kivy 1.10.0
# We create a reference to the ButtonLabel class in our py file
<ButtonLabel>:
# We add our callback to our ButtonLabels on press event, on_press
on_press: root.change_location()
# We create a reference to our CustomRecycleView class in our py file
<CustomRecycleView>:
# We create a GridLayout to store all of the content in our RecycleView
GridLayout:
# We give it the id content so we can define the two property values in
# ButtonLabel class in the py file
id: content
size_hint_y: None
# One column because we want it to be vertical list list
cols: 1
# This set up, as well as size_hint_y set to None
# is so we can scroll vertically without issue
row_default_height: 60
height: self.minimum_height
<MainWindow>:
# We then create two instances of our CustomRecycleView, give them the ids
# referenced by the ButtonLabel methods as well as give them equal share of the
# screen space so they do not step on each others toes
# The canvas here is just for prototyping purposes to make sure they are the
# properly defined sizes. You can do whatever with them you would like tbh.
CustomRecycleView:
id: selected_list
size_hint: 1, .5
pos_hint: {'x': 0, 'y': .5}
canvas:
Color:
rgba: 100, 0, 0, .2
Rectangle:
size: self.size
pos: self.pos
CustomRecycleView:
id: deselected_list
size_hint: 1, .45
canvas:
Color:
rgba: 0, 0, 100, .2
Rectangle:
size: self.size
pos: self.pos
I've got some programs written in previous versions of Kivy that use ListViews. Using ListViews, it was quite easy to get the selected node via the adapter. However, it is much less clear how to do this with a RecycleView. Now, it is the case that rv.layout_manager.selected_nodes can be used to get the selected value, but there are also times where I'm interested in the actual node. Also, the following snippet can be used to generate the nodes, but apparently, they aren't the actual nodes in the RecycleView.
opts = rv.layout_manager.view_opts
for i in range(len(rv.data)):
s = rv.view_adapter.get_view(i, rv.data[i], opts[i]['viewclass'])
print s.text, s.selected
I'd be interested in finding a way of getting the selected node from the RecycleView.
Full code:
import random
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.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
#Aside: the code in the string would need to be indented back one tab, but it's like this for SO formatting
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
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
key_selection: "True"
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: False
touch_multiselect: True
touch_deselect_last: True
''')
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
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
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(random.random())} for x in range(20)]
class TestApp(App):
def build(self):
self.rv = RV()
self.rv.layout_manager.bind(selected_nodes=self.selectionChange)
return self.rv
def selectionChange(self, inst, val):
print inst, val
if __name__ == '__main__':
b = TestApp()
b.run()
please show us your code. in the rv.data you can add a parameter 'selected' = 0 when the node is not selected and 1 when it is this is an example:
class TestRecycleView(RecycleView):
def __init__(self, **kwargs):
super(TestRecycleView, self).__init__(**kwargs)
self.data = [{'name': 'test', 'ind':0,'selected': 0}]
class TestBox(RecycleDataBehaviour, BoxLayout):
def action(self, boss):
if boss.data[self.ind]['selected'] == 0:
boss.data[self.ind]['selected'] = 1
else:
boss.data[self.ind]['selected'] = 0
the kv:
<TestRecycleView>:
viewclass: 'TestBox'
RecycleBoxLayout:
default_size: None, 440
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
<TestBox>:
Button:
on_press: root.action(root.parent.parent)
if i understand well you want to print the selectedLabel value:
class TestApp(App):
def build(self):
self.rv = RV()
self.rv.layout_manager.bind(selected_nodes=self.selectionChange)
return self.rv
def selectionChange(self, inst, val):
print self.rv.data[val[0]]['text']
proof(image link)
I have a RecycleView widget that responds to touch input. I need each row on the RecycleView to take the user to a specific screen. For now I only have two screens setup.
Here is the Python code:
class Navigator(NavigationDrawer):
image_source = StringProperty('images/1canaa.jpg')
title = StringProperty('Navigation')
# This is the screen that is initiated when the app runs
class MainScreen(Screen):
pass
# This is the screen that is suppose to initiate when the first row is
# touched
class MapScreen(Screen):
pass
class Manager(ScreenManager):
main_screen_obj = ObjectProperty(None)
map_screen_obj = ObjectProperty(None)
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
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]))
if rv.data[index] == {'text': 'FIRST ROW'}:
Manager.current = 'mapScreen'
print('The evaluation was executed')
else:
print("selection removed for {0}".format(rv.data[index]))
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
Unidades = ['FIRST ROW', 'SECOND ROW', 'THIRD ROW', 'FORTH ROW']
self.data = [{'text': x} for x in Unidades]
class Main(App):
theme_cls = ThemeManager()
nav_drawer = ObjectProperty()
def build(self):
self.nav_drawer = Navigator()
return Manager()
if __name__ == '__main__':
Main().run()
If you look at the apply_selection method, which is in the SelectableLabel class, you will see that I have tried to sort this out by checking:
if the rv.data[index] == {'text': 'FIRST ROW'}:
Manager.current = 'mapScreen'
print('The evaluation was executed')
This did not work. Notice that I printed a message to check if the evaluation had happened, and it did. When I run the app I get the message: 'The evaluation was executed'. The user was not taken to the MapScreen though.
And here is the kv code:
#:import MapSource mapview.MapSource
#:import Toolbar kivymd.toolbar.Toolbar
#:import hex kivy.utils.get_color_from_hex
<SelectableLabel>:
# Draw a background to indicate selection
canvas.before:
Color:
rgba: hex('#867979') if self.selected else hex('#808080')
Rectangle:
pos: self.pos
size: self.size
<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: False
touch_multiselect: True
<MainScreen>:
BoxLayout:
orientation: 'vertical'
Label:
size_hint: None, None
height: 45
width: 100
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
text: '[size=40]Unidades de AssistĂȘncia[/size]'
color: hex('#676767')
markup: True
font_name: 'alex-brush.regular.ttf'
RV:
<MapScreen>:
BoxLayout:
Label:
text: 'MapScreen'
<Manager>:
id: screen_manager
main_screen_obj: main_screen
map_screen_obj: map_screen
MainScreen:
id: main_screen
name: 'mainScreen'
manager: screen_manager
MapScreen:
id: map_screen
name: 'mapScreen'
manager: screen_manager
So, what I am trying to accomplish here is very simple, or at least it should be. It all boils down to this: If the first row is touched, take the user to MapScreen... If the second row is touched, take the user to some other screen ...so on.
I hope this is not very confusing. Thanks for any help.
There are two ways to do this, off the top of my head.
One is to use the router module in the kivy garden:
https://github.com/kivy-garden/garden.router
The other one would be by passing a reference of the screenmanager to the widgets in the recycle view so that you can just say self.manager.current = self.text, but this means that you would have had to add all screens previously to the manager.
I would use the factory for this:
from kivy.factory import Factory
class MyScreenManager(ScreenManager):
def create_and_switch_screen(self, name):
# This creates an instance of the class name you passed
new_screen = getattr(Factory, name)()
self.switch_to(my_screen)
This way you can programatically create each screen and define them as separate classes.