CheckBox Action Repeats in a KivyMD RecycleView Grid - python

When the checkbox for an item is clicked/unclicked in a recycleview grid, the click/unclick also automatically repeats for other data items in the grid. Why is this happening? The code below is a minimum working example. Thanks.
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty
from kivy.clock import Clock
from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox
Builder.load_string("""
<Check>:
<GridTile>:
SmartTile:
source: root.tile
size_hint_y: None
height: '150dp'
Check:
<GridScreen>:
name: 'grid_screen'
RV:
id: rv
viewclass: 'GridTile'
RecycleGridLayout:
cols: 2
size_hint_y: None
default_size: 1, dp(150)
default_size_hint: 1, None
height: self.minimum_height
""")
class GridTile(Screen):
tile = StringProperty('')
class GridScreen(Screen):
pass
class RV(RecycleView):
data = ListProperty('[]')
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.cell_data()
def cell_data(self):
self.data = [{"tile": 'The Beatles'} for i in range(41)]
class Check(SmartTile):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.add_checkbox)
def add_checkbox(self, interval):
app = MDApp.get_running_app()
self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
self.check.bind(active=app.on_checkbox_active)
self._box_overlay.add_widget(self.check)
class ThisApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(GridScreen(name='grid_screen'))
return self.sm
def on_checkbox_active(self, checkbox, value):
if value:
print('The checkbox', checkbox, 'is active', 'and', checkbox.state, 'state')
else:
print('The checkbox', checkbox, 'is inactive', 'and', checkbox.state, 'state')
if __name__ == "__main__":
ThisApp().run()

Here is a modified version of your original posted code. This version works, but there is some interaction between GridTile instances (when you click on one check box, another GridTile appears to refresh itself). I have only seen this interaction with KivyMd. Writing a similar app without KivyMD does not display that odd interaction.
from functools import partial
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty, ObjectProperty
from kivy.clock import Clock
from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox
Builder.load_string("""
<GridTile>:
SmartTile:
source: root.tile
size_hint_y: None
height: '150dp'
Check:
id: ck
root_ref: root # creat reference to containing GridTile
<GridScreen>:
name: 'grid_screen'
RV:
id: rv
viewclass: 'GridTile'
RecycleGridLayout:
cols: 2
size_hint_y: None
default_size: 1, dp(150)
default_size_hint: 1, None
height: self.minimum_height
""")
class GridTile(Screen):
# properties to be set in the rv.data
tile = StringProperty('')
index = NumericProperty(-1)
cb_state = StringProperty('normal')
def __init__(self, **kwargs):
super(GridTile, self).__init__(**kwargs)
self.bind(cb_state=self.set_cb_state) # bind the cb_state property to set the state of the MDCheckBox
def set_cb_state(self, gridtile, cb_state_value):
self.ids.ck.check.state = cb_state_value # actually set the state of the MDCheckBox
class GridScreen(Screen):
pass
class RV(RecycleView):
data = ListProperty('[]')
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.cell_data()
def cell_data(self):
self.data = [{"tile": 'The Beatles', "index": i, "cb_state": 'normal'} for i in range(41)]
class Check(SmartTile):
root_ref = ObjectProperty(None) # reference to the containing GridTile (set by kv)
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.add_checkbox)
def add_checkbox(self, interval):
app = MDApp.get_running_app()
self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
self.check.bind(on_press=partial(app.on_checkbox_press, self)) # bind to on_press to avoid possible looping when active is changed
self._box_overlay.add_widget(self.check)
class ThisApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(GridScreen(name='grid_screen'))
return self.sm
def on_checkbox_press(self, check, checkbox):
new_state = checkbox.state
# set checkbox state back to the default
checkbox.state = 'normal' # avoids setting checkbox state without data
rv = self.root.get_screen('grid_screen').ids.rv
rv.data[check.root_ref.index]['cb_state'] = new_state
rv.refresh_from_data() # set the state from data
if __name__ == "__main__":
ThisApp().run()
Th gist of the modifications is the adding of the index and cb_state properties to the GridTile class and to the data. The index property is just used as the index into the data when adjusting the data. And the cb_state is the state of the MDCheckbox. Since the MDCheckbox does not appear in the kv, there is no automatic binding if the cb_state property to the actual state of the MDChckbox, so that binding is explicitly created in the GridTile class. Also, the binding of the MDCheckbox to update the data is changed to bind to on_press, rather than on_active, since the active property will be changed by the RecycleView based on the data and could result in a looping effecet.

