Workaround for listctrl first row single click edit annoyance in unselected row case (i.e. sort of bugfix)

An editable listctrl performs well in all but one circumstances: when no row is selected – as immediately when the list shows up or by clicking on empty space, by single clicking on any cell on first row, that cell enters directly in edit mode. On the other hand, by single clicking on any other row (also when none is prior selected), that row is first selected and only at the next single click the respective cell enters in edit mode.

Here is a test code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import wx
import wx.lib.mixins.listctrl as listmix

##

class EditableListCtrl(wx.ListCtrl, listmix.TextEditMixin):

    def __init__(self, parent, ID = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.DefaultSize, style = 0):

        wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
        listmix.TextEditMixin.__init__(self)

##

class MyPanel(wx.Panel):

    def __init__(self, parent):

        wx.Panel.__init__(self, parent)
        some_data = [("abc", "def", "ghi"), ("jkl", "mno", "pqr")]

        self.list_ctrl = EditableListCtrl(self, wx.ID_ANY, style = wx.LC_REPORT | wx.LC_HRULES)
        self.list_ctrl.InsertColumn(0, "col_1")
        self.list_ctrl.InsertColumn(1, "col_2")
        self.list_ctrl.InsertColumn(2, "col_3")

        index = 0
        for row in some_data:
            self.list_ctrl.InsertItem(index, row[0])
            self.list_ctrl.SetItem(index, 1, row[1])
            self.list_ctrl.SetItem(index, 2, row[2])
            index += 1

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.list_ctrl, 0, wx.ALL|wx.EXPAND, 5)
        self.SetSizer(sizer)

        self.list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnListItemSelected)

##

    def OnListItemSelected(self, event):
        event.Skip()

##

class MyFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Editable List Control")
        panel = MyPanel(self)
        self.Show()

##

if __name__ == "__main__":
    app = wx.App(False)
    frame = MyFrame()
    app.MainLoop()

Because that ‘feature’ continued to annoy the hell out of me [*], I digged into the listctrl code and came up with a workaround, that – at least for me – it simply works: by changing value 0 to -2 in line 476 of the code (under class TextEditMixin), all listctrl lines now behave the same, whether it’s about the first or the tenth row. More specifically:

# value of old line 476:
self.curRow = 0
# value of new line 476:
self.curRow = -2

As far as I understand, a negative value is required by OnLeftDown call in order to differentiate from clicking on empty space versus over a line (I inserted a print (row) at line 539 to observe that) and because -1 is already used as flag for other part of listctrl program, I just used -2 as my own flag.

Also there there is a self.curCol = 0, but the columns didn’t bother me, so I didn’t touch that one :slight_smile:

The particular implementation of listctrl in my program is a bit more complex than just enable editing (involves sorting, some line manipulation etc.) and until now everything seems to work fine.

So, question: could this be a permanent fix ? What undesired side effect may occur ?

In my case, I used the modified listctrl.py file as if it were my own custom module, i.e. I put it together with my other own modules that are part of my main program; that way, the workaround is ported to whatever PC my program is running.

Cristi

[*] I also tried switching to ultimatelistctrl alternative and then to a patched-for-python3 ObjectListView version (further patched by myself in order to solve some runtime errors), but I didn’t liked various things there, so I came back to listctrl.

@secarica well, you are dead right, a current indicator should never be initialized to a valid value :stuck_out_tongue_closed_eyes: (the problem occurs first in line 539)
so I would say (-2 looks odd)

        self.curRow = -1
        self.curCol = -1

No, value -1 cannot be used, just because of line 539 condition.

This can be easily checked right away: by changing value from line 476 to -1 and by running my above test code, by clicking first on an empty space (under the last row), a wx error dialog tells me ‘Couldn’t retrieve information about list control item -1.’ (and some more bla, bla error message shows on the terminal window). Instead, with -2 everything works perfect :slight_smile: [*]

[*] I only speak about self.curRow, I din’t dig the self.curCol road.

Cristi

one should never replace one evil with another !!! :stuck_out_tongue_closed_eyes:
line 539 should read
if row < 0 or row != self.curRow:
(I do admit there are some obstacles in this coding :rofl: and if you have more don’t be shy)

this also requires a binding

self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected, self)

