community icon indicating copy to clipboard operation
community copied to clipboard

RecycleView Duplicateing User added content to blank text box.

Open dataforxyz opened this issue 1 year ago • 3 comments

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. image

dataforxyz avatar Aug 24 '24 17:08 dataforxyz

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()

dataforxyz avatar Aug 24 '24 18:08 dataforxyz

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()

dataforxyz avatar Aug 25 '24 18:08 dataforxyz

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()

ElliotGarbus avatar Aug 25 '24 21:08 ElliotGarbus

👋 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.

github-actions[bot] avatar Dec 25 '24 14:12 github-actions[bot]