wxWidgets / Phoenix

wxPython's Project Phoenix. A new implementation of wxPython, better, stronger, faster than he was before.
http://wxpython.org/
2.29k stars 516 forks source link

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

Open TJ-59 opened 17 hours ago

TJ-59 commented 17 hours ago

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) ```python 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 commented 16 hours ago

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 commented 2 hours ago

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 commented 1 hour ago

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.