with

    def OnItemDeselected(self, evt):
        self.curRow = -1
        evt.Skip()

(but my main problem would be that I don’t see the cursor in the editor :flushed:)

Well, agreed :slight_smile:

My original intention was to obtain maximum yield with “Minimum Necessary Change” to a 22 years old code, but – naturally – it is preferable if things can be done right if/where possible.

Strange, though, how in all these years nobody truly complained about the annoyance I am referring to in the present topic.

Probably right, only that – at least at first glance – I cannot notice any difference with or without this addition.

Later edit: no, not probably right, but definitely right :slight_smile: Further testing shows that, with this addition, clicking repeatedly between empty space and any – but the same – row, that row always remain in selection mode (while without this addition the row’s cell most likely enters in edit mode on first click).

So, thank you for the further fix :slight_smile:

Hmm: I noticed that too, but this is true only on Windows (10) [*]; running my test code on Linux (Mint), the cursor is always visible. Could this be related somehow to the C code on which wxPython is built ? (only a vague assumption, I am not a skilled in that direction)

Cristi

[*] Later edit: in fact, on Windows, the cursor always shows once, at first cell edit after the list is shown (drawn); then, after leaving that first cell edit, any subsequent cell edit will no longer display the cursor.

So far, that would be like this listctrl.zip (8.9 KB), which includes these differences:

Text Compare
Produced: 10.11.2023 11:39:30

Mode:  Differences
Left file: original listctrl.py
Right file: patched listctrl.py
                                                                                -+ 461         # added 2023-11-10
------------------------------------------------------------------------
                                                                                -+ 462         # see https://discuss.wxpython.org/t/workaround-for-listctrl-first-row-single-click-edit-annoyance-in-unselected-row-case-i-e-sort-of-bugfix/36713
------------------------------------------------------------------------
                                                                                -+ 463         self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected, self)
------------------------------------------------------------------------
                                                                                -+ 464
------------------------------------------------------------------------
                                                                                -+ 465 ##
------------------------------------------------------------------------
                                                                                -+ 466
------------------------------------------------------------------------
                                                                                -+ 467     # added on 2023-11-10
------------------------------------------------------------------------
                                                                                -+ 468     # see https://discuss.wxpython.org/t/workaround-for-listctrl-first-row-single-click-edit-annoyance-in-unselected-row-case-i-e-sort-of-bugfix/36713
------------------------------------------------------------------------
                                                                                -+ 469     def OnItemDeselected(self, evt):
------------------------------------------------------------------------
                                                                                -+ 470         self.curRow = -1
------------------------------------------------------------------------
                                                                                -+ 471         evt.Skip()
------------------------------------------------------------------------
                                                                                -+ 472
------------------------------------------------------------------------
                                                                                -+ 473 ##
------------------------------------------------------------------------
------------------------------------------------------------------------
                                                                                -+ 489         # modified on 2023-11-10
------------------------------------------------------------------------
                                                                                -+ 490         # see https://discuss.wxpython.org/t/workaround-for-listctrl-first-row-single-click-edit-annoyance-in-unselected-row-case-i-e-sort-of-bugfix/36713
------------------------------------------------------------------------
476         self.curRow = 0                                                     <> 491         self.curRow = -1
------------------------------------------------------------------------
477         self.curCol = 0                                                     <> 492         self.curCol = -1
------------------------------------------------------------------------
------------------------------------------------------------------------
                                                                                -+ 554         # modified on 2023-11-10
------------------------------------------------------------------------
                                                                                -+ 555         # see https://discuss.wxpython.org/t/workaround-for-listctrl-first-row-single-click-edit-annoyance-in-unselected-row-case-i-e-sort-of-bugfix/36713
------------------------------------------------------------------------
539         if row != self.curRow: # self.curRow keeps track of the current row <> 556         if row < 0 or row != self.curRow: # self.curRow keeps track of the current row
------------------------------------------------------------------------

Could be I found a workaround (rather) I found a fix for this one too :slight_smile:

Line 486 (original listctrl.py code) says this:

        self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)

Comment it, and the cursor no longer disappears on subsequent edits !

Matter of fact, in my opinion the new behaviour is even better than before: according to docs, the EVT_KILL_FOCUS is related to losing the window focus, i.e. wehen clicking somewhere outside the window where the edit happens; or, that line of code closes the edit, so when returning focus back where the edit happens, the edit is no longer active.

