community
community copied to clipboard
RecycleView Duplicateing User added content to blank text box.
Software Versions
- Python: 3.10
- OS: Windows 10
- Kivy: 2.2.1, 2.3.0, master/5b28aaa
- Kivy installation method: pip
Describe the bug When creating new objects on the fly. if those objects in the rv are a text box with blank value user typed values can be duplicated but not actually duplicated. You can see them and interact with them in the text box but they don't show in the data. if you add something the double and the added show up in data.
Expected behavior I expect the new ones to also be blank. I think it thinks they should be the same but are not then it does some refresh and seems to fix it. But I cant figure out what does it because refresh_from_data, refresh_from_layout, and refresh_from_viewport don't change it. scrolling does sometimes and sometimes it sticks around weird.
To Reproduce Run the code. add something to the empty text box. Press ctrl J a few times. or use the add button.
Code and Logs and screenshots
from kivy.app import App
from kivy.config import Config
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.properties import BooleanProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
Builder.load_string(
'''#:import utils kivy.utils
<StatefulLabel>:
orientation: 'horizontal'
active: stored_state.active
size_hint_y: None
height: dp(50)
padding: dp(10)
spacing: dp(10)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#E0E0E0') if self.index % 2 == 0 else utils.get_color_from_hex('#F5F5F5')
Rectangle:
pos: self.pos
size: self.size
CheckBox:
id: stored_state
active: root.active
on_release: root.store_checkbox_state()
size_hint_x: None
width: dp(40)
TextInput:
id: text_input
text: root.text
on_text: root.store_text_state(self.text, 'text')
size_hint_x: 0.3
font_size: sp(18)
multiline: False
TextInput:
id: text_input_v
text: root.text_v
on_text: root.store_text_state(self.text, 'text_v')
size_hint_x: 0.3
font_size: sp(18)
multiline: False
Label:
id: generate_state
text: root.generated_state_text
size_hint_x: 0.3
font_size: sp(18)
color: utils.get_color_from_hex('#333333')
Button:
text: 'Delete'
size_hint_x: 0.2
on_press: root.delete_row()
<RV>:
viewclass: 'StatefulLabel'
RecycleBoxLayout:
default_size: None, dp(50)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#FFFFFF')
Rectangle:
pos: self.pos
size: self.size
'''
)
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
text = StringProperty()
text_v = StringProperty()
generated_state_text = StringProperty()
active = BooleanProperty()
index = 0
def refresh_view_attrs(self, rv, index, data):
self.index = index
if data['text'] == '0':
self.generated_state_text = "is zero"
elif int(data['text']) % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
print(data)
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
def store_checkbox_state(self):
rv = App.get_running_app().rv
rv.data[self.index]['active'] = self.active
def store_text_state(self, new_text, field_name):
rv = App.get_running_app().rv
rv.data[self.index][field_name] = new_text
self.update_generated_state(new_text)
def update_generated_state(self, text):
try:
num = int(text)
if num == 0:
self.generated_state_text = "is zero"
elif num % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
except ValueError:
self.generated_state_text = "not a number"
def delete_row(self):
rv = App.get_running_app().rv
rv.data.pop(self.index)
rv.refresh_from_data()
class RV(RecycleView, App):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x), 'active': False, 'text_v': ''} for x in range(3)]
App.get_running_app().rv = self
# Bind the keyboard
Window.bind(on_keyboard=self.on_keyboard)
def build(self):
layout = BoxLayout(orientation='vertical')
add_button = Button(text='Add Row', size_hint_y=None, height=50)
add_button.bind(on_press=lambda x: self.add_row())
layout.add_widget(add_button)
layout.add_widget(self)
return layout
def add_row(self):
new_index = len(self.data)
self.data.append({'text': str(new_index), 'active': False})
def on_keyboard(self, window, key, scancode, codepoint, modifier):
if key == 106 and 'ctrl' in modifier: # 106 is the key code for 'j'
Window.release_all_keyboards()
self.add_row()
return True # Indicates that the key was handled
return False # Indicates that the key was not handled
if __name__ == '__main__':
Config.set('kivy', 'keyboard_mode', 'system')
RV().run()
Additional context
Add any other context about the problem here.
This is an example where you can see a workaround. but its clear now its not just empty. its all duplicate text fields. they are duplicate because thats what rv do. but its not appropriate or not rechecking right for this usage.
from kivy.app import App
from kivy.config import Config
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.properties import BooleanProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
Builder.load_string(
'''#:import utils kivy.utils
<StatefulLabel>:
orientation: 'horizontal'
active: stored_state.active
size_hint_y: None
height: dp(50)
padding: dp(10)
spacing: dp(10)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#E0E0E0') if self.index % 2 == 0 else utils.get_color_from_hex('#F5F5F5')
Rectangle:
pos: self.pos
size: self.size
CheckBox:
id: stored_state
active: root.active
on_release: root.store_checkbox_state()
size_hint_x: None
width: dp(40)
TextInput:
id: text_input
text: root.text
on_text: root.store_text_state(self.text, 'text')
on_focus: root.on_text_focus(self, self.focus)
size_hint_x: 0.3
font_size: sp(18)
multiline: False
TextInput:
id: text_input_v
text: root.text_v
on_text: root.store_text_state(self.text, 'text_v')
on_focus: root.on_text_focus(self, self.focus)
size_hint_x: 0.3
font_size: sp(18)
multiline: False
Label:
id: generate_state
text: root.generated_state_text
size_hint_x: 0.3
font_size: sp(18)
color: utils.get_color_from_hex('#333333')
Button:
text: 'Delete'
size_hint_x: 0.2
on_press: root.delete_row()
<RV>:
viewclass: 'StatefulLabel'
RecycleBoxLayout:
default_size: None, dp(50)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#FFFFFF')
Rectangle:
pos: self.pos
size: self.size
'''
)
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
text = StringProperty()
text_v = StringProperty()
generated_state_text = StringProperty()
active = BooleanProperty()
index = 0
def refresh_view_attrs(self, rv, index, data):
self.index = index
if data['text'] == '0':
self.generated_state_text = "is zero"
elif int(data['text']) % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
print(data)
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
def store_checkbox_state(self):
rv = App.get_running_app().rv
rv.data[self.index]['active'] = self.active
def store_text_state(self, new_text, field_name):
rv = App.get_running_app().rv
rv.data[self.index][field_name] = new_text
self.update_generated_state(new_text)
def update_generated_state(self, text):
try:
num = int(text)
if num == 0:
self.generated_state_text = "is zero"
elif num % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
except ValueError:
self.generated_state_text = "not a number"
def delete_row(self):
rv = App.get_running_app().rv
rv.data.pop(self.index)
rv.refresh_from_data()
def on_text_focus(self, instance, focus):
if focus: # When the TextInput gains focus
if instance.text.strip() == '': # If the text is only spaces (or empty)
instance.text = '' # Clear the text
instance.focus = True # Ensure focus is maintained
class RV(RecycleView, App):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x), 'active': False, 'text_v': ' ' * x} for x in range(3)]
App.get_running_app().rv = self
# Bind the keyboard
Window.bind(on_keyboard=self.on_keyboard)
def build(self):
layout = BoxLayout(orientation='vertical')
add_button = Button(text='Add Row', size_hint_y=None, height=50)
add_button.bind(on_press=lambda x: self.add_row())
layout.add_widget(add_button)
layout.add_widget(self)
return layout
def add_row(self):
new_index = len(self.data)
self.data.append({'text': str(new_index), 'active': False, 'text_v': ' ' * new_index})
def on_keyboard(self, window, key, scancode, codepoint, modifier):
if key == 106 and 'ctrl' in modifier: # 106 is the key code for 'j'
Window.release_all_keyboards()
self.add_row()
return True # Indicates that the key was handled
return False # Indicates that the key was not handled
if __name__ == '__main__':
Config.set('kivy', 'keyboard_mode', 'system')
RV().run()
here is an example that shows what the 3 types of refresh does. same as before add somthing to a blank field and hit ctr jor add button a couple times and it will duplicate. refresh data sometimes fixes it. but mostly they just move it around.
from kivy.app import App
from kivy.config import Config
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.properties import BooleanProperty, StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
Builder.load_string(
'''#:import utils kivy.utils
<StatefulLabel>:
orientation: 'horizontal'
active: stored_state.active
size_hint_y: None
height: dp(50)
padding: dp(10)
spacing: dp(10)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#E0E0E0') if self.index % 2 == 0 else utils.get_color_from_hex('#F5F5F5')
Rectangle:
pos: self.pos
size: self.size
CheckBox:
id: stored_state
active: root.active
on_release: root.store_checkbox_state()
size_hint_x: None
width: dp(40)
TextInput:
id: text_input
text: root.text
on_text: root.store_text_state(self.text, 'text')
on_focus: root.on_text_focus(self, self.focus)
size_hint_x: 0.3
font_size: sp(18)
multiline: False
TextInput:
id: text_input_v
text: root.text_v
on_text: root.store_text_state(self.text, 'text_v')
on_focus: root.on_text_focus(self, self.focus)
size_hint_x: 0.3
font_size: sp(18)
multiline: False
Label:
id: generate_state
text: root.generated_state_text
size_hint_x: 0.3
font_size: sp(18)
color: utils.get_color_from_hex('#333333')
Button:
text: 'Delete'
size_hint_x: 0.2
on_press: root.delete_row()
<RV>:
viewclass: 'StatefulLabel'
RecycleBoxLayout:
default_size: None, dp(50)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
canvas.before:
Color:
rgba: utils.get_color_from_hex('#FFFFFF')
Rectangle:
pos: self.pos
size: self.size
'''
)
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
text = StringProperty()
text_v = StringProperty()
generated_state_text = StringProperty()
active = BooleanProperty()
index = 0
def refresh_view_attrs(self, rv, index, data):
self.index = index
if data['text'] == '0':
self.generated_state_text = "is zero"
elif int(data['text']) % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
print(data)
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
def store_checkbox_state(self):
rv = App.get_running_app().rv
rv.data[self.index]['active'] = self.active
def store_text_state(self, new_text, field_name):
rv = App.get_running_app().rv
rv.data[self.index][field_name] = new_text
self.update_generated_state(new_text)
def update_generated_state(self, text):
try:
num = int(text)
if num == 0:
self.generated_state_text = "is zero"
elif num % 2 == 1:
self.generated_state_text = "is odd"
else:
self.generated_state_text = "is even"
except ValueError:
self.generated_state_text = "not a number"
def delete_row(self):
rv = App.get_running_app().rv
rv.data.pop(self.index)
rv.refresh_from_data()
def on_text_focus(self, instance, focus):
if focus: # When the TextInput gains focus
if instance.text.strip() == '': # If the text is only spaces (or empty)
instance.text = '' # Clear the text
instance.focus = True # Ensure focus is maintained
class RV(RecycleView, App):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x), 'active': False, 'text_v': ''} for x in range(3)]
App.get_running_app().rv = self
# Bind the keyboard
Window.bind(on_keyboard=self.on_keyboard)
def build(self):
layout = BoxLayout(orientation='vertical')
button_layout = BoxLayout(size_hint_y=None, height=50)
add_button = Button(text='Add Row')
add_button.bind(on_press=lambda x: self.add_row())
button_layout.add_widget(add_button)
# Add the three refresh buttons
refresh_data_button = Button(text='Refresh Data')
refresh_data_button.bind(on_press=lambda x: self.refresh_from_data())
button_layout.add_widget(refresh_data_button)
refresh_layout_button = Button(text='Refresh Layout')
refresh_layout_button.bind(on_press=lambda x: self.refresh_from_layout())
button_layout.add_widget(refresh_layout_button)
refresh_viewport_button = Button(text='Refresh Viewport')
refresh_viewport_button.bind(on_press=lambda x: self.refresh_from_viewport())
button_layout.add_widget(refresh_viewport_button)
layout.add_widget(button_layout)
layout.add_widget(self)
return layout
def add_row(self):
new_index = len(self.data)
self.data.append({'text': str(new_index), 'active': False, 'text_v': ''})
def on_keyboard(self, window, key, scancode, codepoint, modifier):
if key == 106 and 'ctrl' in modifier: # 106 is the key code for 'j'
Window.release_all_keyboards()
self.add_row()
return True # Indicates that the key was handled
return False # Indicates that the key was not handled
if __name__ == '__main__':
Config.set('kivy', 'keyboard_mode', 'system')
RV().run()
Your post here is not a bug, but really a support question. In the future please take your support questions to one of the support forums: https://discord.com/invite/eT3cuQp https://groups.google.com/g/kivy-users
It helps to understand how RecycleView works:
Every time a widget is visible in the view, the visible widget will apply the list of attributes from the items in the data list, to that widget. Of course, the binding applies, so keeping a selected state in the widget doesn't work.
You want the (recycled) widget to be set/reset when the widget is used for another data item so you have to save that selected state outside of the widget.
One possible solution is to edit the items in data(the RecycleView data attribute), but that could trigger new dispatches and so reset which widgets displays which items, and cause trouble.
The preferred solution is to save the widget state to a different list property, and just make the widget lookup that property when the widget’s key is updated.
Here is an example that uses a separate list to store the state of the checkboxes. The selected property under RV holds the state of the list property.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.properties import StringProperty, ListProperty, BooleanProperty, NumericProperty
kv = """
<DataLine>: # this class, a BoxLayout holds one line of data.
size_hint_y: None
height: dp(30)
canvas:
Color:
rgba: 1, 1, 1, 1
Line:
points: self.x, self.y, self.right, self.y
# width: 2
CheckBox:
size_hint_x: None
width: dp(32)
active: app.root.ids.rv.selected[root.index] if not root.header else False
on_active: app.root.ids.rv.selected[root.index] = self.active
Label:
text: root.date
Label:
text: root.plate
Label:
text: root.work
Label:
text: root.price
BoxLayout:
orientation: 'vertical'
Heading:
size_hint_y: None
height: dp(30)
RV:
id: rv
viewclass: 'DataLine'
RecycleBoxLayout:
default_size: None, dp(30)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
BoxLayout:
size_hint_y: None
height: dp(48)
Button:
text: 'Clear checkboxes'
on_release: rv.clear_checks()
Button:
text: 'Print checkboxes'
on_release: rv.print_checks()
"""
class DataLine(RecycleDataViewBehavior, BoxLayout): # used to hold data from database
date = StringProperty()
plate = StringProperty()
work = StringProperty()
price = StringProperty()
header = BooleanProperty(False)
index = NumericProperty()
def refresh_view_attrs(self, rv, index, data):
"""capture the index of each line when the view updates, used to access checkbox state"""
self.index = index
return super().refresh_view_attrs(rv, index, data)
class Heading(BoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Use DataLine to create the heading
dl = DataLine(date='Date', plate='Plate Number', work='Work', price='Price', header=True)
self.add_widget(dl)
class RV(RecycleView):
selected = ListProperty()
def load_data(self, records):
# records is a list of tuples, each tuple is a db record
# convert the db records into a dict
# RV uses a list of dicts to populate the widgets
for r in records:
d = {'date': r[0], 'plate': r[1], 'work': r[2], 'price': r[3], 'header': False}
self.data.append(d) # the RecycleView Data List
self.selected.append(False) # the state of the checkboxes
def clear_checks(self):
for i, _ in enumerate(self.selected):
self.selected[i] = False
def print_checks(self):
for i, checked in enumerate(self.selected):
if checked:
print(self.data[i])
class ViewDBApp(App):
def build(self):
return Builder.load_string(kv)
def on_start(self):
# create mock data
data = [('2024', '123123', 'repair', str(i)) for i in range(100)]
self.root.ids.rv.load_data(data)
ViewDBApp().run()
👋 We use the issue tracker exclusively for bug reports and feature requests. However, this issue appears to be a support request. Please use our support channels to get help with the project.
If you're having trouble installing Kivy, make sure to check out the installation docs for Windows, Linux and macOS.
Let us know if this comment was made in error, and we'll be happy to reopen the issue.