Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detecting Keys being pressed, held, or released (with EVT_KEY_DOWN,EVT_KEY_UP) is not working ? #2615

Open
TJ-59 opened this issue Sep 27, 2024 · 11 comments

Comments

@TJ-59
Copy link

TJ-59 commented Sep 27, 2024

Might be something missing, but I've tried the examples on the various sites showing how to use wx for that purpose,
And couldn't get it to detect some keys being held. Had a lenghty AI session just in case I missed something from all the docs,
but I've tried so many permutations : TRANSPARENT_WINDOW, WANTS_CHARS, Etc, doing it from different level of windows (Frame, panel, subpanels), modifier keys like shift or control, actual letter keys (even choosing one that is not changing position in most "latin" alphabet layouts) to no avail.

Operating system: Windows 10 pro 22H2
wxPython version & source: '4.2.1 msw (phoenix) wxWidgets 3.2.2.1'
Python version & source: 3.11.8 ('stock', IDLE day mode user spotted 🕶️ )
keyboard layout: EN-US & FR-FR

Description of the problem:
When a key is being pressed and held down, a function bound to EVT_KEY_DOWN should start, in which you would discriminate what to do depending on the key being pressed, from event.GetKeyCode()
(with something like :

class MyPanel(wx.Panel):
    def __init__(self,...)
        (...)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self,event):
            if event.GetKeyCode() == wx.WXK_SHIFT :
                print("SHIFT DOWN")

or in a MyFrame(wxFrame) class, it's about the same...)
I noticed it first in something I'm working on, not managing to obtain what I wanted (basically, I want a modifier key that, if held down during a right-click, will hide, among other actions, the subpanel it was clicked on. A sort of "unclutter"/"I don't need to see them at the moment" selection, but depending on the current state of another modifier key, they will either vanish on click, or stay visible until that other modifier key is released.
Think of it as "Shift + right-click to hide/show", and "hold Control to Show everything that is hidden", meaning if a subpanel you previously hid is now needed, press Control to display everything, those that are hidden will show back with a different color, so you know what can be clicked with "shift + control + right-click".

The "shift + Rclick" part to hide a panel works like this :

class Subpanel(wx.Panel):
    def __init__(self,...):
        (...)
        self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)

    def on_right_down(self,event):
        print("right down")
        if event.ShiftDown():
            print("right down and shift down")
            if self.hidden is False :
                # the things to do to hide the panel like a conditional Hide(), the color change, etc, and of course, updating the hidden status
            else :
                # the things to make it Show() and return the usual background color, of course, updating the status too
        event.Skip()

This part works as far as I'm aware, since the elements are hidden one by one with each click, ONLY if Shift is held down when the click happens; only I cannot really check the color change since the "Hold Control down to see everything" will not trigger. I tried other keys, and nothing changes.

Here under is a basic example, that everything else, AI included, considered should work.
(it does the right click thingy combination with "shift", "control" and "g", but any of these 3 should also print on it's own when pressed or released)

Code Example (click to expand)
import wx

class SubPanel(wx.Panel):
    def __init__(self, parent, name, color):
        super().__init__(parent)
        # you might want to use those : style=wx.WANTS_CHARS | wx.CLIP_CHILDREN | wx.TAB_TRAVERSAL

        self.name = name
        self.SetBackgroundColour(color)
        self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)

    def on_right_click(self, event):
        key_state = []
        if self.GetTopLevelParent().is_shift_pressed:
            key_state.append("Shift")
        if self.GetTopLevelParent().is_ctrl_pressed:
            key_state.append("Ctrl")
        if self.GetTopLevelParent().is_g_pressed:
            key_state.append("g")

        if key_state:
            print(f"Right mouse button clicked on {self.name} subpanel [{', '.join(key_state)}]")
        else:
            print(f"Right mouse button clicked on {self.name} subpanel")

class MainPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        # you might want to use those : style=wx.WANTS_CHARS | wx.CLIP_CHILDREN | wx.TAB_TRAVERSAL | wx.TRANSPARENT_WINDOW

        # Create the subpanels in a horizontal box sizer
        sub_panel_1 = SubPanel(self, "Subpanel 1", "red")
        sub_panel_2 = SubPanel(self, "Subpanel 2", "green")
        sub_panel_3 = SubPanel(self, "Subpanel 3", "blue")

        hbox = wx.BoxSizer(wx.HORIZONTAL)
        hbox.Add(sub_panel_1, 1, wx.EXPAND)
        hbox.Add(sub_panel_2, 1, wx.EXPAND)
        hbox.Add(sub_panel_3, 1, wx.EXPAND)

        # Create the main panel in a vertical box sizer
        vbox = wx.BoxSizer(wx.VERTICAL)
        vbox.Add(hbox, 1, wx.EXPAND)
        self.SetSizer(vbox)