The RecycleView works by recycling a minimal number of instances of the viewclass, which is GridTile in your case. The RecycleView assigns properties to those instances of GridTile based on the entries in the data. If you change any properties of GridTile, or its children, that are not handled in the data, then the RecycleView is unaware of those changes and those changes remain in the recycled instances of GridTile. So, if you want the MDCheckBox state to be handled correctly, you must include it in your data as another property of GridTile. The fact that your MDCheckBox is not in your kv, makes this much more difficult to accomplish. This answers the why question.

Related

Instance of a custom class assigned to Kivy ObjectProperty doesn't update GUI

I am trying to use Kivy Properties to refresh GUI after property is changed. However, when I try to assign an instance of custom class to property it doesn't work. After modifying property GUI is not refreshed with new value.
MWE:
python file:
from typing import List
from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.screenmanager import Screen
class SomeStruct:
def __init__(self, atr1: List[int], atr2: str):
self.atr1 = atr1
self.atr2 = atr2
class Root(Screen):
prop1 = ObjectProperty(None)
prop2 = ObjectProperty(None)
def __init__(self, **kw):
super().__init__(**kw)
self.prop1 = SomeStruct(atr1=[1, 2, 3], atr2="test1")
self.prop2 = "control1"
def modify1(self):
self.prop1.atr1 = [3, 2, 1]
def modify2(self):
self.prop1.atr2 = "test2"
def modify3(self):
self.prop2 = "control2"
class Mwe(App):
pass
if __name__ == '__main__':
Mwe().run()
kv file:
Root:
BoxLayout:
orientation: 'vertical'
Button:
text: str(root.prop1.atr1)
on_press: root.modify1()
Button:
text: root.prop1.atr2
on_press: root.modify2()
Button:
text: root.prop2
on_press: root.modify3()
Button 1 and 2 change classe's varaibles but text on these buttons is not updated. Button 3 works as it should.
Any help would be aprreciated, I really want to avoid making every varaible from class a separate property.

CheckBox problems with Kivy using RecycleView

The minimum code is for a grid of images each with a checkbox using Kivy, RecycleView and RecycleGridLayout. The problems include: i) The selection/deselection of a checkbox doesn't display; 2) It is resetting the checkbox so a deselect appears to be a select - see output of the print() statements in on_checkbox_active and on_checkbox_press (code for both is included).
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty
Builder.load_string("""
<GridTile>:
AsyncImage:
source: root.tile
size_hint_y: None
height: '150dp'
CheckBox:
id: ck
root_ref: root.index # create reference to containing GridTile
# on_active: app.on_checkbox_active(self, self.active, self.state, self.root_ref)
on_press: app.on_checkbox_press(self, self.active, self.state, self.root_ref)
<GridScreen>:
name: 'grid_screen'
RV:
id: rv
viewclass: 'GridTile'
RecycleGridLayout:
cols: 2
size_hint_y: None
default_size: 1, dp(150)
default_size_hint: 1, None
height: self.minimum_height
""")
class GridTile(Screen):
# properties to be set in the rv.data
tile = StringProperty('')
index = NumericProperty(-1)
cb_state = StringProperty('normal')
class GridScreen(Screen):
pass
class RV(RecycleView):
data = ListProperty('[]')
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.cell_data()
def cell_data(self):
self.data = [{"tile": 'The Rolling Stones', "index": i, "cb_state": 'normal'} for i in range(41)]
class ThisApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(GridScreen(name='grid_screen'))
return self.sm
def on_checkbox_active(self, checkbox, active, state, root_ref):
if active:
print(active, state, root_ref)
else:
print(active, state, root_ref)
new_state = checkbox.state
# set checkbox state back to the default
checkbox.state = 'normal' # avoids setting checkbox state without data
rv = self.root.get_screen('grid_screen').ids.rv
rv.data[root_ref]['cb_state'] = new_state
rv.refresh_from_data() # set the state from data
def on_checkbox_press(self, checkbox, active, state, root_ref):
if active:
print(active, state, root_ref)
else:
print(active, state, root_ref)
new_state = checkbox.state
# set checkbox state back to the default
checkbox.state = 'normal' # avoids setting checkbox state without data
rv = self.root.get_screen('grid_screen').ids.rv
rv.data[root_ref]['cb_state'] = new_state
rv.refresh_from_data() # set the state from data
if __name__ == "__main__":
ThisApp().run()
You are just missing the code to set the CheckBox state when the cb_state property of GridTile gets changed by the RecycleView:
class GridTile(Screen):
# properties to be set in the rv.data
tile = StringProperty('')
index = NumericProperty(-1)
cb_state = StringProperty('normal')
# code to set CheckBox state based on cb_state property
# this could also be accomplished with a `bind`
def on_cb_state(self, grid_tile, new_state):
self.ids.ck.state = new_state
Min code with "on_active: app.on_checkbox_active(self, ...)" set in GridTile. The RecycleView enters an infinite loop when any checkbox is selected. The message in the continuous output is:
[CRITICAL] [Clock] Warning, too much iteration done before the next
frame. Check your code, or increase the Clock.max_iteration attribute
Is a Clock() function needed someplace?
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty
Builder.load_string("""
<GridTile>:
AsyncImage:
source: root.tile
size_hint_y: None
height: '150dp'
CheckBox:
id: ck
root_ref: root.index # create reference to containing GridTile
on_active: app.on_checkbox_active(self, self.active, self.state, self.root_ref)
# on_press: app.on_checkbox_press(self, self.active, self.state, self.root_ref)
<GridScreen>:
name: 'grid_screen'
RV:
id: rv
viewclass: 'GridTile'
RecycleGridLayout:
cols: 2
size_hint_y: None
default_size: 1, dp(150)
default_size_hint: 1, None
height: self.minimum_height
""")
class GridTile(Screen):
# properties to be set in the rv.data
tile = StringProperty('')
index = NumericProperty(-1)
cb_state = StringProperty('normal')
def __init__(self, **kwargs):
super(GridTile, self).__init__(**kwargs)
self.bind(cb_state=self.set_cb_state) # bind the cb_state property to set the state of the CheckBox
def set_cb_state(self, gridtile, cb_state_value):
self.ids.ck.state = cb_state_value # actually set the state of the CheckBox
class GridScreen(Screen):
pass
class RV(RecycleView):
data = ListProperty('[]')
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.cell_data()
def cell_data(self):
self.data = [{"tile": 'The Beatles', "index": i, "cb_state": 'normal'} for i in range(41)]
class ThisApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
self.sm = ScreenManager()
self.sm.add_widget(GridScreen(name='grid_screen'))
return self.sm
def on_checkbox_active(self, checkbox, active, state, root_ref):
if active:
print(active, state, root_ref)
else:
print(active, state, root_ref)
new_state = checkbox.state
# set checkbox state back to the default
checkbox.state = 'normal' # avoids setting checkbox state without data
rv = self.root.get_screen('grid_screen').ids.rv
rv.data[root_ref]['cb_state'] = new_state
rv.refresh_from_data() # set the state from data
def on_checkbox_press(self, checkbox, active, state, root_ref):
if active:
print(active, state, root_ref)
else:
print(active, state, root_ref)
new_state = checkbox.state
# set checkbox state back to the default
checkbox.state = 'normal' # avoids setting checkbox state without data
rv = self.root.get_screen('grid_screen').ids.rv
rv.data[root_ref]['cb_state'] = new_state
rv.refresh_from_data() # set the state from data
if __name__ == "__main__":
ThisApp().run()

Instantiate data class from dropdown menu options using on_touch_up

I would like to populate MyData with information gathered from dropdown menus that show up in a popup window with "on_touch_up" in AddTouch. That data includes the position of "on_touch_up", in addition to the dropdown data. I am able to print the position within the AddTouch class, but I am having a hard time getting the data further down in my script using (for example: print('from MyMainApp: {}'.format(MyData.pos))).
I am also unable to get "mainbutton" or "dropdown" to show up in a popup window.
Hacking around with this I came up with the following which works, but doesn't do what i need
.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.uix.dropdown import DropDown
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.graphics import Color, Rectangle
class AddTouch(Widget):
def __init__(self, **kwargs):
super(AddTouch, self).__init__(**kwargs)
with self.canvas:
Color(1, 0, 0, 0.5, mode="rgba")
self.rect = Rectangle(pos=(0, 0), size=(10, 10))
def on_touch_down(self, touch):
self.rect.pos = touch.pos
def on_touch_move(self, touch):
self.rect.pos = touch.pos
def on_touch_up(self, touch):
# final position
self.pos = touch.pos
print(self.pos)
class MyPopup(Popup):
def __init__(self, **kwargs):
super(MyPopup, self).__init__(**kwargs)
# create a main button
self.mainbutton = Button(text='Hello', size_hint=(None, None))
# create a dropdown with 10 buttons
self.dropdown = DropDown()
for index in range(10):
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
btn.bind(on_release=lambda btn: self.dropdown.select(btn.text))
self.dropdown.add_widget(btn)
self.mainbutton.bind(on_release=self.dropdown.open)
self.dropdown.bind(on_select=lambda instance, x: setattr(self.mainbutton, 'text', x))
class MyData:
def __init__(self, **kwargs):
super(MyData, self).__init__(**kwargs)
self.pos=AddTouch.pos
# using kivy screen for consistency
class MainWindow(Screen):
pass
class WindowManager(ScreenManager):
pass
kv = Builder.load_file("dropd.kv")
class MyMainApp(App):
def build(self):
return kv
print('from MyMainApp: {}'.format(MyData.pos))
if __name__ == "__main__":
MyMainApp().run()
.kv
WindowManager:
MainWindow:
<MainWindow>:
name: "main"
AddTouch:
on_touch_up:
#MyPopup gives 'MyPopup' is not defined, even if I add <MyPopup>: below
#root.MyPopup gives 'MainWindow' object has no attribute 'MyPopup'
I tried adding a simple dynamic Popup class in the .kv file based on this, but again it says 'MyPopup' is not defined:
.kv
AddTouch
on_touch_up:
MyPopup
<MyPopup#Popup>:
auto_dismiss: False
Button:
text: 'Close me!'
on_release: root.dismiss()
What am I missing (other than experience, ability, and general intelligence)?
In addition to adding the line:
self.content = self.mainbutton
to the MyPopup __init__() method, you can trigger the MyPopup creation by modifying your .kv file as:
#:import Factory kivy.factory.Factory
WindowManager:
MainWindow:
<MainWindow>:
name: "main"
AddTouch:
on_touch_up:
Factory.MyPopup().open()

The logic behind 'adapter'? (AttributeError: 'NoneType' object has no attribute 'adapter')

I'm trying to implement a code I found for a list that returns the item selected in kivy.
The code works as an indepedent app. But I can't figure out how it is working step by step to implement it within my own application (using mainly the kv lang).
from kivy.uix.modalview import ModalView
from kivy.uix.listview import ListView
from kivy.uix.gridlayout import GridLayout
from kivy.properties import StringProperty
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.factory import Factory
# Note the special nature of indentation in the adapter declaration, where
# the adapter: is on one line, then the value side must be given at one level
# of indentation.
Builder.load_string("""
#:import lv kivy.uix.listview
#:import la kivy.adapters.listadapter
<MainView>:
ListViewModal
<ListViewModal>:
list_view: list_view_id
size_hint: None,None
size: 400,400
ListView:
id: list_view_id
size_hint: .8,.8
adapter:
la.ListAdapter(
data=["Item #{0}".format(i) for i in xrange(10)],
selection_mode='single',
allow_empty_selection=False,
cls=lv.ListItemButton)
""")
class ListViewModal(ModalView):
selected_item = StringProperty('no selection')
list_view = ObjectProperty(None)
def __init__(self, **kwargs):
super(ListViewModal, self).__init__(**kwargs)
self.list_view.adapter.bind(on_selection_change=self.selection_changed)
# This is for the binding set up at instantiation, to the list adapter's
# special on_selection_change (bind to it, not to adapter.selection).
def selection_changed(self, *args):
print ' args when selection changes gets you the adapter', args
self.selected_item = args[0].selection[0].text
# This is to illustrate another type of binding. This time it is to this
# class's selected_item StringProperty (where the selected item text is set).
# See other examples of how bindings are set up between things. This one
# works because if you put on_ in front of a Kivy property name, a binding
# is set up for you automatically.
def on_selected_item(self, *args):
print ' args when a list property changes gets you the list property, and the changed item', args
print 'selected item text', args[1]
class MainView(GridLayout):
"""
Implementation of a ListView using the kv language.
"""
# def __init__(self, **kwargs):
# kwargs['cols'] = 1
# kwargs['size_hint'] = (1.0, 1.0)
# super(MainView, self).__init__(**kwargs)
# listview_modal = ListViewModal()
# self.add_widget(listview_modal)
if __name__ == '__main__':
from kivy.base import runTouchApp
runTouchApp(MainView(width=800))from kivy.uix.modalview import ModalView
from kivy.uix.listview import ListView
from kivy.uix.gridlayout import GridLayout
from kivy.properties import StringProperty
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.factory import Factory
# Note the special nature of indentation in the adapter declaration, where
# the adapter: is on one line, then the value side must be given at one level
# of indentation.
Builder.load_string("""
#:import lv kivy.uix.listview
#:import la kivy.adapters.listadapter
<MainView>:
ListViewModal
<ListViewModal>:
list_view: list_view_id
size_hint: None,None
size: 400,400
ListView:
id: list_view_id
size_hint: .8,.8
adapter:
la.ListAdapter(
data=["Item #{0}".format(i) for i in xrange(10)],
selection_mode='single',
allow_empty_selection=False,
cls=lv.ListItemButton)
""")
class ListViewModal(ModalView):
selected_item = StringProperty('no selection')
list_view = ObjectProperty()
def __init__(self, **kwargs):
super(ListViewModal, self).__init__(**kwargs)
self.list_view.adapter.bind(on_selection_change=self.selection_changed)
# This is for the binding set up at instantiation, to the list adapter's
# special on_selection_change (bind to it, not to adapter.selection).
def selection_changed(self, *args):
print ' args when selection changes gets you the adapter', args
self.selected_item = args[0].selection[0].text
# This is to illustrate another type of binding. This time it is to this
# class's selected_item StringProperty (where the selected item text is set).
# See other examples of how bindings are set up between things. This one
# works because if you put on_ in front of a Kivy property name, a binding
# is set up for you automatically.
def on_selected_item(self, *args):
print ' args when a list property changes gets you the list property, and the changed item', args
print 'selected item text', args[1]
class MainView(GridLayout):
"""
Implementation of a ListView using the kv language.
"""
pass
# def __init__(self, **kwargs):
# kwargs['cols'] = 1
# kwargs['size_hint'] = (1.0, 1.0)
# super(MainView, self).__init__(**kwargs)
# listview_modal = ListViewModal()
# self.add_widget(listview_modal)
if __name__ == '__main__':
from kivy.base import runTouchApp
runTouchApp(MainView(width=800))
It raises me this error:
"AttributeError: 'NoneType' object has no attribute 'adapter'"
I have read the documentation about 'adapter' but it's pretty new to me and I can't figure out why do I need an adapter for the list to return a result?

