= How to create a list control with drag and drop (Phoenix) =
'''Keywords :''' ListCtrl, Drag and drop.

<<TableOfContents>>

--------
= Demonstrating : =
__'''''Tested''' py3.x, wx4.x and Win10. ''__

Are you ready to use some samples ? ;)

Test, modify, correct, complete, improve and share your discoveries ! (!)

--------
== Sample one ==
{{attachment:img_sample_one.png}}

Here's how you can use drag-and-drop to reorder a simple list, or to move items from one list to another.

{{{#!python
# sample_one.py

"""

Drag and drop (DnD) demo with listctrl :
- Dragging of multiple selected items.
- Dropping on an empty list.
- Dropping of items on a list with a different number of columns.
- Dropping on a different applications.

"""

import pickle
import sys
from   random import choice
import wx

# class MyFrame
# class MyDragList
# class MyListDrop
# class MyApp

#---------------------------------------------------------------------------

items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof']

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id,
                          "Sample one",
                          size=(450, 295))

        #------------

        self.SetIcon(wx.Icon('icons/wxwin.ico'))
        self.SetMinSize((450, 295))

        #------------

        dl1 = MyDragList(self, style=wx.LC_LIST)
        dl1.SetBackgroundColour("#e6ffd0")

        dl2 = MyDragList(self, style=wx.LC_REPORT)
        dl2.InsertColumn(0, "Column - 0", wx.LIST_FORMAT_LEFT)
        dl2.InsertColumn(1, "Column - 1", wx.LIST_FORMAT_LEFT)
        dl2.InsertColumn(2, "Column - 2", wx.LIST_FORMAT_LEFT)
        dl2.SetBackgroundColour("#f0f0f0")

        maxs = -sys.maxsize - 1

        for item in items:
            dl1.InsertItem(maxs, item)
            idx = dl2.InsertItem(maxs, item)
            dl2.SetItem(idx, 1, choice(items))
            dl2.SetItem(idx, 2, choice(items))

        #------------

        sizer = wx.BoxSizer(wx.HORIZONTAL)

        sizer.Add(dl1, proportion=1, flag=wx.EXPAND)
        sizer.Add(dl2, proportion=1, flag=wx.EXPAND)

        self.SetSizer(sizer)
        self.Layout()

#---------------------------------------------------------------------------

class MyDragList(wx.ListCtrl):
    def __init__(self, *arg, **kw):
        wx.ListCtrl.__init__(self, *arg, **kw)

        #------------

        self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.StartDrag)

        #------------

        dt = MyListDrop(self)
        self.SetDropTarget(dt)

    #-----------------------------------------------------------------------

    def GetItemInfo(self, idx):
        """
        Collect all relevant data of a listitem, and put it in a list.
        """

        l = []
        l.append(idx) # We need the original index, so it is easier to eventualy delete it.
        l.append(self.GetItemData(idx)) # Itemdata.
        l.append(self.GetItemText(idx)) # Text first column.
        for i in range(1, self.GetColumnCount()): # Possible extra columns.
            l.append(self.GetItem(idx, i).GetText())
        return l


    def StartDrag(self, event):
        """
        Put together a data object for drag-and-drop _from_ this list.
        """

        l = []
        idx = -1
        while True: # Find all the selected items and put them in a list.
            idx = self.GetNextItem(idx, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
            if idx == -1:
                break
            l.append(self.GetItemInfo(idx))

        # Pickle the items list.
        itemdata = pickle.dumps(l, 1)
        # Create our own data format and use it
        # in a Custom data object.
        ldata = wx.CustomDataObject("ListCtrlItems")
        ldata.SetData(itemdata)
        # Now make a data object for the  item list.
        data = wx.DataObjectComposite()
        data.Add(ldata)

        # Create drop source and begin drag-and-drop.
        dropSource = wx.DropSource(self)
        dropSource.SetData(data)
        res = dropSource.DoDragDrop(flags=wx.Drag_DefaultMove)

        # If move, we want to remove the item from this list.
        if res == wx.DragMove:
            # It's possible we are dragging/dropping from this list to this list.
            # In which case, the index we are removing may have changed...

            # Find correct position.
            l.reverse() # Delete all the items, starting with the last item.
            for i in l:
                pos = self.FindItem(i[0], i[2])
                self.DeleteItem(pos)


    def Insert(self, x, y, seq):
        """
        Insert text at given x, y coordinates --- used with drag-and-drop.
        """

        # Find insertion point.
        index, flags = self.HitTest((x, y))

        if index == wx.NOT_FOUND: # Not clicked on an item.
            if flags & (wx.LIST_HITTEST_NOWHERE|wx.LIST_HITTEST_ABOVE|wx.LIST_HITTEST_BELOW): # Empty list or below last item.
                index = self.GetItemCount() # Append to end of list.
            elif self.GetItemCount() > 0:
                if y <= self.GetItemRect(0).y: # Clicked just above first item.
                    index = 0 # Append to top of list.
                else:
                    index = self.GetItemCount() + 1 # Append to end of list.
        else: # Clicked on an item.
            # Get bounding rectangle for the item the user is dropping over.
            rect = self.GetItemRect(index)

            # If the user is dropping into the lower half of the rect,
            # we want to insert _after_ this item.
            # Correct for the fact that there may be a heading involved.
            if y > rect.y - self.GetItemRect(0).y + rect.height/2:
                index += 1

        for i in seq: # Insert the item data.
            idx = self.InsertItem(index, i[2])
            self.SetItemData(idx, i[1])
            for j in range(1, self.GetColumnCount()):
                try: # Target list can have more columns than source.
                    self.SetItem(idx, j, i[2+j])
                except:
                    pass # Ignore the extra columns.
            index += 1

#---------------------------------------------------------------------------

class MyListDrop(wx.DropTarget):
    """
    Drop target for simple lists.
    """
    def __init__(self, source):
        """
        Arguments:
        source: source listctrl.
        """
        wx.DropTarget.__init__(self)

        #------------

        self.dv = source

        #------------

        # Specify the type of data we will accept.
        self.data = wx.CustomDataObject("ListCtrlItems")
        self.SetDataObject(self.data)

    #-----------------------------------------------------------------------

    # Called when OnDrop returns True.
    # We need to get the data and do something with it.
    def OnData(self, x, y, d):
        """
        ...
        """

        # Copy the data from the drag source to our data object.
        if self.GetData():
            # Convert it back to a list and give it to the viewer.
            ldata = self.data.GetData()
            l = pickle.loads(ldata)
            self.dv.Insert(x, y, l)

        # What is returned signals the source what to do
        # with the original data (move, copy, etc.)  In this
        # case we just return the suggested value given to us.
        return d

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame(parent=None, id=-1)
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
--------
== Sample two ==
{{attachment:img_sample_two.png}}

This striped drag list allows for dragging and dropping within itself while maintaining its appearance.

While not used in the example stub, it'll also stripe itself on an insert event (like from a pop-up file dialog) or on a delete event.

{{{#!python
# sample_two.py

"""

Drag and Drop with a striped drag list.

"""

import wx

# class MyFrame
# class MyDragListStriped
# class MyApp

#---------------------------------------------------------------------------

firstNameList = ["Ben", "Bruce", "Clark", "Dick", "Tom", "Jerry", "John"]
lastNameList  = ["Grimm","Wayne", "Kent", "Grayson", "Pete", "Black", "Martin"]
superNameList = ["30", "20", "40", "56", "26", "32", "89"]

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id,
                          "Sample two (drag list striped)",
                          size=(400, 200))

        #------------

        self.SetIcon(wx.Icon('icons/wxwin.ico'))
        self.SetMinSize((400, 200))

        #------------

        dls = MyDragListStriped(self, style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
        dls.InsertColumn(0, "First name", wx.LIST_FORMAT_LEFT, 125)
        dls.InsertColumn(1, "Last name", wx.LIST_FORMAT_LEFT, 125)
        dls.InsertColumn(2, "Age", wx.LIST_FORMAT_LEFT, 130)
        dls.SetBackgroundColour("#f0f0f0")

        for index in range(len(firstNameList)):
            dls.InsertItem(index, firstNameList[index])
            dls.SetItem(index, 1, lastNameList[index])
            dls.SetItem(index, 2, superNameList[index])

        #------------

        sizer = wx.BoxSizer(wx.HORIZONTAL)

        sizer.Add(dls, proportion=1, flag=wx.EXPAND)

        self.SetSizer(sizer)
        self.Layout()

        #------------

        dls._onStripe()

#---------------------------------------------------------------------------

class MyDragListStriped(wx.ListCtrl):
    def __init__(self, *arg, **kw):
        wx.ListCtrl.__init__(self, *arg, **kw)

        #------------

        self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._onDrag)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._onSelect)
        self.Bind(wx.EVT_LEFT_UP,self._onMouseUp)
        self.Bind(wx.EVT_LEFT_DOWN, self._onMouseDown)
        self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
        self.Bind(wx.EVT_ENTER_WINDOW, self._onEnterWindow)
        self.Bind(wx.EVT_LIST_INSERT_ITEM, self._onInsert)
        self.Bind(wx.EVT_LIST_DELETE_ITEM, self._onDelete)

        #------------
        # Variables.
        #------------
        self.IsInControl = True
        self.startIndex = -1
        self.dropIndex = -1
        self.IsDrag = False
        self.dragIndex = -1

    #-----------------------------------------------------------------------

    def _onLeaveWindow(self, event):
        """
        ...
        """

        self.IsInControl = False
        self.IsDrag = False
        event.Skip()


    def _onEnterWindow(self, event):
        """
        ...
        """

        self.IsInControl = True
        event.Skip()


    def _onDrag(self, event):
        """
        ...
        """

        CURSOR_ARROW = wx.Cursor('cursor/arrow.cur', wx.BITMAP_TYPE_CUR)
        self.SetCursor(wx.Cursor(CURSOR_ARROW))

        self.IsDrag = True
        self.dragIndex = event.Index
        event.Skip()
        pass


    def _onSelect(self, event):
        """
        ...
        """

        self.startIndex = event.Index
        event.Skip()


    def _onMouseUp(self, event):
        """
        Purpose : to generate a dropIndex.
        Process : check self.IsInControl, check self.IsDrag, HitTest, compare HitTest value
        The mouse can end up in 5 different places :
        - Outside the Control,
        - On itself,
        - Above its starting point and on another item,
        - Below its starting point and on another item,
        - Below its starting point and not on another item.
        """

        self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))

        if self.IsInControl == False:       # 1. Outside the control : Do Nothing.
            self.IsDrag = False
        else:                               # In control but not a drag event : Do Nothing.
            if self.IsDrag == False:
                pass
            else:                           # In control and is a drag event : Determine Location.
                self.hitIndex = self.HitTest(event.GetPosition())
                self.dropIndex = self.hitIndex[0]
                # Drop index indicates where the drop location is; what index number.
                #---------
                # Determine dropIndex and its validity.
                #--------
                if self.dropIndex == self.startIndex or self.dropIndex == -1:    # 2. On itself or below control : Do Nothing.
                    pass
                else:
                    #----------
                    # Now that dropIndex has been established do 3 things :
                    # 1. gather item data
                    # 2. delete item in list
                    # 3. insert item & it's data into the list at the new index
                    #----------
                    dropList = []         # Drop List is the list of field values from the list control.
                    thisItem = self.GetItem(self.startIndex)
                    for x in range(self.GetColumnCount()):
                        dropList.append(self.GetItem(self.startIndex, x).GetText())
                    thisItem.SetId(self.dropIndex)
                    self.DeleteItem(self.startIndex)
                    self.InsertItem(thisItem)
                    for x in range(self.GetColumnCount()):
                        self.SetItem(self.dropIndex, x, dropList[x])
            #------------
            # I don't know exactly why, but the mouse event MUST
            # call the stripe procedure if the control is to be successfully
            # striped. Every time it was only in the _onInsert, it failed on
            # dragging index 3 to the index 1 spot.
            #-------------
            # Furthermore, in the load button on the wxFrame that this lives in,
            # I had to call the _onStripe directly because it would occasionally fail
            # to stripe without it. You'll notice that this is present in the example stub.
            # Someone with more knowledge than I probably knows why...and how to fix it properly.
            #-------------
        self._onStripe()
        self.IsDrag = False
        event.Skip()


    def _onMouseDown(self, event):
        """
        ...
        """

        self.IsInControl = True
        event.Skip()


    def _onInsert(self, event):
        """
        Sequencing on a drop event is:
        wx.EVT_LIST_ITEM_SELECTED
        wx.EVT_LIST_BEGIN_DRAG
        wx.EVT_LEFT_UP
        wx.EVT_LIST_ITEM_SELECTED (at the new index)
        wx.EVT_LIST_INSERT_ITEM
        """

        # this call to onStripe catches any addition to the list; drag or not.
        self._onStripe()
        self.dragIndex = -1
        event.Skip()


    def _onDelete(self, event):
        """
        ...
        """

        self._onStripe()
        event.Skip()


    def _onStripe(self):
        """
        ...
        """

        if self.GetItemCount() > 0:
            for x in range(self.GetItemCount()):
                if x % 2 == 0:
                    self.SetItemBackgroundColour(x, wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT))
                else:
                    self.SetItemBackgroundColour(x, wx.WHITE)

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame(parent=None, id=-1)
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
--------
= Download source =
[[attachment:source.zip]]

--------
= Additional Information =
'''Link :'''

http://jak-o-shadows.users.sourceforge.net/python/wxpy/dblistctrl.html

http://wxpython-users.1045709.n5.nabble.com/Example-of-Database-Interaction-td2361801.html

http://www.kitebird.com/articles/pydbapi.html

https://dabodev.com/

https://www.pgadmin.org/download/

https://github.com/1966bc/pyggybank

https://sourceforge.net/projects/pyggybank/

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

--------
= Thanks to =
??? (sample_one.py coding), ??? (sample_two.py coding), the wxPython community...

--------
= About this page =
Date(d/m/y)     Person (bot)    Comments :

14/03/20 - Ecco (Created page and updated examples for wxPython Phoenix).

--------
= Comments =
- blah, blah, blah...