MenuBar, SpinCtrl and Copy

I have a MenuBar set up to catch Ctrl+c however this stops the SpinCtrl copy behaviour from working.
Testing with a TextCtrl works as expected. (the text is copied and the MenuBar event is not run)

I would expect that the events on the active UI element would take priority over the MenuBar items and at the very least this behaviour would be consistent.

I am using Windows 10 and wxPython 4.1.1

Here is some demo code that reproduces the issue.

import wx


class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.create_menu()
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)
        self.text_ctrl = wx.TextCtrl(self)  # copy works here
        sizer.Add(self.text_ctrl)
        self.spin_ctrl = wx.SpinCtrl(self)  # copy does not work here
        sizer.Add(self.spin_ctrl)

    def create_menu(self):
        menu_bar = wx.MenuBar()
        menu = wx.Menu()
        # commenting out the two lines below makes it work as expected
        menu_item = menu.Append(wx.ID_ANY, "copy\tCtrl+c")
        self.Bind(wx.EVT_MENU, self.copy, menu_item)
        menu_bar.Append(menu, "edit")
        self.SetMenuBar(menu_bar)

    def copy(self, evt):
        print("copy")


def main():
    app = wx.App(0)
    frame = Frame(None)
    frame.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

I think it’s a case of too many cooks spoil the broth, but EVT_CHAR_HOOK may lead out of the jungle :rofl:

Hi gentlegiantJGC,

As @da-dada implies, You can hook EVT_CHAR_HOOK event on the SpinCtrl object and choose whether it propagates to the parent frame and the menu.

The propagation of key events is as follows:

EVT_            Spin    Panel   Frame   Menu
----------------------------------------------
EVT_CHAR_HOOK   (0) --> (1) --> (2) --> (3) ┬> (4)
EVT_KEY_DOWN    (5) <-----------------------┘

where --> indicates propagation by calling event.Skip.

(0,1,2) EVT_CHAR_HOOK propagates from child to parent and parent to grandparent…
(3) If you don’t want to handle it in the menu, you only need to stop calling Skip.
(4) When menu handles the event, it won’t propagates any more, but,
(5) when menu doesn’t handle the event, EVT_KEY_DOWN is fired.

1 Like

I think the best solution would be, not to use [C-c] as a menu shortcut.
The second way I can think of is:

        def hook_char(evt):
            key = evt.GetKeyCode()
            if evt.ControlDown() and key in (ord('C'), ord('c')):
                wx.UIActionSimulator().KeyDown(wx.WXK_CONTROL_C) # emulates [C-c]
                return
            evt.Skip()
        self.spin_ctrl.Bind(wx.EVT_CHAR_HOOK, hook_char)

Thanks for the responses.
I have implemented this and it does work for the copy command however I need to do the same with cut and paste however these do not behave the same as the copy operation.

The following works as expected. The text gets copied to the clipboard. For completeness (and I don’t know why) doing this generates an EVT_CHAR_HOOK with value WXK_CANCEL
wx.UIActionSimulator().KeyDown(wx.WXK_CONTROL_C)

The cut and paste versions of this generate EVT_CHAR_HOOK events with values 88 (x) and 86 (v) with control not pressed. They do not do the actual cut or paste in the window.
wx.UIActionSimulator().KeyDown(wx.WXK_CONTROL_X)
wx.UIActionSimulator().KeyDown(wx.WXK_CONTROL_V)

I have tried manually specifying the control key however this generates the char hook event with the control key pressed which is caught by the handler and triggered again causing a cyclic calling of the handler and the actual cut/paste is not applied.
wx.UIActionSimulator().KeyDown(88, modifiers=wx.MOD_CONTROL)
wx.UIActionSimulator().KeyDown(86, modifiers=wx.MOD_CONTROL)

Edit: I got this slightly wrong. I have updated it to correct it

Adding to this the docs say that not calling skip on the char hook event should stop it from propagating and that DoAllowNextEvent should allow normal event creation after the char hook event.
It seems that DoAllowNextEvent continues the propagation up the UI tree even if skip was not called.
Based on the description I would have though that would stop it propagating and generate the key down and char events as normal. Not sure if that would cause the actual copy/cut/paste event to work normally even if it did work since I don’t know how the copy operation is triggered.

before you get overheated what about just binding this one-liner event earther to your control (after all this should be python :dragon:)

self.spin_ctrl.Bind(wx.EVT_CHAR_HOOK, lambda e: e.Skip(not e.cmdDown))

The document says,

wx.KeyEvent — wxPython Phoenix 4.1.2a1 documentation
However by calling the special DoAllowNextEvent method you can handle wxEVT_CHAR_HOOK and still allow normal events generation. This is something that is rarely useful but can be required if you need to prevent a parent wxEVT_CHAR_HOOK handler from running without suppressing the normal key events.

I didn’t notice the method, and it sounds to me too that it should work as you expected, but unfortunately no… :worried: No difference between ‘Skip’ and 'DoAllowNextEvent`?

I tested the following code (I thought it should work):

        def hook_char(evt):
            key = evt.GetKeyCode()
            if evt.ControlDown() and key in map(ord, 'xXcCvV'):
                evt.DoAllowNextEvent()
                return
            evt.Skip()

with no luck.

Why only wx.WXK_CONTROL_C emulates (accidentally) well is because, I guess, [C-c] is the system exit key.

Not only skipping the event but also handling the default action of the widget is required, which seems difficult… :thinking:

EDIT
TextCtrl do this right. I think the second solution is to make the composite panel of TextCtrl and SpinButton.

This is starting to sound like quite a complex problem.

This would block those events but the actual copy/cut/paste event would also not happen. I am starting to think this might be the best solution though.

I was wondering why it was only this that worked. That explains why it was generating the WXK_CANCEL char hook event.

Yeh I noted that in my first post. Not sure why it works there and not in the SpinCtrl.

I looked into emulating the copy/cut/paste/delete command through text modification but I don’t see any way of inspecting the selection like you can with a TextCtrl.

Edit: I have just opened a bug report here https://github.com/wxWidgets/Phoenix/issues/2148

well, I’m sure we are not out of the bushes yet and, as far as I am concerned, there is not much complexity
clipboard action may be created anywhere by simply using wx.Clipboard (and some programming, of course)
with a textual widget like wx.TextCtrl this is a default feature (provided by wx.TextEntry)
the Ctrl-c is a mixed feature and the app must decide on its usage (I thought it was in the way and grounded it, others may write a lengthy documentation that a copy on a spin control doesn’t make all that much sense)

OK, it will probably take some time until the fix will find it’s way into wxPython. It should be in wxWidgets 3.1.7.
https://github.com/wxWidgets/wxWidgets/pull/22397

Anyway, if you are desparate, you can get the data via win32 calls:

  • enumerate child windows of frame
  • identify the text control part of the spin control; it’ s between the text and the spin control
  • read the selection range and text content via Windows messages
>>> import win32con, win32api, win32gui

>>> self.text_ctrl.GetHandle(), self.spin_ctrl.GetHandle()
(657974, 3672668)


def on_child(hwnd, lParam):
    print(hwnd, lParam)

>>> win32gui.EnumChildWindows(self.GetHandle(), on_child, None)

657974 None
2755380 None
3672668 None

# 2755380 is the text ctrl component of the SpinCtrl


>>> selection = win32gui.SendMessage(2755380, win32con.EM_GETSEL, 0, 0)
>>> wwin32api.LOWORD(selinfo), win32api.HIWORD(selinfo)
(0, 1)

# in the example, the first (and only) character is selected


# from here, you can continue with WM_GETTEXT`
>>> buffer = win32gui.PyMakeBuffer(1024)
>>> win32gui.SendMessage(2755380, win32con.WM_GETTEXT, 1024, buffer)
>>> ...

if you kick off a mend it looks as though that textural controls having wx.TextEntry integrated behave as expected (may be it’s just a copy & paste :slightly_smiling_face:)

import wx


class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.create_menu()
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.SetSizer(sizer)
        ctrl = wx.TextCtrl(self)
        sizer.Add(ctrl)
        ctrl = wx.SpinCtrl(self)  # copy does not work here
        sizer.Add(ctrl)
        ctrl = wx.SearchCtrl(self)
        sizer.Add(ctrl)
        ctrl = wx.ComboBox(self)
        sizer.Add(ctrl)

    def create_menu(self):
        menu_bar = wx.MenuBar()
        menu = wx.Menu()
        # commenting out the two lines below makes it work as expected
        menu_item = menu.Append(wx.ID_ANY, "copy\tCtrl+c")
        self.Bind(wx.EVT_MENU, self.copy, menu_item)
        menu_bar.Append(menu, "edit")
        self.SetMenuBar(menu_bar)

    def copy(self, evt):
        print("copy")


def main():
    app = wx.App(0)
    Frame(None).Show()
    # from wx.lib.inspection import InspectionTool
    # InspectionTool().Show()
    app.MainLoop()