However, by commenting line 486 (original code), in the click-somewhere-then-return scenario, the edit just freezes and then resumes, so the better !!

Later edit: this (new) behaviour is very similar to any Calc/Excel cell editing, where pausing an edit in a spreadsheet just freezes that cell’s editing action until focus returns – which is useful, for example, when text from another application must be copied and pasted where editing is still in progress.

(the above is true on Windows, will test later on Linux too)

Putting everything together so far (II): listctrl.zip (8.9 KB)

On Linux Mint it still works well :grin:

Later edit: I just noticed that someone (‘MW’) once commented this on line 624 (original code):

    # FIXME: this function is usually called twice - second time because
    # it is binded to wx.EVT_KILL_FOCUS. Can it be avoided? (MW)
    def CloseEditor(self, evt=None):
        ''' Close the editor etc.

Well, now the ‘this function is usually called twice’ is somehow avoided :slight_smile: (and that ‘is … called twice’ seems to be the reason for edit cursor disappearance; strange, though, why this only happen on Windows / why this does not happen on Linux too)

well, this may be a fix (never give up :rofl:)

replace

        self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)

by

        def evt_kill_focus(evt):
            self.CloseEditor()
            evt.Skip()
        self.editor.Bind(wx.EVT_KILL_FOCUS, evt_kill_focus)

Well, that would be ‘only’ a bug fix :grin: – i.e. the edit action will end as soon as the window loses focus – which, that’s true, is the original behaviour of the listctrl.py module; however, as I said previously, in my opinion leaving the edit ‘open’ is much more desirable – i.e. totally ignore the EVT_KILL_FOCUS event.

Now that also depends on the preferences of the original listctrl author :grin: (@Robin Dunn I presume)

Btw, I think there are two missing self’s in the above fix version :slight_smile:

Putting everything together so far (III): listctrl.zip (9.1 KB)

The attached code does include the above @da-dada’s fix version, so it keeps the original edit behaviour. As far as I can tell – that is, after moderate, but real in-production testing (so to say) – the listctrl code is now bug free in relation to the present topic and its subtopic :slight_smile:

Cristi

no, not at all (a little exercise in Python :sweat_smile: here is the whole method for tinkerers)

    def make_editor(self, col_style=wx.LIST_FORMAT_LEFT):

        style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB
        style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT,
                  wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT,
                  wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE
                  }[col_style]

        editor = wx.TextCtrl(self, -1, style=style)
        editor.SetBackgroundColour(self.editorBgColour)
        editor.SetForegroundColour(self.editorFgColour)
        font = self.GetFont()
        editor.SetFont(font)

        self.curRow = -1
        self.curCol = -1

        editor.Hide()
        if hasattr(self, 'editor'):
            self.editor.Destroy()
        self.editor = editor

        self.col_style = col_style
        self.editor.Bind(wx.EVT_CHAR, self.OnChar)
        def evt_kill_focus(evt):
            self.CloseEditor()
            evt.Skip()
        self.editor.Bind(wx.EVT_KILL_FOCUS, evt_kill_focus)

Yes, right :slight_smile: But is there any particular reason (benefit) for putting it that way ? (i.e. completely inside the current function, versus a distinct define) If it’s still about exercises…

well, that’s the way I found the bug :joy:

theoretically it’s the pinciple of subsidiarity which broadly states keep locals local
importantly it’s not against an instance above, quite the opposite it takes workload off that instance: in a certain way it’s focus is somewhat sharper (the comment already pointed to the event, but the mate had a lack of endurance although the method has a proper parameter)

if you change the start of the method to

    def CloseEditor(self, evt=None):
        ''' Close the editor and save the new value to the ListCtrl. '''
        if evt: evt.Skip()
        if not self.editor.IsShown():
            return

it gives the same result but violates the above principle and disguises the nature of the event

putting everything into self is a little bit different but similarly debatable

but what keeps my belief is that nature itself seems entrenched in that principle: it’s just the division of labour (still to negotiate the pay :roll_eyes:)

P.S. I just read the docu and generally it will be better to move

        self.editor.Hide()

to be the last line of that method and delete

        self.SetFocus()

altogether, sorry :hot_face: