ListCtrl - extended selection after right-click

This question is about extended selection using the Shift key in a ListCtrl that is configured in Report view with multiple selection enabled.

If you left-click on an item in a ListCtrl with no modifier keys depressed, the item is selected and any items that were previously selected are deselected. If you left-click on an item with a Ctrl key depressed, you toggle its selection state without deselecting any other items that were previously selected. If you left-click on an item with a Shift key depressed, it will be selected, along with all the other items between it and the item on which the last left-click was made. Holding down a Shift key and pressing the Up or Down arrow keys also extends the selection.

In my application I have programmed a ListCtrl to display a context menu when you right click on an item. If the item was not previously selected then it gets set as the only selected item, then menu options are configured accordingly. The problem I have is that, if I then Shift + left-click on another item in the ListCtrl, the start point for the extended selection is not the item that was selected by the right-click, but whichever item that had been left-clicked before it.

I have been looking to see if there is a way to programmatically override the start point for the extended selection. I did wonder if setting the focus along with the selection, using the following statements, would work.

SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED
self.list_ctrl.SetItemState(item, SEL_FOC, SEL_FOC)

However, that made no difference. I also tried using an UltimateListCtrl, but that behaved the same way as the ListCtrl.

Here is a simple example that can be used to demonstrate the issue:

import sys
import wx

DATA = [
    ("A", "Alpha"), ("B", "Bravo"), ("C", "Charlie"), ("D", "Delta"),
    ("E", "Echo"), ("F", "Foxtrot"), ("G", "Golf"), ("H", "Hotel"),
    ("I", "India"), ("J", "Juliet"), ("K", "Kilo"), ("L", "Lima"),
    ("M", "Mike"), ("N", "November"), ("O", "Oscar"), ("P", "Papa"),
    ("Q", "Quebec"), ("R", "Romeo"), ("S", "Sierra"), ("T", "Tango"),
    ("U", "Uniform"), ("V", "Victor"), ("W", "Whisky"), ("X", "X-ray"),
    ("Y", "Yankee"), ("Z", "Zulu")
]

class ListFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((210, 620))
        self.SetTitle("Test ListCtrl selections")
        self.main_panel = wx.Panel(self, wx.ID_ANY)
        main_sizer = wx.BoxSizer(wx.VERTICAL)

        self.list_ctrl = wx.ListCtrl(self.main_panel, wx.ID_ANY, style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES)
        self.list_ctrl.AppendColumn("Letter", format=wx.LIST_FORMAT_LEFT, width=-1)
        self.list_ctrl.AppendColumn("Word", format=wx.LIST_FORMAT_LEFT, width=-1)

        for letter, word in DATA:
            index = self.list_ctrl.InsertItem(sys.maxsize, letter)
            self.list_ctrl.SetItem(index, 1, word)

        self.list_ctrl.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
        self.list_ctrl.Bind(wx.EVT_RIGHT_UP,   self.OnRightUp)

        self._preparePopupMenu()

        main_sizer.Add(self.list_ctrl, 1, wx.EXPAND, 0)
        self.main_panel.SetSizer(main_sizer)
        self.Layout()


    def _preparePopupMenu(self):
        """Prepare settings for the list control's popup menu. """

        self.popup_edit_id = wx.NewIdRef()
        self.popup_print_id = wx.NewIdRef()
        self.Bind(wx.EVT_MENU, self.OnPopupEdit,   id=self.popup_edit_id)
        self.Bind(wx.EVT_MENU, self.OnPopupPrint, id=self.popup_print_id)


    def deselectAllRecords(self):
        """Deselect all records in the list ctrl. """

        item = self.list_ctrl.GetFirstSelected()
        while item != wx.NOT_FOUND:
            self.list_ctrl.Select(item, on=False)
            item = self.list_ctrl.GetNextSelected(item)


    def popupContextMenu(self):
        num_selected = self.list_ctrl.GetSelectedItemCount()
        if num_selected == 0:
            return

        edit_enabled = num_selected == 1
        popup_menu = wx.Menu()
        popup_menu.Append(self.popup_edit_id, "Edit")
        popup_menu.Enable(self.popup_edit_id, edit_enabled)
        popup_menu.Append(self.popup_print_id, "Print selections")
        self.PopupMenu(popup_menu)
        popup_menu.Destroy()


    def OnPopupPrint(self, _event):
        words = []
        index = self.list_ctrl.GetFirstSelected()
        while index != wx.NOT_FOUND:
            words.append(self.list_ctrl.GetItemText(index, 1))
            index = self.list_ctrl.GetNextSelected(index)
        print(", ".join(words))


    def OnPopupEdit(self, _event):
        if self.list_ctrl.GetSelectedItemCount() != 1:
            return

        index = self.list_ctrl.GetFirstSelected()
        word = self.list_ctrl.GetItemText(index, 1)
        dlg = wx.TextEntryDialog(self, "Edit selected word:", 'Edit')
        dlg.SetValue(word)
        result = dlg.ShowModal()
        if result == wx.ID_OK:
            new_word = dlg.GetValue()
            self.list_ctrl.SetItem(index, 1, new_word)


    def OnRightDown(self, event):
        x = event.GetX()
        y = event.GetY()
        item, flags = self.list_ctrl.HitTest((x, y))

        if item != wx.NOT_FOUND and flags & wx.LIST_HITTEST_ONITEM:
            if not self.list_ctrl.IsSelected(item):
                # Make the item the only selected one
                self.deselectAllRecords()
                self.list_ctrl.Select(item)

    def OnRightUp(self, _event):
        self.popupContextMenu()


class MyApp(wx.App):
    def OnInit(self):
        self.frame = ListFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

I am using Python 3.8.10 + wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 + Linux Mint 20.2

Does anyone know how to programmatically override the start point of an extended selection in a ListCtrl?

I would check the MouseState and if not LeftIsDown then deselect the item

Hi, Richard

The code didn’t work on my Windows 10 PC and should be changed as follows

@@ -27,2 +27,2 @@ class ListFrame(wx.Frame):
-        for letter, word in DATA:
-            index = self.list_ctrl.InsertItem(sys.maxsize, letter)
+        for j, (letter, word) in enumerate(DATA):
+            index = self.list_ctrl.InsertItem(j, letter)

ListCtrl seems to remember the last left-clicked item, but there seems to be no such attribute or get method (probably glitch?). How about adding the followings to OnRightDown

    def OnRightDown(self, event):
        ...
        wx.UIActionSimulator().MouseClick(wx.MOUSE_BTN_LEFT)

When you press the right button, you can feel as if you also pressed the left button.

Thanks Kazuya & Georg, I will check out your suggestions.

Edit: I have decided to go with calling wx.UIActionSimulator().MouseClick(wx.MOUSE_BTN_LEFT) in OnRightDown(). It does exactly what I need. Thanks again!

I don’t think there is a glitch at all: right click selects and that can be deselected by Select(…, 0), which can be tested by GetSelectedItemCount etc, thus the internals of wx are ok

however, the underlying control provides a multi selection and that always starts with a left click, which will deselect a possible previous right click selection

confusion usually erupts when jumping into a multi selection with shift or control

1 Like

You are right! The action I saw was the customized one.

I checked the default Listctrl actions in this opportunity. I borrow the code of @RichardT here

import sys
import wx

DATA = [
    ("A", "Alpha"), ("B", "Bravo"), ("C", "Charlie"), ("D", "Delta"),
    ("E", "Echo"), ("F", "Foxtrot"), ("G", "Golf"), ("H", "Hotel"),
    ("I", "India"), ("J", "Juliet"), ("K", "Kilo"), ("L", "Lima"),
    ("M", "Mike"), ("N", "November"), ("O", "Oscar"), ("P", "Papa"),
    ("Q", "Quebec"), ("R", "Romeo"), ("S", "Sierra"), ("T", "Tango"),
    ("U", "Uniform"), ("V", "Victor"), ("W", "Whisky"), ("X", "X-ray"),
    ("Y", "Yankee"), ("Z", "Zulu")
]

class ListFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        wx.Frame.__init__(self, *args, **kwds)

        self.list_ctrl = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES)
        self.list_ctrl.AppendColumn("Letter", format=wx.LIST_FORMAT_LEFT, width=-1)
        self.list_ctrl.AppendColumn("Word", format=wx.LIST_FORMAT_LEFT, width=-1)

        for j, (letter, word) in enumerate(DATA):
            index = self.list_ctrl.InsertItem(j, letter)
            self.list_ctrl.SetItem(index, 1, word)


class MyApp(wx.App):
    def OnInit(self):
        self.frame = ListFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

The results are as follows.

[Lbtn] select (only one item)
[S-Lbtn] extend the selection
[C-Lbtn] add/remove selections
[S-C-Lbtn] extend the selection to the maximum

[Rbtn] select (only one item)
[S-Rbtn] does nothing (but when dragged, add a selection)
[C-Rbtn] same as [S-Rbtn]
[S-C-Rbtn] same as [S-Rbtn]

>>> self.list_ctrl.Select(idx) select without focusing
>>> self.list_ctrl.Focus(idx)  focus only, no selection

Continued from the last post,…

Wx.EVT_CONTEXT_MENU can be an alternative solution; Instead of following the default action of Right-click, a context menu event can be used as follows.

@@ -31,2 +31,2 @@ class ListFrame(wx.Frame):
-        self.list_ctrl.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
-        self.list_ctrl.Bind(wx.EVT_RIGHT_UP,   self.OnRightUp)
+        self.list_ctrl.Bind(wx.EVT_CONTEXT_MENU,
+                            lambda v: self.popupContextMenu())

Otherwise,

        def on_rclick(v):
            wx.CallAfter(self.popupContextMenu)
            v.Skip()
        self.list_ctrl.Bind(wx.EVT_RIGHT_DOWN, on_rclick)

if you don’t want apps-key to become effective (Ah you are Linux user…)
EDIT sorry I mistook it. (Fixed to EVT_RIGHT_DOWN).