Kivy multiple selection with checkboxes

I'm trying to create a view using Kivy that has a list of options that are all selected by default, and the user can choose to deselect some entries (by clicking on the checkbox or anywhere on the row).
Clicking on the label part of the row item works, but I noticed that clicking on the checkbox doesn't change the selection which I can't work out how to solve (I tried a few different state bindings, I left them commented out in the example code)
Here is a quick example showing what I've tried.
from kivy.app import App
from kivy.properties import StringProperty, ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.selectableview import SelectableView
from kivy.uix.togglebutton import ToggleButtonBehavior
from kivy.adapters.models import SelectableDataItem
from kivy.lang import Builder
Builder.load_string("""
#: import ListAdapter kivy.adapters.listadapter.ListAdapter
#: import Factory kivy.factory.Factory
<MyListItem>:
height: 50
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
BoxLayout:
spacing: 10
CheckBox:
on_state: root.is_selected = args[1] == "down"
state: "down" if root.is_selected else "normal"
# on_state: root.state = args[1]
# state: root.state
Label:
text: root.name
<Page>:
orientation: "vertical"
ListView:
id: LV
adapter: ListAdapter(data=root.data, cls=Factory.MyListItem, args_converter=root.args_converter, selection_mode="multiple", propagate_selection_to_data=True)
Button:
size_hint_y: None
text: "print selection"
on_press: print(LV.adapter.selection)
""")
class MyListItem(ToggleButtonBehavior, SelectableView, BoxLayout):
name = StringProperty()
def __repr__(self):
return "%s(name=%r)" % (type(self).__name__, self.name)
def on_state(self, me, state):
print me, state
if state == "down":
self.select()
else:
self.deselect()
# self.is_selected = state == "down"
class DataItem(SelectableDataItem):
def __init__(self, name, **kwargs):
super(DataItem, self).__init__(**kwargs)
self.name = name
def __repr__(self):
return "%s(name=%r, is_selected=%r)" % (type(self).__name__, self.name, self.is_selected)
class Page(BoxLayout):
data = ListProperty()
def __init__(self, **kwargs):
super(Page, self).__init__(**kwargs)
self.data = [DataItem("Item {}".format(i), is_selected=True) for i in range(10)]
def args_converter(self, index, data_item):
return {
"index": index,
"name": data_item.name,
}
class ExampleApp(App):
def build(self):
return Page()
if __name__ == "__main__":
ExampleApp().run()
I'm using Kivy v1.9.1-dev
Edit: I worked out how to get all the entries pre-selected, I've updated the code and took that part of the question out.
Just in case someone else has the the question I point to the right url:
You should consider the new RecycleView, which has all the functionality you request. Look here for a sample: Kivy: alternative to deprecated features

Categories