Processing multi-select in wx.ListCtrl

Knock on wood! :joy:

a somewhat more standard solution: select & context

multiselect_1.py (1.8 KB)

You’re not the first one to come across this problem. See https://trac.wxwidgets.org/ticket/4541.

I ended up with a timer-based workaround exactly as Robin describes it.

Looks like the timer solution is the closest to what I want. Thanks. Conceptually I still think it is wrong to fire the event once for each selection on a shift-click but perhaps there are reasons why this is not the approach that was taken.

if you don’t mix shift/ctrl then kindly have a look at

multiselect_su.py (1.6 KB)

That’s an interesting approach. I think I’ll play around with it a bit. Thanks.

and if you want to start synchronizing this selecting/processing a bit then just throw in an EventBlocker

multiselect_su_b.py (2.0 KB)

and if the blocking takes too long, just thread it: a fast non-workaround…

multiselect_su_b_t.py (2.5 KB)

I think the last approach makes something that should be simple way too complex to be used although as an exercise it is interesting. A little explanation of how the code works would be useful. I’ve used threading but not blocking, and have no idea what’s going on.

Well, you didn’t say much about what you actually are going to achieve except that it be ‘rapid’ and since technology changes and software must adapt there are bound to be some bumps along the way (at screen selection there is sensitive, hyper sensitive and touch-tap-swipe etc)
here is a somewhat more robust version

multiselect_su_b_t.py (2.6 KB)

Staying on the ‘rapid’ path I don’t think there is any complexity so far, unless we are talking about the selection of classes you seem to like (which is, honestly, not quite as I would have done it):
fast selection means accurate GUI input and the processing has to be done in the background (maybe some plausies in-between)
so you may imagine real programming starts at the background (error handling, bookings etc) and threading was just meant for demo purpose)

(programming the error makes more fun than the boring rest)

What I was trying to achieve was in effect “one click generates one event.” With the scenario:

  • user clicks on item 3
  • user shift-clicks on item 7

it is reasonable to expect that step 1 will result in the selection and processing of item 3. After all there is no way to anticipate an upcoming extended selection. It is also reasonable to expect that step 2 will result in the selection and processing of items 3-7. Instead, step 2 results in the selection and processing (each by a separate event) of item 3, then items 3-4, then 3-5, then 3-6, then 3-7. I think Alan Cooper (The Essentials of User Interface Design) would agree that this is counter-intuitive and therefore wrong.

Yes, this could be alleviated by a two step process

  • select all desired items
  • right-click context menu to execute desired processing on selected items

but this then requires two steps to process a single selection.

Perhaps an example would help. Imagine you have a large number of photo images. Several thousand, in fact. The images are named as “base name ####.jpg”. You want to show only the images selected from a list of base names. You may want to show more than one set at a time. Clicking on one base name shows that set and hides all others. CTRL-clicking a base name skips the “hide previous selection” step. I have tried checking for CTRL and/or SHIFT down in the event handler but this typically causes the system to think the key is permanently in the pressed state, even outside the application. If it were not for this sticky behaviour it would be trivial to get the items processed the way that I want.

Well, I don’t know Alan Cooper (although I’m somewhat in front of him, age wise) and wx is doing exactly what you assume he fancies. But selecting is one thing and processing another (maybe the Chinese will change that: camera, brainprint etc)
try your procedure with this (too many printouts gives too many headaches)

multiselect_su_b_t_ac.py (2.6 KB)

The last code is doing what you described but I doubt it’s what you expected. So let’s throw in the breaks from the dungeons of the ivory tower in order to unify the racer with the purist (no AI so far)

multiselect_su_b_t_ac.py (3.2 KB)
and the DIY version
multiselect_su_b_t_ac_diy.py (2.9 KB)

if you test it carefully (and I hope no bugs pending) then you will see that there is only a difference on the single select: if the racer taps quickly CTRL he’ll live up to his name
(by the way, subclassing without customizing doesn’t make all that much sense unless the machine is too fast) :wink:

I think this is the simplest solution. It does not require timers, threads, or a context menu. I maintain the state variable self.extended which tracks whether an extended election is in progress (shift or control pressed). If True then an OnClick merely updates self.selected to include and new selections. If we transit from extended to non-extended then the OnClick event is called to process the selected items. I may have to play with it a bit to allow removal (ctrl-click) or previously selected items but this is 99%

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)

        self.panel = Panel(self)
        self.SetSize(200, 400)      
        self.Show()
        self.event = None
        
       
class Panel(wx.Panel):

    def __init__(self, parent):

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

        self.extended = False
        self.selected = set()
        
        self.list = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_NO_HEADER | wx.LC_REPORT)
        self.list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnClick)
        self.list.AppendColumn('',width=200)

        self.list.Bind(wx.EVT_KEY_DOWN, self.KeyDown)
        self.list.Bind(wx.EVT_KEY_UP,   self.KeyUp)

        names = 'one two three four five six seven eight nine ten eleven twelve'.split()

        for indx,name in enumerate(names):
            self.list.InsertItem(indx,name)

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

    def KeyDown(self, event):
        self.extended = event.ControlDown() or event.ShiftDown()
        event.Skip()

    def KeyUp(self, event):
        self.extended = event.ControlDown() or event.ShiftDown()

        if self.selected and not self.extended:
            self.OnClick(None)
        event.Skip()
        
    def OnClick(self, event):

        if event is None:
            print("process",self.selected)
            self.selected.clear()
            return
        
        if self.extended:   
            indx = -1
            while (indx := self.list.GetNextSelected(indx)) != -1:
                self.selected.add(indx)
        else:
            self.selected.clear()
            self.selected.add(self.list.GetFirstSelected())
            self.OnClick(None)        

       
if __name__ == '__main__':
    app = wx.App(False)
    frame = Frame()
    app.MainLoop()

OK. I think this is 100%

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)

        self.panel = Panel(self)
        self.SetSize(200, 400)      
        self.Show()
        self.event = None
        
       
class Panel(wx.Panel):

    def __init__(self, parent):

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

        self.extended = False
        
        self.list = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_NO_HEADER | wx.LC_REPORT)
        self.list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnClick)
        self.list.AppendColumn('',width=200)

        self.list.Bind(wx.EVT_KEY_DOWN, self.KeyDown)
        self.list.Bind(wx.EVT_KEY_UP,   self.KeyUp)

        names = 'zero one two three four five six seven eight nine ten eleven twelve'.split()

        for indx,name in enumerate(names):
            self.list.InsertItem(indx,name)

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

    def KeyDown(self, event):
        self.extended = event.ControlDown() or event.ShiftDown()
        event.Skip()

    def KeyUp(self, event):
        self.extended = event.ControlDown() or event.ShiftDown()

        if self.list.GetSelectedItemCount() and not self.extended:
            self.OnClick(None)
        event.Skip()
        
    def OnClick(self, event):

        if event is None:
            indx = -1
            while (indx := self.list.GetNextSelected(indx)) != -1:
                print(f'process {indx=}')
            return

        if not self.extended:       
            self.OnClick(None)        

       
if __name__ == '__main__':
    app = wx.App(False)
    frame = Frame()
    app.MainLoop()

Well, if that is the logic you wanted then congrats!
But I think it’s a very special one and requires a proper description:
I want to select 1 & 3 and have to press CTRL first! and then 1,3 for otherwise I get the 1 twice
If I now press SHIFT or CTRL accidentally 1, 3 is selected again and again (I hope there are no boockings involved!!!)
if this faux pas dose not happen and I now like to select 4 to 6 I get 3, 4, 5, 6
Ok, may be I have to read this book you suggested by Alan Cooper :dizzy_face:

I agree it’s still not ideal. But if the control triggered the OnClick event once for every click instead of once for every item in the selection a work around would be unnecessary. I’ll probably be playing with it for some time to fine tune it. Thanks for all the suggestions.

That’s why I always opt for the practice proven 80% solution!
If you ever find anything else please let us know, but till then have a jolly click-shift-click (and not shift-click-oh)
P.S.
Not every program qualifies for a work-around just because it works, but there should be added a cancel option as soon as the ‘Selection pending’ message pops up :ok_hand:

even the cancel can be quick: just hover over it…

my_fast_select.py (3.1 KB)

(on Friday 13th one shouldn’t get out of bed)

and the user is entitled to know what the machine is doing (or setting the status bar non intrusively)

my_fast_select.py (3.6 KB)