pythonkivydrag-and-dropkivy-recycleview

How to get target RecycleView's item by x/y pos, e.g. in on_drop_file event?


Scenario: I've a window with 3 columns, in the 2nd is a RecycleView with 20 items. When the user drops an item onto the RecycleView, I want to get the corresponding data-item.

All I need is to tell, onto which RecycleView's item was a file dropped. The same would go for a on_touch event, when dispatched at the parent's level; of course, the item can have its own on_dispatch, but the on_file_drop is bound at the Window class itself, so I have to drill-down which item in the RV is hit.

Here is the demo-code:


<PageThumb>:
    size_hint: 1, None
    orientation: 'vertical'
    Image:
        id: imgThumbnail
        size_hint: 1, None
        height: 300
    Label:
        id: lblPageNr
        size_hint: 1, None
        height: 20

GridLayout:
    cols: 3
    id: mainGrid
    BoxLayout:
        size_hint: 0.4, 1
        orientation: 'vertical'
        Label:
            text: '1st column'
    RecycleView:
        size_hint: 0.2, 1
        id: rvMain
        viewclass: 'PageThumb'
        RecycleBoxLayout:
            padding: 5
            spacing: 20
            default_size: None, 320
            default_size_hint: 1, None
            size_hint_y: None
            height: self.minimum_height
            orientation: 'vertical'
    BoxLayout:
        size_hint: 0.4, 1
        id: layoutPreview
        Label:
            text: '3rd column'


from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import NumericProperty

class PageThumb(BoxLayout):
    page_nr = NumericProperty(None)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
    
    def on_page_nr(self, obj, page_nr):
        obj.ids.lblPageNr.text = str(page_nr+1)

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            # touch is inside this widget
            app = App.get_running_app()
            print(f"touched page_nr={self.page_nr}")

class DemoApp(App):

    def getSystemSizeFromScaledSize(self, win, scaledX, scaledY):
        scaleX = win.size[0] / win.system_size[0]
        scaleY = win.size[1] / win.system_size[1]

        systemX = scaledX * scaleX;
        systemY = scaledY * scaleY;
    
        return (systemX, systemY)

    def on_drop_file(self, win, bFileName, x, y):
  
        fileName = bFileName.decode("utf-8")

        xS, yS = self.getSystemSizeFromScaledSize(win, x, y)

        if self.root.ids.rvMain.collide_point(xS, yS):
            yL = self.root.ids.rvMain.layout_manager.size[1] - yS

            # pos = self.root.ids.rvMain.to_local(xS, yL)
            ndx = self.root.ids.rvMain.layout_manager.get_view_index_at(pos=(xS, yL))

            page = list(self.root.ids.rvMain.layout_manager.view_indices)[ndx]

            print(f"d&d ndx={ndx} page_nr={page.page_nr}")
    
    def build(self):
        from kivy.core.window import Window
        Window.bind(on_drop_file = self.on_drop_file)

        pages = ({"page_nr": i} for i in range(0, 20))
        self.root.ids.rvMain.data = pages

DemoApp().run()

Questions:

  1. How to get the corresponding item from the RV based on the screen's x/y got in on_file_drop?
  2. Is my collide-detection conversion based on the resolution-density ok like this? I'm testing only under Windows, maybe it's needed only here? I didn't find any hints for this in the kivy's docs/APIs/sources

My analysis so far:

  1. The problem boils down to handle the on_drop_file event, which gets the screen's x/y. I've realized (despite the doc/examples don't show this) that I must convert the screen's x/y using the resolution density: My Windows' desktop uses 250% scaling and other event-handlers, like on_touch, need the density multiplication for correct detection in collide_point().

  2. I think I must apply this density calculation also inside the layout_manager.get_view_index_at(pos). When I look into the layout_manager.size, I see a huge y value, suggesting it covers all the underlying data, it's a bit bigger than 20 (items) x 320 (height of 1 item), but it's unfortunately the same small value. I've tried all the to_local()/to_widget() combinations of RecycleView, it's .layout_manager and that like - but all trials got me rather small values, although to get the item's index 0, I need to call the layout_manager.get_view_index_at() with something like (0, 6790) and not like (0, 10).

  3. I've tried to play with the layout_manager.view_indices and children[..] etc. with no luck.

TY :)!


Solution

  • Here is a hack that seems to work. I added an id to your RecycleBoxLayout:

    RecycleView:
        size_hint: 0.2, 1
        id: rvMain
        viewclass: 'PageThumb'
        RecycleBoxLayout:
            id: rvBox
    

    Then, I use that in the on_drop_file() method:

    def on_drop_file(self, win, bFileName, x, y):
        fileName = bFileName.decode("utf-8")
    
        rvbox = self.root.ids.rvBox
        x1, y1 = rvbox.to_widget(x, self.root.top - y)
    
        for w in rvbox.walk():
            if isinstance(w, PageThumb):
                if w.collide_point(x1, y1):
                    print('\tdropped', fileName, 'on', w.ids.lblPageNr.text)
                    break
    

    The coordinate transform methods are difficult to comprehend, and I believe my code will only work for your specific widget tree.