Process all selected ListCtrl items at once

I want to be able to select multiple ListCtrl items and process them all at once. What is happening is that the wx.EVT_LIST_ITEM_SELECTED event is firing for each selected item so when I run the following code and select a range of items

import wx

class MyApp(wx.App):

    def OnInit(self):
        frame = Frame()
        return True

class Frame(wx.Frame):

    def __init__(self,parent=None,id=-1):

        wx.Frame.__init__(self, None, -1, 'Show Groups')
        self.Bind(wx.EVT_CLOSE,self.OnExit,self)

        screenw, screenh = wx.DisplaySize()

        self.panel = Panel(self)
        self.SetSize(self.panel.list.GetBestSize()[0], screenh - 30)
        self.Move(screenw-self.Size.width,0)
        self.Fit()        
        self.Show()

    def OnExit(self,event):

        self.Destroy()

class Panel(wx.Panel):

    def __init__(self, parent):

        wx.Panel.__init__(self, parent, -1)

        sizer = wx.BoxSizer(wx.VERTICAL)

        text  = "Time exists in so that everything doesn't happen all at once and space exists so that it doesn't all happen to you"

        self.list = wx.ListCtrl(self, -1, style=wx.LC_REPORT)
        self.list.InsertColumn(0, 'Name')
        self.list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelected)
        self.index = 0

        for word in text.split():
            self.add_line(word)

        sizer.Add(self.list, 1, wx.ALIGN_LEFT | wx.EXPAND, 0)
        self.SetSizer(sizer)          

    def OnSelected(self, evt):

        print("\nOnSelected", self.list.GetSelectedItemCount())

        item = self.list.GetFirstSelected()

        while item != -1:
            data = self.list.GetItem(item)
            print(data.Text)
            item = self.list.GetNextSelected(item)

    def add_line(self, line):

        self.list.InsertItem(self.index, line)
        self.index += 1

app = wx.App(False)
frame = Frame()
app.MainLoop()

I get the following output

>>> 
============================ RESTART: D:\test.py ============================

OnSelected 1
happen

OnSelected 2
happen
all

OnSelected 3
happen
all
at

OnSelected 4
happen
all
at
once

OnSelected 5
happen
all
at
once
and

OnSelected 6
happen
all
at
once
and
space

OnSelected 7
happen
all
at
once
and
space
exists

Is there an event I can use to only process the selected items when I have the entire list?

You could always handle your events delayed by calling a callback with wx.CallLater.
The callback checks whether it’s being called for the latest event. If not, it just returns.

This is a code snippet I’m using for a text control with a delay of 500ms, but for your purpose you need less:

    def _on_text(self, pending):
        # delayed effect
        if pending!=self._pending: return  # another call is pending
        self._on_change()

    def on_text(self, event):
        # callback will be delayed
        event.Skip()
        if self._ignore_events: return
        value = self.GetValue() # event.GetString()
        if value==self._previous_value:
            # handled from on_combobox already
            self._set_background_colour(wx.WHITE)
            return
        self._pending = value
        try:
            self._convert_string_to_value(self._pending)
            self._set_background_colour(wx.WHITE)
        except:
            self._set_background_colour(wx.RED)
        wx.CallLater(self.DELAY, self._on_text, self._pending)

I have absolutely no idea what that is doing.

Well, on an event, it will store the value to identify the last event:

self._pending = value

Then it triggers a delayed callback:

wx.CallLater(self.DELAY, self._on_text, self._pending)

The delayed callback does check whether it’s being called for the latest event. If not, it will return. If yes, it will do whatever needs to be done.

if pending!=self._pending: return  # another call is pending

AFAIK, there is no way to do this. I mean, the CallLater trick is funny, but it doesn’t really work in the general case. What if you select the first item and then you take your time before selecting the rest of the range? What if you control-select a bunch of non-consecutive items? What if the list is really long and it takes some scrolling to find all the items you want? What if you select a bunch of items, then you de-select some of them? How do you know for sure when the user has completed the selection?

I’m afraid that EVT_LIST_ITEM_SELECTED works better with an LC_SINGLE_SEL type of list. If you want a multiple selection list, perhaps you should give up using the selection event, and just add a “process selected” button somewhere in your gui.

I find it odd that it behaves like this. I had originally used a ListBox and it worked the way I wanted. I only changed to a ListCtrl so that I could change the font of selected items in order to indicate that they had been selected at least once. I used the wx.EVT_LISTBOX event.

If I change the code slightly to use wx.EVT_LIST_ITEM_FOCUSED then it works exactly as I want (and the same as it did with the ListBox control) without the need of the complicated callback.

import wx

class MyApp(wx.App):

    def OnInit(self):
        frame = Frame()
        return True

class Frame(wx.Frame):

    def __init__(self,parent=None,id=-1):

        wx.Frame.__init__(self, None, -1, 'Show Groups')
        self.Bind(wx.EVT_CLOSE,self.OnExit,self)

        screenw, screenh = wx.DisplaySize()

        self.panel = Panel(self)
        self.SetSize(self.panel.list.GetBestSize()[0], screenh - 30)
        self.Move(screenw-self.Size.width,0)
        self.Fit()        
        self.Show()

    def OnExit(self,event):

        self.Destroy()

class Panel(wx.Panel):

    def __init__(self, parent):

        wx.Panel.__init__(self, parent, -1)

        sizer = wx.BoxSizer(wx.VERTICAL)

        text  = "Time exists in so that everything doesn't happen all at once and space exists so that it doesn't all happen to you"

        self.list = wx.ListCtrl(self, -1, style=wx.LC_REPORT)
        self.list.InsertColumn(0, 'Name')

        self.list.Bind(wx.EVT_LIST_ITEM_FOCUSED,   self.OnFocused)

        self.index = 0

        for word in text.split():
            self.add_line(word)

        sizer.Add(self.list, 1, wx.ALIGN_LEFT | wx.EXPAND, 0)
        self.SetSizer(sizer)

    def OnFocused(self, evt):
        print('OnFocused')
        item = self.list.GetFirstSelected()

        while item != -1:
            data = self.list.GetItem(item)
            print('\t' + data.Text)
            item = self.list.GetNextSelected(item)

    def add_line(self, line):

        self.list.InsertItem(self.index, line)
        self.index += 1

app = wx.App(False)
frame = Frame()
app.MainLoop()

Yes, a focus event almost works… as long as you realize that “focused” doesn’t always mean “selected”.
Try this:
select a bunch of items (-> event fired)
control-deselect one of the items (-> event fired)
control-select again the same item you have just deselected (-> event NOT fired…, because the selection has changed but the focus has not)

But you’re right, a ListBox does have the notion of “selection changed”… Unfortunately a ListCtrl doesn’t work this way.
A cheap way to add this behaviour could be to check at idle time if the selection has changed - if so, fire a custom event. Something like this:

import wx
import wx.lib.newevent as newev

SelModifiedEvent, EVT_SELECTION_MODIFIED = newev.NewCommandEvent()

class MyListCtrl(wx.ListCtrl):
    def __init__(self, *a, **k):
        wx.ListCtrl.__init__(self, *a, **k)
        self.Bind(wx.EVT_IDLE, self.on_idle)
        self._selected = []

    def on_idle(self, event):
        event.Skip()
        sel = []
        item = self.GetFirstSelected()
        while item != -1:
            sel.append(item)
            item = self.GetNextSelected(item)
        if sel != self._selected:
            self._selected = sel
            e = SelModifiedEvent(self.GetId())
            wx.PostEvent(self.GetEventHandler(), e)

class MainFrame(wx.Frame): 
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)
        self.list = MyListCtrl(self, style=wx.LC_REPORT)
        self.list.InsertColumn(0, 'data')
        for n, val in enumerate('1 2 3 4 5 6'.split()):
            self.list.InsertItem(n, val)
        # catch the custom event to be notified of a changed selection
        self.list.Bind(EVT_SELECTION_MODIFIED, self.on_sel_modified)

    def on_sel_modified(self, event):
        print('selection has changed!')

if __name__ == '__main__':
    app = wx.App(False)
    MainFrame(None).Show()
    app.MainLoop()

That’s a logical action. Fortunately that won’t affect the operation of my app as I am unlikely to ever deselect an item (other than deselecting by making a different selection). Thanks for the response. I’ve made a note in my handler to remind me of this behaviour in case I forget (likely).

RE: wx.EVT_IDLE ok. That’s actually pretty nifty.