class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='EVT_KEY_DOWN Example')
        # you might want to use those : style=wx.DEFAULT_FRAME_STYLE | wx.WANTS_CHARS | wx.CLIP_CHILDREN | wx.TRANSPARENT_WINDOW

        self.main_panel = MainPanel(self)
        self.is_shift_pressed = False
        self.is_ctrl_pressed = False
        self.is_g_pressed = False

        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
        self.Bind(wx.EVT_KEY_UP, self.on_key_up)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            self.is_shift_pressed = True
            print("Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            self.is_ctrl_pressed = True
            print("Control key pressed")
        elif key_code == ord('g') or key_code == ord('G'):
            self.is_g_pressed = True
            print("'g' key pressed")
        event.Skip()

    def on_key_up(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            self.is_shift_pressed = False
            print("Shift key released")
        elif key_code == wx.WXK_CONTROL:
            self.is_ctrl_pressed = False
            print("Control key released")
        elif key_code == ord('g') or key_code == ord('G'):
            self.is_g_pressed = False
            print("'g' key released")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    app.MainLoop()

Any help would be appreciated, even just a "This does work for me with version xyz", because at this point, it's either a bug (in wx or on my machine) or something so obvious that it is hidden in plain sight, so well that even simply copy-pasting code from the docs will not make it work and even the AI did not notice it.
Some may consider the EVT_CHAR_HOOK, but then please consider the fact that a key pressed in this mode will just trigger the "release" a first time, then a delay, then rapid-fire trigger the release until the key is actually released, all without a single "key pressed" print even being sent to stdout...
(basically, think of what happens when you open a text editor and hold a character key down : "A......A-A-A-A-A-A-A-A-A" depending on the delay and repeat rate, except it's shift and control, keys "made" to be modifiers since their inception)

If you try this example, whether it works or not for you, please also indicate you keyboard layout(s), in case it is related to some misinterpreted keycodes somewhere.

@infinity77
Copy link
Contributor

I haven’t tried your code yet, and maybe I’m misunderstanding what you’re trying to accomplish, but is there any reason why you’re not considering the simpler approach of using only wx.EVT_RIGHT_DOWN and checking the state of the keyboard there using wx.GetMouseState()?

@TJ-59
Copy link
Author

TJ-59 commented Sep 27, 2024

There are 2 things that are needed :

1/ right-clicking, on specific panels, WHILE SHIFT IS DOWN, to make them disappear (aka the "unclutter part").
This part seems to work, it's a EVT_RIGHT_DOWN during which the event.ShiftDown() is checked.
Note that it is a "Toggle", as doing the same thing again ("Right-click on a panel while shift is down") will make them visible again, but of course, they are NOT visible yet, thus you cannot click on them, unless...

2/a) Holding the Control key down should trigger an EVT_KEY_DOWN, which is bound to a function that, if the event.GetKeyCode() returns the value for the Control key, checks all those children panels, and those that have the attribute hidden == True (a.k.a. "those panels that have been hidden by a right-click while shift was held down") will get their Show(True) called, making them visible again.
(in order to clearly mark which panels are the ones that are supposed to be hidden, the "right-click with shift down" function from part 1/ not only set the hidden attribute to True, but also, their background color is noticeably changed)
Basically, the Control key is a "True Sight" when down.

b) When you release the Control key, it should trigger an EVT_KEY_UP, which event.GetKeyCode() should also return the value for the Control key, and set all those "hidden" panels back to Hide() (or Show(False) if you prefer)

The problem I encounter is that no amount of EVT_KEY_DOWN or EVT_KEY_UP will work. Not even in a very minimal code where, as you can see, there is NOTHING else but panels and a frame (no other event interaction, no TextCtrl or validator, simply asking for a print, not even something complicated to do)
It just does not work at all. The EVT_RIGHT_DOWN works great, to detect right clicks, and WHILE it is a right-down event, checking if the Shift key is currently "down" actually works, meaning the system DOES acknowledge those keys. Other events in wx I have used for other bits of code and always worked flawlessly, or at least, within the expected restrictions and caveat they were meant to have.

The WANTS_CHARS and EVT_CHAR_HOOK approach have been tried, but seem to have their own limitations and, for a lack of a better term, incoherences, as a key like control or shift, MEANT to be "modifiers" and never a "character to type" since the very beginning of computers, also act like characters with a "delay before repetition" and "repetition rate", when all you did was press down on it and keep your finger on the key, no tapping, no releasing, just plain holding it down, and yet, the prints you obtain from this with EVT_CHAR_HOOK are ONLY the "release" ones (well, they are actually the "else" part of a formatted string, but they are the result of checking event.GetEventType() == wx.EVT_KEY_DOWN, which means that EVT_KEY_DOWN is never seen, at all, during those events.
Here is a bit of code to modify the example with :

class MainPanel(wx.Panel):
    def __init__(self, parent):
        (...)
        self.is_shift_pressed = False
        self.is_ctrl_pressed = False
        self.Bind(wx.EVT_CHAR_HOOK, self.on_char_hook)

    def on_char_hook(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            self.is_shift_pressed = event.GetEventType() == wx.EVT_KEY_DOWN
            print(f"Shift key {'pressed' if self.is_shift_pressed else 'released'}")
        elif key_code == wx.WXK_CONTROL:
            self.is_ctrl_pressed = event.GetEventType() == wx.EVT_KEY_DOWN
            print(f"Control key {'pressed' if self.is_ctrl_pressed else 'released'}")
        else:
            event.Skip()

Using keys as modifiers while they are held down is as old as computers themselves, from the Shift to type CAPITALS, the Control to use shortcuts to frequent functions (ctrl+a, ctrl+c, ctrl+v, ctrl+s...), or those keys used to change how a tool works in some image editing software... even PAINT, the most basic stuff from microsoft, (whose only use cases are 1) "cropping a screenshot" and 2) "letting the kids put colors everywhere without having to wash the walls afterward") makes use of it (pressing Shift will restrain lines to 45° snaps, and shapes keep equal sides, only squares and perfect circles, no rectangles or ellipses).
I guess you now understand why I'm perplexed at this happening.

@infinity77
Copy link
Contributor

I would suggest posting a sample application showing the problem - I kind of understand what you’re trying to do but it’s difficult to help without a reproducer (or more precisely, I have zero willpower to sit down and write one myself).

I am sure you are aware of this, but just to clarify: keyboard events (with the exception of wx.EVT_CHAR_HOOK) are sent to the window that has keyboard focus. Only wx.EVT_CHAR_HOOK propagates upwards.

@TJ-59
Copy link
Author

TJ-59 commented Sep 28, 2024

In my 1st post, there is a collapsed "Code Example (click to expand)" part containing a quite minimalist example.
The problem is not the "mouse right-click", it is solely the fact that whenever a key is pressed, a EVT_KEY_DOWN should fire, and when the key is released, a EVT_KEY_UP should fire.
About the focus, I suspected so, I have tested it with just only a frame (which means, there is NO PROPAGATION involved as there is literally just this, a frame, with nothing inside) and it is the only case where it works without problems. That is, if having only a frame with a title and no buttons/panel/controls is what you need... quite limited use.
Even adding just an empty Panel breaks it, even if you were to give focus to the frame before running the MainLoop(), it would break at the first click in the client area.
EVT_KEY_DOWN and EVT_KEY_UP are, depending on who you ask, either limited to the current window with the focus (like, a subpanel or some control) or totally free and propagating from the children up until it reaches toplevel (the frame) as long as there is no handler intercepting them, OR the handler actually Skip() the event so something after that handler can handle it in turn.
Now you see my dilemma :
IF it is "limited to the current element with focus", it means you cannot have application-wide shortcuts/events, or it would imply some very heavy gasworks, constantly trying to give back the focus to the frame itself which would lose that focus at the slightest interaction, or would need EVERY element in-between the ones catching the event, UP to the toplevel, to have a bind on that same type of event, and to Skip() it to the parent, which would do the same and Skip() it again, and again and again, until it reach the Toplevel Frame;
OR it is not, and it is supposed to seek for a handler at the current level, and if not found, go up one level and seek again, repeating until it finds something that handles it and does not Skip() it further. This seems like the ideal/intended use, but there might be more to the key events specifically (if so, please enlighten me).

Here are various bits of code of interest :

Note

Be sure to swap to those windows with alt-tab instead of clicking them, as clicking them could give the focus to another element.

Click to expand - Frame only
import wx
class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Frame only')
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("'g' or 'G' key pressed")
        else:
            print(f"Key code: {key_code}")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    app.MainLoop()
Here, you can see the EVT_KEY_DOWN is seen by the frame... obviously, the frame is the ONLY element, and as such, has the focus, and cannot lose it.

Let's add a panel :

Click to expand - Just a Panel
import wx
class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Just a Panel')
        panel = wx.Panel(self)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("'g' or 'G' key pressed")
        else:
            print(f"Key code: {key_code}")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    app.MainLoop()
As I see it, it does not work. the Panel has the focus by default (Don't know why, I'd say it is given to the 1st child recursively, so by most standards, the upper-leftmost control/panel ends up with the focus)

Let's give the focus back to the frame just before the MainLoop()

Click to expand - Just a Panel, Frame starts with focus
import wx
class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Just a Panel, Frame starts with focus')
        panel = wx.Panel(self)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("'g' or 'G' key pressed")
        else:
            print(f"Key code: {key_code}")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    frame.SetFocus()
    app.MainLoop()

This, as long as you do not click anywhere in the client area, works. Then if you click in it, the panel receives the focus, and it doesn't work anymore.

What if both the frame and the panel had their own binds ?

Click to expand -Panel and Frame bound, Frame starts with focus
import wx
class MyPanel(wx.Panel):
    def __init__(self,parent):
        super().__init__(parent)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down_p)
    def on_key_down_p(self,event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("[P]Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("[P]Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("[P]'g' or 'G' key pressed")
        else:
            print(f"[P]Key code: {key_code}")
        event.Skip()

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Panel and Frame bound, Frame starts with focus')
        self.panel = MyPanel(self)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("[F]Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("[F]Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("[F]'g' or 'G' key pressed")
        else:
            print(f"[F]Key code: {key_code}")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    frame.SetFocus()
    app.MainLoop()
Here we see that as long as we don't touch the client area, the prints come from the frame ( "[F]" at the start of prints ), as soon as we click anything, it is the Panel's prints showing instead (starting with "[P]" ). Again, nothing we can do to have the focus back to the frame, unless we add a menubar, a menu, a menu entry, and have it give focus to the frame... not exactly ergonomic...

But what if... we DO try to make it a gasworks, but only on such a small structure ?
Surely, if we remove the Panel's function print content, and just make it Skip(), the Frame's handler for EVT_KEY_DOWN should react ?

Click to expand -Panel and Frame bound, Panel just Skip() it to the Frame
import wx
class MyPanel(wx.Panel):
    def __init__(self,parent):
        super().__init__(parent)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down_p)
    def on_key_down_p(self,event):
        event.Skip()

        
class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Panel and Frame bound, Panel just Skip() it to the Frame')
        self.panel = MyPanel(self)
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)

    def on_key_down(self, event):
        key_code = event.GetKeyCode()
        if key_code == wx.WXK_SHIFT:
            print("[F]Shift key pressed")
        elif key_code == wx.WXK_CONTROL:
            print("[F]Control key pressed")
        elif key_code == 103 or key_code == 71:
            print("[F]'g' or 'G' key pressed")
        else:
            print(f"[F]Key code: {key_code}")
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    frame.Show()
    # frame.SetFocus() #not needed anymore, but you can test with it
    app.MainLoop()

Sadly, this does not work either.

There are still more things to check, (like the GetKeyState(), while I fail to see how this would count as an event —And I'd really prefer to avoid locking myself in a loop to verify the current state— ; and the absence of GetAsyncKeyState wrapper in wx could mean we're not supposed to even think of using it...), And something I preferred to avoid because it is supposedly just a fancy way of doing those binds, namely the "accelerator tables", which should, logically, be held to the same limitations.

Still, I would gladly appreciate any info you could spare, including anything on the "why" of such limitation for key events ("Why can't it leave that element's bubble and be happily skipped over toward the parent"), which is unlike everything else in wx, or how YOU would do it in wx with such requirements.

@infinity77
Copy link
Contributor

infinity77 commented Sep 28, 2024

See if this may give you some inspiration - maybe it will, maybe it won't.

import wx

class EventPropagator(wx.EvtHandler):

    def __init__(self, old_handler):

        wx.EvtHandler.__init__(self)
        self.old_handler = old_handler

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

    def OnKeyDown(self, event):
        print('OnKeyDown Propagator', event.GetEventObject().GetName())
        event.ResumePropagation(1)
        event.Skip()

    def OnKeyUp(self, event):
        print('OnKeyUp Propagator', event.GetEventObject().GetName())
        event.ResumePropagation(1)
        event.Skip()

    def Register(self, win):

        for child in list(win.GetChildren()):
            child.PushEventHandler(EventPropagator(child.GetEventHandler()))

class MyFrame(wx.Frame):

    def __init__(self):

        wx.Frame.__init__(self, None, -1, 'Test KeyEvents', size=(800, 600))

        self.panel = panel = wx.Panel(self, name='Main Panel')

        colors = [wx.RED, wx.GREEN, wx.BLUE, wx.WHITE, wx.Colour(128, 128, 128)]
        sizer = wx.BoxSizer(wx.VERTICAL)

        for level in range(3):
            orientation = wx.VERTICAL if level % 2 == 0 else wx.HORIZONTAL
            inner_sizer = wx.BoxSizer(orientation)
            for i in range(5):
                sub_panel = wx.Panel(panel, -1, name='Level %d, Panel %d' % (level + 1, i + 1), style=wx.SUNKEN_BORDER)
                sub_panel.SetBackgroundColour(colors[i])
                inner_sizer.Add(sub_panel, 1, wx.EXPAND, wx.ALL, 5)

            sizer.Add(inner_sizer, 1, wx.EXPAND)

        panel.SetSizer(sizer)
        propagator = EventPropagator(panel.GetEventHandler())
        propagator.Register(panel)
                          
        panel.Bind(wx.EVT_KEY_DOWN, self.OnFrameKeyDown)
        panel.Bind(wx.EVT_KEY_UP, self.OnFrameKeyUp)

        self.Bind(wx.EVT_CLOSE, self.OnClose)


    def OnClose(self, event):

        self.RemoveEventHandlers()
        event.Skip()

    def RemoveEventHandlers(self, win=None):

        if win is None:
            win = self.panel

        for child in list(win.GetChildren()):
            child.PopEventHandler(True)
            self.RemoveEventHandler(child)

    def OnFrameKeyDown(self, event):

        print('    ==> Frame OnKeyDown: %s, KeyCode: %d' % (event.GetEventObject().GetName(), event.GetKeyCode()))
        print('        ==> Ctrl Down      : ', event.ControlDown())
        print('        ==> Shift Down     : ', event.ShiftDown())
        event.Skip()

    def OnFrameKeyUp(self, event):
        print('    ==> Frame OnKeyUp  : %s, KeyCode: %d' % (event.GetEventObject().GetName(), event.GetKeyCode()))
        print('        ==> Ctrl Down      : ', event.ControlDown())
        print('        ==> Shift Down     : ', event.ShiftDown())
        event.Skip()


def Main():

    app = wx.App(0)
    frame = MyFrame()
    frame.CenterOnScreen()
    frame.Show()
    app.MainLoop()


if __name__ == '__main__':
    Main()

@TJ-59
Copy link
Author

TJ-59 commented Oct 5, 2024

Great inspiration actually.

Looking for that ResumePropagation() infos allowed me to see and learn a bunch about how the events are processed in wx.
I still have questions about the history and reasons of such choices as the command events going through while the normal events don't, and why there aren't "command events" equivalents for EVT_KEY_UP & EVT_KEY_DOWN when there are "command events" equivalents for the mouse clicks, and my current guess is "it's probably related to security", seeing as without it, events could be sent through some objects coming from packages of "questionable" reputation (since pip names don't seem to be always protected & legit, this very project module being wx but users need to install it with pip install wxpython since something else is already using that package name) that could be intercepting keypresses and acting, basically, as a keylogger.
I tried to make my own version of this EventPropagator, with slightly different specifications.
Feel free to add any criticism, remarks or questions about it.

Propagation sauce

import wx


class MyOwnSauce():
    """
    technically, all events could be bound to one function instead of a specific function,
     since we do the same in every case (bump them up), but we want different prints here.
     Also worth noting :
     there could also be a list of event types given as argument,
     to bind each of them to the unique function bumping them one level up.
    """
    def __init__(self):
        self.proplevel = self.calcPropLevel()
        if not self.IsTopLevel():
            self.DoBinds()
   
    def DoBinds(self):
        self.Bind(wx.EVT_RIGHT_DOWN,self.on_sauce_mouse_right_down)
        self.Bind(wx.EVT_RIGHT_UP,self.on_sauce_mouse_right_up)
        self.Bind(wx.EVT_KEY_DOWN,self.on_sauce_key_down)
        self.Bind(wx.EVT_KEY_UP,self.on_sauce_key_up)
        
    def calcPropLevel(self):
        if self.IsTopLevel():
            print("This window is TopLevel")
            return self.prop_offset if hasattr(self,"prop_offset") else 0
        else :
            target = self.GetParent()
            n = 0
            while target is not None:
                n += 1
                if hasattr(target,"proplevel"):
                    return target.proplevel + n
                else:
                    target = target.GetParent()
            #The following case should never happen, but just to be sure
            print("Warning, returning n for proplevel, is something amiss ?")
            return n

    def on_sauce_mouse_right_down(self,event):
        print("Mouse Right DOWN Propagation, proplevel = {}".format(self.proplevel))
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def on_sauce_mouse_right_up(self,event):
        print("Mouse Right -UP- Propagation, proplevel = {}".format(self.proplevel))
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def on_sauce_key_down(self, event):
        print("Key DOWN Propagation, proplevel = {}".format(self.proplevel))
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def on_sauce_key_up(self, event):
        print("Key -UP- Propagation, proplevel = {}".format(self.proplevel))
        event.ResumePropagation(self.proplevel)
        event.Skip()
    

class MyPanelWithWidgets(wx.Panel,MyOwnSauce):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        MyOwnSauce.__init__(self)
        self.PWWsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.PWWsizer)
        self.text = wx.TextCtrl(self,value=str(self.proplevel))
        self.PWWsizer.Add(self.text, 0, wx.CENTER | wx.ALL,3)
        self.Bind(wx.EVT_LEFT_DOWN,self.on_mouse_left_down)
        self.Bind(wx.EVT_LEFT_UP,self.on_mouse_left_up)

    def on_mouse_left_down(self,event):
        print("[Local]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def on_mouse_left_up(self,event):
        print("[Local]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))
        event.ResumePropagation(self.proplevel)
        event.Skip()


class MyPanel(wx.Panel,MyOwnSauce):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        MyOwnSauce.__init__(self)
        self.SetBackgroundColour((255,255,0,255))
        self.panelsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.panelsizer)
        self.pww1 = MyPanelWithWidgets(self,size=(-1,-1),name="pww1")
        self.pww1.SetBackgroundColour((255,0,0,255))
        self.panelsizer.Add(self.pww1,1,wx.EXPAND,5)
        self.pww2 = MyPanelWithWidgets(self,size=(-1,-1),name="pww2")
        self.pww2.SetBackgroundColour((0,255,0,255))
        self.panelsizer.Add(self.pww2,1,wx.EXPAND,5)
        self.pww3 = MyPanelWithWidgets(self,size=(-1,-1),name="pww3")
        self.pww3.SetBackgroundColour((0,0,255,255))
        self.panelsizer.Add(self.pww3,1,wx.EXPAND,5)
        self.Bind(wx.EVT_LEFT_DOWN,self.on_panel_mouse_left_down)
        self.Bind(wx.EVT_LEFT_UP,self.on_panel_mouse_left_up)

    def on_panel_mouse_left_down(self,event):
        print("[Panel]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))

    def on_panel_mouse_left_up(self,event):
        print("[Panel]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))


class MyFrame(wx.Frame,MyOwnSauce):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        MyOwnSauce.__init__(self)
        self.framesizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.framesizer)
        self.panel = MyPanel(self,size=(-1,-1),name="mainpanel")
        self.framesizer.Add(self.panel, 1,wx.EXPAND, wx.ALL,5)

        self.Bind(wx.EVT_LEFT_DOWN,self.on_frame_mouse_left_down)
        self.Bind(wx.EVT_LEFT_UP,self.on_frame_mouse_left_up)
        self.Bind(wx.EVT_RIGHT_DOWN,self.on_frame_mouse_right_down)
        self.Bind(wx.EVT_RIGHT_UP,self.on_frame_mouse_right_up)
        self.Bind(wx.EVT_KEY_DOWN,self.on_frame_key_down)
        self.Bind(wx.EVT_KEY_UP,self.on_frame_key_up)

    def on_frame_mouse_left_down(self,event):
        print("[Frame]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))

    def on_frame_mouse_left_up(self,event):
        print("[Frame]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))

    def on_frame_mouse_right_down(self,event):
        print("[Frame]Mouse RIGHT DOWN in {}".format(event.GetEventObject().GetName()))

    def on_frame_mouse_right_up(self,event):
        print("[Frame]Mouse RIGHT -UP- in {}".format(event.GetEventObject().GetName()))

    def on_frame_key_down(self, event):
        print("[Frame]Key DOWN : {} in {}".format(event.GetKeyCode(),event.GetEventObject().GetName()))
        print("    ==> Ctrl Down : ", event.ControlDown())
        print("    ==> Shift Down : ", event.ShiftDown())

    def on_frame_key_up(self, event):
        print("[Frame]Key -UP- : {} in {}".format(event.GetKeyCode(),event.GetEventObject().GetName()))
        print("    ==> Ctrl Down : ", event.ControlDown())
        print("    ==> Shift Down : ", event.ShiftDown())


if __name__ == '__main__':
    app = wx.App(False)
    frame = MyFrame(None,-1,"propagation test",size=(400,200))
    frame.Show()
    frame.SetFocus() #purely to avoid giving the focus automatically to one of the TextCtrl
    app.MainLoop()

Left clicks are supposed to be seen by the subpanels and the mainpanel,
right clicks are supposed to go through,
key presses, as long as they aren't blocked by a control hogging the focus, are transmitted to the frame.

The principle is relatively simple :

  1. make your frame/panel/etc inherit from the sauce (for lack of a better name)
  2. add the call to its __init__ right after the super() : MyOwnSauce.__init__(self) (so it can use the actual instance and attributes)
  3. define the binds that need to occur in the DoBinds() function, and their handler functions.
    As noted in the code, it would typically be a list of event types given as an argument to __init__, all bound to the same function :
(...)
        super().__init__(*args,**kwargs)
        MyOwnSauce.__init__(self, [wx.EVT_LEFT_DOWN,wx.EVT_LEFT_UP,wx.EVT_RIGHT_DOWN,wx.EVT_RIGHT_UP,wx.EVT_KEY_DOWN,wx.EVT_KEY_UP])
(...)
class MyOwnSauce():
    def __init__(self,evtlist):
        self.proplevel = self.calcPropLevel()
        if not self.IsTopLevel():
            for eventtype in evtlist : 
                self.Bind(eventtype, self.BumpEvent)
    def BumpEvent(self,event):
        event.ResumePropagation(self.proplevel)
        event.Skip()

The if not self.IsTopLevel() could be replaced by a if self.proplevel > 0, in case you decide the frame, while being toplevel, is not the end of propagation.
There is no restriction to only ONE frame, not sure exactly how to make sure an event goes from one frame to another, but there IS the possibility of setting a self.prop_offset to a specific value (I'd guess, 1) .

Again, any insight, criticism, infos, commentary, remarks or questions are welcome.

@TJ-59
Copy link
Author

TJ-59 commented Oct 7, 2024

Also, I'd like to point out that maybe the EVT_KEY_DOWN and EVT_KEY_UP names are a bit misleading.
I was under the impression that it "should" stay down (and therefore fire only one down event) until it becomes "up" again,
but the "down" event keeps repeating like a character you'd like to repeat numerous times until you release the key.
The "up" event on the other hand, is only firing once per release.
And this happens with all keys, modifiers included.
Isn't it quite wasteful and imposing a heavy load on the event loop, for a mere "Control+something" or "Shift+something" keyboard shortcut ?

@TJ-59
Copy link
Author

TJ-59 commented Oct 11, 2024

I figured I could give it a try with something working top-down like your EventPropagator, with some modifications from what I saw during my previous try.

The BumpHandler
class BaseBumpHandler(wx.EvtHandler):
    """
    Base class for BumpHandlers
    """
    def __init__(self,win,evtlist,offset=0,exclude=None):
        wx.EvtHandler.__init__(self)
        self.win = win
        self.evtlist = set(evtlist)
        self.offset = offset
        self.exclude = exclude
        
        self.proplevel = self.calcPropLevel()
        
        if not self.win.IsTopLevel():
            for eventtype in self.evtlist :
                self.Bind(eventtype, self.BumpEvent)
            self.Register()     # starting the propagation automatically ONLY on subwindows
                            # This allows to place it early in your frame __init__ and choose when to Register()
                
    def BumpEvent(self,event):
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def calcPropLevel(self):    # This one might look overkill (I mean, it's just self.offset), but this allows subclasses to override the things done in the subclass and/or work differently.
        # Could be "if self.win.IsTopLevel():" or any other conditions
        return self.offset

    def Register(self):
        """
        if provided, exclude must be a list of types to exclude from this propagation.
        """
        if self.exclude : # This Register part could be modified for other means of exclusion from the propagation, like the window's name instead of type, or anything else, really.
            children = [kid for kid in self.win.GetChildren() if type(kid) not in exclude]
        else :
            children = list(self.win.GetChildren())
        for child in children:
            n = self.proplevel + 1
            child.PushEventHandler(self.__class__(child,self.evtlist,offset=n,exclude=self.exclude))

Using it is quite simple,

class MyBumpHandler(BaseBumpHandler):
    pass

 # Somewhere in your constants
events_to_bump_for_MyFrame = [wx.EVT_KEY_DOWN,wx.EVT_KEY_UP]

 # and when you define your MyFrame class

class MyFrame(wx.Frame):
    def __init__(self,...):
        super().__init__(...)
        # do your sizers, panels, widgets, binds...
        self.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
        self.Bind(wx.EVT_KEY_UP, self.on_key_up)

        # then create the bumper and Register() it
        self.bumper = MyBumpHandler(self, events_to_bump_for_MyFrame)
        self.bumper.Register()

        # probably the last thing you do here, Show()
        self.Show()

    def on_key_down(self,event):
        print("Frame : on_key_down")
        event.Skip()   # or not, depends on your needs

    def on_key_up(self,event):
        print("Frame : on_key_up")
        event.Skip()   # or not, depends on your needs

It basically reduce the thing to :

  1. Put the base class and subclass it if needed,
  2. Make a list of the Events types you want to be bumped along
  3. In the Frame, create an instance of it, giving it the self (for the window), and the event type list, then Register() it when you're ready to go.

The self.__class__ part in the Register() function allow users to "not have to redefine it" for subclasses, and you can see that each child (that is not excluded) gets its own instance of the very same class, with everything it has to know (event type list, propagation level, exclude list...) given to it.
Since they are not toplevels, they get automatically "registered" as well. and that behavior can be changed within a subclass as well.

Feel free to comment/criticize/question on this iteration as well, and my previous questions about the automatic repeating of the EVT_KEY_DOWN still stand : It does feel strange to fire that many events for something that is, quite literally, a NON-event.

@paul-ollis
Copy link

If my understanding is correct, you wish to detect when the Control key is
pressed and when it is subsequently released. And you wish to do this
regardless of which widget is currently focused, which means that EVT_KEY_UP
and EVT_KEY_DOWN are not appropriate.

You can achieve this using a combination of EVT_CHAR_HOOK and a timer. The hook
will trigger when a key is pressed, but not when it is released. The timer is
used to detect when the Control key is released.

A minimal example is for this is:

import wx

class ControlReleasedTimer(wx.Timer):

    def __init__(self, handler):
        super().__init__()
        self.handler = handler
        self.Start(milliseconds=50)

    def Notify(self):
        if not wx.GetMouseState().ControlDown():
            self.Stop()
            self.handler.on_control_released()

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="Keys example", size=(350,200))
        self.Bind(wx.EVT_CHAR_HOOK, self.on_hooked_char)
        self.control_timer = None

        # An empty frame does not 'see' key/char events. So add a Panel.
        self.child = wx.Panel(self)

    def on_hooked_char(self, event):
        mod = wx.KeyboardState()
        if event.GetKeyCode() == wx.WXK_CONTROL:
            print("Control Down")
            self.control_timer = ControlReleasedTimer(self)

    def on_control_released(self):
        print("Control Up")

app = wx.App(redirect=False)
top = MyFrame()
top.Show()
app.MainLoop()

I have run this under Linux and Windows, with wxPython 4.2.1.

@TJ-59
Copy link
Author

TJ-59 commented Oct 15, 2024

I'll have a look at that EVT_CHAR_HOOK too, but for now, here is the latest version of the BumpHandler :

Full test and annotations for BumpHandler
import wx



class BaseBumpHandler(wx.EvtHandler):
    """
    Base class for BumpHandlers
    """
    def __init__(self,win,evtlist,offset=0,exclude=None):
        wx.EvtHandler.__init__(self)
        self.win = win
        self.evtlist = set(evtlist)
        self.offset = offset
        self.exclude = exclude
        
        self.proplevel = self.calcPropLevel()

        self.Bind(wx.EVT_CLOSE,self.OnClose)
        
        if not self.win.IsTopLevel() or self.proplevel > 0:
            print("Associated to a non-toplevel, or toplevel in a multi-toplevels app...") #TEST
            for eventtype in self.evtlist :
                print(f"Event type to be bumped : {eventtype}") #TEST
                self.Bind(eventtype, self.BumpEvent)
            self.Register()     # starting the propagation automatically ONLY on subwindows
                                # This allows to place it early in your frame __init__ and choose when to Register()
        if self.win.IsTopLevel():
            self.Bind(wx.EVT_CLOSE,self.OnClose) # Only toplevels should really care for EVT_CLOSE
            self.win.PushEventHandler(self) # Auto pushing this "limited self" onto the main frame.
                                            # If you are not starting it from the frame level,
                                            # don't forget to RemoveEventHandlers() when needed.

    def OnClose(self, event):
        print("OnClose started in {self.win}'s {self.__class__}")
        self.RemoveEventHandlers()
        print("Just out of RemoveEventHandlers, back in OnClose")
        # event.Skip() causes Python to crash ("self" has just been removed by REH, we're still inside the collapsing bubble)
        wx.PostEvent(self.win,event)    #So we use this instead
        # Think of "I,Robot" Dr Lanning's "Everything that follows, is the result of what you see here."
                                        # "Is there something you want to tell me ?"
                                        # "I'm sorry, my responses are limited, you must ask the right questions."
                                        # "Why did you __call__() me ?"
                                        # "I trust your judgment."
                                        # "Normally these circumstances wouldn't require a <wx.PostEvent>."
                                        # "But then, our interactions have never been entirely normal..."

    def RemoveEventHandlers(self, win=None): # REH for short
        if win is None:
            win = self.win
        print(f"Entering REH for {win.GetName()}...") #TEST
        print("Getting into children list...") #TEST
        for child in list(win.GetChildren()):
            print(f"Using REH on {child.GetName()}") #TEST
            child.GetEventHandler().RemoveEventHandlers()
        print(f"Popping handler in {win.GetName()}") #TEST
        try:
            win.PopEventHandler(True)
        except wx._core.wxAssertionError as e:
            print(f"Could not Pop {self.__class__} in {self.win.GetName()}") #TEST
            print(f"{e}") #TEST
            print(f"Type : {type(e)}") #TEST
        else :
            print(f"Just Popped {self.__class__} in {self.win.GetName()}") #TEST

    def BumpEvent(self,event):
        print("BumpEvent : bumping a {} event in {}".format(event.GetEventType(),event.GetEventObject())) #TEST
        event.ResumePropagation(self.proplevel)
        event.Skip()

    def calcPropLevel(self):
        return self.offset

    def Register(self):
        """
        if provided, exclude must be a list of types to exclude from this propagation.
        """
        if self.exclude :
            children = [kid for kid in self.win.GetChildren() if type(kid) not in self.exclude]
        else :
            children = list(self.win.GetChildren())
        for child in children:
            n = self.proplevel + 1
            child.PushEventHandler(self.__class__(child,self.evtlist,offset=n,exclude=self.exclude))

class MyBumpHandler(BaseBumpHandler):
    pass # Here, it's just the very same, but we could be overriding functions.
    # As an example, an exclude based not upon type, but upon name instead, would look like this :
#   def Register(self):
#       """
#       This version require a list of strings that excludes the child if found inside the child's name
#       """
#       if self.exclude :
#           children = [kid for kid in self.win.GetChildren() if all(excluded not in kid.GetName() for excluded in self.exclude)]
#       else :
#           children = list(self.win.GetChildren())
#       for child in children:
#           n = self.proplevel + 1
#           child.PushEventHandler(self.__class__(child,self.evtlist,offset=n,exclude=self.exclude))


class MyPanelWithWidgets(wx.Panel):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.PWWsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.PWWsizer)
        self.text = wx.TextCtrl(self,value=str(self.GetName()))
        self.PWWsizer.Add(self.text, 0, wx.CENTER | wx.ALL,3)
        self.Bind(wx.EVT_LEFT_DOWN,self.on_mouse_left_down)
        self.Bind(wx.EVT_LEFT_UP,self.on_mouse_left_up)

    def on_mouse_left_down(self,event):
        print("[Local]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))
        event.Skip()

    def on_mouse_left_up(self,event):
        print("[Local]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))
        event.Skip()


class MyPanel(wx.Panel):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.SetBackgroundColour((255,255,0,255))
        self.panelsizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.panelsizer)
        self.pww1 = MyPanelWithWidgets(self,size=(-1,-1),name="pww1")
        self.pww1.SetBackgroundColour((255,0,0,255))
        self.panelsizer.Add(self.pww1,1,wx.EXPAND,5)
        self.pww2 = MyPanelWithWidgets(self,size=(-1,-1),name="pww2")
        self.pww2.SetBackgroundColour((0,255,0,255))
        self.panelsizer.Add(self.pww2,1,wx.EXPAND,5)
        self.pww3 = MyPanelWithWidgets(self,size=(-1,-1),name="pww3")
        self.pww3.SetBackgroundColour((0,0,255,255))
        self.panelsizer.Add(self.pww3,1,wx.EXPAND,5)
        self.Bind(wx.EVT_LEFT_DOWN,self.on_panel_mouse_left_down)  # test commenting out those two binds
        self.Bind(wx.EVT_LEFT_UP,self.on_panel_mouse_left_up)      # to see the bumps in action

    def on_panel_mouse_left_down(self,event):
        print("[Panel]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))
        event.Skip()    # Remember, ALWAYS Skip() the event unless you want odd behavior
                        # that look like a failed validator, and no event coming to the widgets.
                        # If you don't need to catch it at this level, it's okay, the bumper still works,
                        # you just don't have to bind it (and therefore do not need this bound function).

    def on_panel_mouse_left_up(self,event):
        print("[Panel]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))
        event.Skip()    # idem as above.


class MyFrame(wx.Frame):
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        #self.prop_offset = 0
        self.mybumper = MyBumpHandler(self, myeventlist) # exclude=myexcludelist can be added here.
        self.framesizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(self.framesizer)
        self.panel = MyPanel(self,size=(-1,-1),name="mainpanel")
        self.framesizer.Add(self.panel, 1,wx.EXPAND, wx.ALL,5)

        self.Bind(wx.EVT_LEFT_DOWN,self.on_frame_mouse_left_down)
        self.Bind(wx.EVT_LEFT_UP,self.on_frame_mouse_left_up)
        self.Bind(wx.EVT_RIGHT_DOWN,self.on_frame_mouse_right_down)
        self.Bind(wx.EVT_RIGHT_UP,self.on_frame_mouse_right_up)
        self.Bind(wx.EVT_KEY_DOWN,self.on_frame_key_down)
        self.Bind(wx.EVT_KEY_UP,self.on_frame_key_up)
        self.Bind(wx.EVT_CLOSE,self.on_frame_close)
        self.mybumper.Register()

    def on_frame_mouse_left_down(self,event):
        print("[Frame]Mouse Left DOWN in {}".format(event.GetEventObject().GetName()))
        event.Skip()

    def on_frame_mouse_left_up(self,event):
        print("[Frame]Mouse Left -UP- in {}".format(event.GetEventObject().GetName()))
        event.Skip()

    def on_frame_mouse_right_down(self,event):
        print("[Frame]Mouse RIGHT DOWN in {}".format(event.GetEventObject().GetName()))

    def on_frame_mouse_right_up(self,event):
        print("[Frame]Mouse RIGHT -UP- in {}".format(event.GetEventObject().GetName()))

    def on_frame_key_down(self, event):
        print("[Frame]Key DOWN : {} in {}".format(event.GetKeyCode(),event.GetEventObject().GetName()))
        print("    ==> Ctrl Down : ", event.ControlDown())
        print("    ==> Shift Down : ", event.ShiftDown())
        event.Skip()

    def on_frame_key_up(self, event):
        print("[Frame]Key -UP- : {} in {}".format(event.GetKeyCode(),event.GetEventObject().GetName()))
        print("    ==> Ctrl Down : ", event.ControlDown())
        print("    ==> Shift Down : ", event.ShiftDown())
        event.Skip()

    def on_frame_close(self,event):
        print("Doing the usual closing and cleanup for the app and frame, before Destroy()")
        wx.CallAfter(self.Destroy)
        event.Skip()
        print("End of on_frame_close")


if __name__ == '__main__':
    app = wx.App(False)
    myeventlist = [wx.EVT_LEFT_DOWN,wx.EVT_LEFT_UP,wx.EVT_RIGHT_DOWN,wx.EVT_RIGHT_UP,wx.EVT_KEY_DOWN,wx.EVT_KEY_UP]
    #myeventlist = [wx.EVT_RIGHT_DOWN,wx.EVT_RIGHT_UP,wx.EVT_KEY_DOWN,wx.EVT_KEY_UP] # same without mouse_left
    myexcludelist = [wx.TextCtrl]
    frame = MyFrame(None,-1,"propagation test with BumpHandler",size=(400,200))
    frame.Show()
    frame.SetFocus() #purely to avoid giving the focus automatically to one of the TextCtrl
    app.MainLoop()

As shown in the commented areas, this allows bumping selected events upward, and as long as you do not forget to Skip(), it should reach its original destination.
It also allows for overrides if you need a specific behavior, as shown in the commented area under MyBumpHandler, where an alternate exclude method is demonstrated.
Note that for simplicity of use, it COULD have another layer of function in the form of a :
children = self.GetIncludedChildren() call instead of the whole children = [kid for kid in self.win.GetChildren() if all(excluded not in kid.GetName() for excluded in self.exclude)] line, with

def GetIncludedChildren(self):
    kids = [kid for kid in self.win.GetChildren() if all(excluded not in kid.GetName() for excluded in self.exclude)]
    return kids

which would allow users to only modify the "exclude condition" part in overrides.

As usual, any comment/question/correction/insight is welcome.

@paul-ollis
Copy link

I do not have any specific comments on the BumpHandler.

Does my suggestion in my previous comment solve the original problem that prompted this to be raised? If it does then I think that this issue can move towards being closed. The BumpHandler idea then (I think) becomes something for https://discuss.wxpython.org/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants