ListCtrl.DeleteItem triggers a focus event with wrong item number

Hi there

I noticed that ListCtrl.DeleteItem triggers a focus event with the wrong item.
This issue is similar to: ListCtrl.GetSelectedItemCount() giving wrong result after DeleteItem()?.
But the current problem occurs with wx 4.2.1 on Windows 10.

Here is a code example that demonstrates the problem.
import wx

class CheckList(wx.ListCtrl):
    def __init__(self, *args, **kwargs):
        wx.ListCtrl.__init__(self, *args,
                             style=wx.LC_REPORT|wx.LC_HRULES, **kwargs)
        self.EnableCheckBoxes()
        self.AppendColumn("A")
        for i in range(6):
            self.InsertItem(i, "Item %d" % i)

        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)

        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnDeleteItem)
        self.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.OnItemFocused)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected)

    def OnKeyDown(self, evt):
        key = evt.GetKeyCode()
        j = self.FocusedItem
        if j != -1 and key == wx.WXK_DELETE:
            print("\nDeleting index", j)
            ## self.Select(j, False) # no effect
            self.DeleteItem(j)
            print(f"{self.FocusedItem = } is correct.")
        evt.Skip()

    def OnDeleteItem(self, evt):
        print("Delete index", evt.Index)
        evt.Skip()

    def OnItemFocused(self, evt):
        print("Focused index", evt.Index)
        evt.Skip()

    def OnItemSelected(self, evt):
        print("Selected index", evt.Index,
              "selection", self.SelectedItemCount,
              "of", self.ItemCount)
        evt.Skip()

    def OnItemDeselected(self, evt):
        print("Deselected index", evt.Index)
        evt.Skip()

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.lc = CheckList(frm)
    frm.Show()
    app.MainLoop()

Do the following steps:

  1. Click on Item 0
  2. Press [delete]

Then, it produces the following output:

Focused index 0
Selected index 0 selection 1 of 6

Deleting index 0
Deselected index 0
Focused index 1
Delete index 0
self.FocusedItem = 0 is correct.

I think the focus event should be triggered after deletion, not during deletion.
I will report these to the issue tracker, but I want to hear your opinions before that.
Thoughts?

well, I don’t know what this focused item could be good for, but what you want here (line 21) is

        # j = self.FocusedItem
        j = self.GetFirstSelected()

if you have FocusedItem and repeat delete (by accident or regular) you may get a non selected item deleted :woozy_face:

Yes, it does make sense to delete the selected items rather than the focused item to avoid unintentional deletion.

I will fix the sample code, but the point is elsewhere:
The EVT_LIST_ITEM_FOCUSED handler retrieves the index that will be removed during deletion. As a result, the last state after deletion and evt.Index is inconsistent.
image

I don’t think it is a bug, but it will potentially produce a bug.
Need to distinguish between the contexts of the focus handler: normal focus or focus in deletion.

I don’t think there is anything like a transactional event, i.e. spanning across other events: it would blow up any event system (you may keep your own flag) :face_with_monocle:

what I was more interested in is what are you using this pretty erratic focus for ? :thinking:

Actually, since I ran into this problem, I noticed that EVT_LIST_ITEM_SELECTED should be used instead of EVT_LIST_ITEM_FOCUSED. I’m not sure :thinking: if I should open this issue.

My use case is:

class CheckListCtrl(wx.ListCtrl):
    # self.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.OnItemFocused)
    def OnItemFocused(self, evt):
        frame = self.Target.all_frames[evt.Index]
        ...

where Target is a sort of reference to a stack-frame list.
If a user removes frames from the stack, it dispatches a message with deleted indices.
The ListCtrl subscribes to it and deletes the corresponding items:

    def on_frames_removed(self, indices):
        for j in reversed(indices):
            self.DeleteItem(j)

As DeleteItem triggers EVT_LIST_ITEM_FOCUSED while deletion, it raises an index error.

if you stick to selecting items for deletion then you may filter like

    def OnItemFocused(self, evt):
        if self.IsSelected(evt.GetIndex()):
            print("Focused index", evt.Index)
        evt.Skip()

I don’t think wx can change anything about this behaviour because it’s just passing it through :ghost:

1 Like

Thank you for the solution!
I think it would work in most cases. However, it cannot be filtered if the evt.Index during deletion was actually selected during deletion.

Yes, wx can! :lollipop: I think I would go to the wxWidgets issue tracker.

before you rush off (also covers your original focus mystic) :cowboy_hat_face:

import wx

class CheckList(wx.ListCtrl):
    def __init__(self, *args, **kwargs):
        wx.ListCtrl.__init__(self, *args,
                             style=wx.LC_REPORT|wx.LC_HRULES, **kwargs)
        self.EnableCheckBoxes()
        self.AppendColumn("A")
        for i in range(6):
            self.InsertItem(i, "Item %d" % i)

        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)

        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnDeleteItem)
        self.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.OnItemFocused)
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected)

    def OnKeyDown(self, evt):
        key = evt.GetKeyCode()
        j = self.FocusedItem
        if j != -1 and key == wx.WXK_DELETE:
            print("\nDeleting index", j)
            ## self.Select(j, False) # no effect
            self.SetItemState(j, 0, wx.LIST_STATE_FOCUSED)
            self.DeleteItem(j)
            print(f"{self.FocusedItem = } is correct.")
        evt.Skip()

    def OnDeleteItem(self, evt):
        print("Delete index", evt.Index)
        evt.Skip()

    def OnItemFocused(self, evt):
        print("Focused index", evt.Index)
        evt.Skip()

    def OnItemSelected(self, evt):
        print("Selected index", evt.Index,
              "selection", self.SelectedItemCount,
              "of", self.ItemCount)
        evt.Skip()

    def OnItemDeselected(self, evt):
        print("Deselected index", evt.Index)
        evt.Skip()

if __name__ == "__main__":
    app = wx.App()
    frm = wx.Frame(None)
    frm.lc = CheckList(frm)
    frm.Show()
    app.MainLoop()
1 Like

Thank you for the solution! It works perfectly.
Killing focus before DeleteItem seems to prevent triggering a new focus event while deletion:

            ## self.Select(j, False) # no effect
+           self.SetItemState(j, 0, wx.LIST_STATE_FOCUSED)
            self.DeleteItem(j)

I looked for other ways to kill focus, and SetItemState seems to be the only way, but is said to be an “error-prone and confusing method”… :worried:
https://docs.wxpython.org/wx.ListCtrl.html#wx.ListCtrl.SetItemState

I suppose in the old days there must have been a really messy ‘discussion’ about focus & select and this is just a leftover, the smoke, after everyone jumped into the cloud… :partying_face: