[RESOLVED] Panda in GTK: problem with some mouse events

Hi Panda people!

It’s been some time I wanted to display the Panda window in a gtk Widget to improve the GUI of the things I develop with it. I understood how to do it by stuying astelix’s code for wx given in this thread :
https://discourse.panda3d.org/viewtopic.php?t=5728&highlight=p3ddojo

I wrote a small sample to help me explain my problem (and help those who would be interested in this). This create a basic GTK window with some boxes (you can change this and load another from a glade if you want), and draw the Panda Window on a GtkDrawingArea.
N.B. : At the begining the Panda Window can be badly resized so you juste have to resize the window to make it correct.

import gtk
import sys
import platform
# == 
from pandac.PandaModules          import loadPrcFileData, WindowProperties
from direct.task                  import Task
from direct.showbase.DirectObject import DirectObject

# == PANDA SETUP == #

def setup() :
    # We don't want the basic window to open
    loadPrcFileData("", "window-type none")

def launch_panda_window(panda_widget, size) :
    """
    Configure and create Panda window
    Connect to the gtk widget resize event
    Load a panda
    """
    props = WindowProperties().getDefault()
    props.setOrigin(0, 0)
    props.setSize(*size)
    props.setParentWindow(get_widget_id(panda_widget))
    base.openDefaultWindow(props=props)
    # ==
    panda_widget.connect("size_allocate", resize_panda_window)
    # ==
    panda = loader.loadModel("panda")
    panda.reparentTo(render)
    panda.setPos(0, 40, -5)

def resize_panda_window(widget, request) :
    """ Connected to resize event of the widget Panda is draw on so that the Panda window update its size """
    props = WindowProperties().getDefault()
    props.setOrigin(0, 0)
    props.setSize(request.width, request.height)
    base.win.requestProperties(props)

def setup_events() :
    """ Just to print mouse events """
    obj = DirectObject()
    obj.accept("mouse1"    , print_info, ["Left press"])
    obj.accept("mouse1-up" , print_info, ["Left release"])
    obj.accept("mouse2"    , print_info, ["Wheel press"])
    obj.accept("mouse2-up" , print_info, ["Wheel release"])
    obj.accept("mouse3"    , print_info, ["Right press"])
    obj.accept("mouse3-up" , print_info, ["Right release"])
    obj.accept("wheel_up"  , print_info, ["Scrolling up"])
    obj.accept("wheel_down", print_info, ["Scrolling down"])
    return obj

# == GTK SETUP == #

def get_widget_id(widget) :
    """ Retrieve gtk widget ID to tell the Panda window to draw on it """
    if platform.system() == "Windows" : return widget.window.handle
    else                              : return widget.window.xid

def gtk_iteration(*args, **kw):
    """ We handle the gtk events in this task added to Panda TaskManager """
    while gtk.events_pending():
        gtk.main_iteration_do(False)
    return Task.cont

def launch_gtk_window(size) :
    """
    Here we create the gtk window and all its content (usually I load it from a gladefile).
    We return the widget where we want the Panda Window to be drawn
    """
    # == Main window
    window = gtk.Window()
    window.resize(*size)
    # == A VBox in which we'll put 2 HBox
    vbox = gtk.VBox()
    window.add(vbox)
    # == First HBox with just a Button in it
    top_hbox = gtk.HBox()
    vbox.pack_start(top_hbox, expand = False)
    # ==
    image = gtk.Image()
    image.set_from_stock(gtk.STOCK_OK, gtk.ICON_SIZE_BUTTON)
    button = gtk.Button()
    button.set_image(image)
    button.connect("clicked", toggle_print_mouse_infos)
    top_hbox.pack_start(button)
    # == Second HBox with some gtk thing on the left and the area for Panda on the right
    bottom_hbox = gtk.HBox()
    vbox.pack_end(bottom_hbox, expand = True)
    # ==
    left_panel = gtk.TreeView()
    left_panel.set_model(gtk.TreeStore(str, object, object))
    column = gtk.TreeViewColumn("Name", gtk.CellRendererText(), text = 0)
    column.set_sort_column_id(0)
    left_panel.append_column(column)
    # ==
    bottom_hbox.pack_start(left_panel, expand = False)
    # ==
    drawing_area = gtk.DrawingArea()
    bottom_hbox.pack_end(drawing_area)
    # ==
    def quit(*args, **kw) :
        window.destroy()
        sys.exit(0)
    # ==
    window.connect("delete-event", quit)
    window.show_all()
    # ==
    gtk_iteration()
    # ==
    return drawing_area

# == INFO == #

mouse_info = False
def toggle_print_mouse_infos(*args, **kw) :
    global mouse_info
    mouse_info = not mouse_info
    print "\n\n Printing Mouse Infos : %s \n\n" % mouse_info

def mouse_info_task(*args, **kw) :
    global mouse_info
    if mouse_info :
        md = base.win.getPointer(0)
        print "Mouse in window: ", md.getInWindow()
        print "Mouse position : ", md.getX(), md.getY()
    # ==
    return Task.cont

def print_info(message) :
    print message

# == MAIN == #

def main() :
    setup()
    # ==
    import direct.directbase.DirectStart
    # ==
    size         = (800, 600)
    drawing_area = launch_gtk_window(size)
    launch_panda_window(drawing_area, size)
    # ==
    direct_object = setup_events()
    taskMgr.add(mouse_info_task, "print info")
    # == 
    taskMgr.add(gtk_iteration, "gtk")
    # ==
    run()

if __name__ == '__main__':
    main()

Not that complicated in fact. When we do this, there are some things to handle differently.

1°) Keyboard events are not caught by Panda but by GTK but with an GtkAccelGroup on the window catching keyboard inputs in GTK works kinda great.

2°) On the contrary, Mouse events aren’t caught by GTK but Panda when the mouse is above the Panda window.
But there are some differences between Linux and Windows. 2 examples:

  • I have a DirectObject accepting wheelp-up and wheel-down events. It works on Linux but not on WIndows.
  • Other difference, on Windows, the base.win.movePointer function works good, but on Linux, it only works when a mouse button is clicked.
  • On linux, when a mouse button is clicked of the wheel turned, MouseData.getInWindow return False.

So point 1 is not annoying for me because, like I said, AccelGroup works great.
But point 2 is really a problem because I can’t use the mouse as I did when Panda wasn’t in GTK.

I’m digging to see if thiese problem comes from GTK, Panda or OS Windows system but if anyone has an idea, it’d wee grateful for life :wink:

Benjamin

w00t! man if this is true you pulled out a tons stone off my foot - this was a major PITA for making my Dojo app working on Windows. I gotta check this out ASAP!

Cool if this helps someone. And, as it’s studying your code that helped me display Panda in GTK, I’m even more glad :smiley:

a little precision:
in windows, the wheel-up and wheel-down events are correctly caught. But if the program loose focus then regain it, the events are no longer caught.

still more about it under windows.

if I create my window with

openWindow(props=props)

the scroll event ( wheel-up/wheel-down) is not catch.
if I create it with openMainWindow or OpenDefaultWindow, then the scroll event is catch.

After I loose and restore focus, I’m in the same situation, I can recreate a window (

base.openMainWindow(props=WindowsProperties(base.win.getProperties))

and I get the event only if I do it with openMainWindow or openDefaultWindow, not OpenWindow.

so after some try, I found that

base.openWindow()
base.setupMouse(base.win)

give me the wheel-up event working in my initialisation
but not if I try to do it after ( when my window restaure focus.) or if I just do it without recreating a window

 base.win.requestProperties(props)
base.setupMouse(base.win)

strange, very strange

Hi Panda people!

I updated my code so that now:

    • it uses the GtkAccelGroup I was talking about the other day
    • it reflects Sylhar’s but report about lost focus and scroll
    • the pointer can be moved.

For point 1), on Windows, shortcuts work only when the Panda Windows lost the focus. See just below how to lose it.

For points 1) and 2), to lose the focus:

  • launch the script on Windows
  • check that scroll events are caught
  • click on your terminal or any other window
  • check again scroll events: there aren’t caught anymore
  • Note that astelix talks about the same problem on wx in this thread https://discourse.panda3d.org/viewtopic.php?t=6089

For point 3), I made a function different on Windows and Linux.

For point 3) again, if you don’t want to use these packages, you can do it “gtk-style”. The code is commented in my script.

Here’s the new version of the script.
Use the “m” key to move the pointer of the center of the panda widget.
Use the “p” key to print a message in the terminal.

import gtk, gtk.glade
import sys
import platform
from functools import partial
# == 
from pandac.PandaModules          import loadPrcFileData, WindowProperties
from direct.task                  import Task
from direct.showbase.DirectObject import DirectObject


if platform.system() == "Windows" :

    import win32api, win32gui

    def move_pointer(window, panda_widget, pos) :
        (left, top, right, bottom) = win32gui.GetWindowRect(get_widget_id(panda_widget))
        win32api.SetCursorPos((left + int(pos[0]), top + int(pos[1])))

elif platform.system() == "Linux" :

    from Xlib import display

    disp   = display.Display()
    screen = disp.screen()

    def move_pointer(window, panda_widget, pos) :
        global disp, screen
        # ==
        win_x, win_y = window.window.get_position()
        x, y         = panda_widget.window.get_position()
        screen.root.warp_pointer(win_x + x + pos[0], win_y + y + pos[1]) # Could use keywords like "src_window" but not succeeded yet
        disp.sync()


# gtk style, test on Linux 
#def move_pointer(window, panda_widget, pos) :
#    disp = panda_widget.get_display()
#    x, y = panda_widget.get_position()
#    disp.warp_pointer(panda_widget.get_screen(), x + pos[0], y + pos[1])

# == PANDA SETUP == #

def setup() :
    # We don't want the basic window to open
    loadPrcFileData("", "window-type none")

def launch_panda_window(panda_widget, size) :
    """
    Configure and create Panda window
    Connect to the gtk widget resize event
    Load a panda
    """
    props = WindowProperties().getDefault()
    props.setOrigin(0, 0)
    props.setSize(*size)
    props.setParentWindow(get_widget_id(panda_widget))
    base.openDefaultWindow(props=props)
    # ==
    panda_widget.connect("size_allocate", resize_panda_window)
    # ==
    panda = loader.loadModel("panda")
    panda.reparentTo(render)
    panda.setPos(0, 40, -5)

def resize_panda_window(widget, request) :
    """ Connected to resize event of the widget Panda is draw on so that the Panda window update its size """
    props = WindowProperties().getDefault()
    props = WindowProperties(base.win.getProperties())
    props.setOrigin(0, 0)
    props.setSize(request.width, request.height)
    props.setParentWindow(get_widget_id(widget))
    base.win.requestProperties(props)

def setup_panda_events(window) :
    """ Setup mouse events in Panda """
    obj = DirectObject()
    obj.accept("mouse1"    , print_info, ["Left press"])
    obj.accept("mouse1-up" , print_info, ["Left release"])
    obj.accept("mouse2"    , print_info, ["Wheel press"])
    obj.accept("mouse2-up" , print_info, ["Wheel release"])
    obj.accept("mouse3"    , print_info, ["Right press"])
    obj.accept("mouse3-up" , print_info, ["Right release"])
    obj.accept("wheel_up"  , print_info, ["Scrolling up"])
    obj.accept("wheel_down", print_info, ["Scrolling down"])
    return obj

# == GTK SETUP == #

def get_widget_id(widget) :
    """ Retrieve gtk widget ID to tell the Panda window to draw on it """
    if platform.system() == "Windows" : return widget.window.handle
    else                              : return widget.window.xid

def gtk_iteration(*args, **kw):
    """ We handle the gtk events in this task added to Panda TaskManager """
    while gtk.events_pending():
        gtk.main_iteration_do(False)
    return Task.cont

def launch_gtk_window(size) :
    """
    Here we create the gtk window and all its content (usually I load it from a gladefile).
    We return the widget where we want the Panda Window to be drawn
    """
    # == Main window
    window = gtk.Window()
    window.resize(*size)
    # == A VBox in which we'll put 2 HBox
    vbox = gtk.VBox()
    window.add(vbox)
    # == First HBox with just a Button in it
    top_hbox = gtk.HBox()
    vbox.pack_start(top_hbox, expand = False)
    # ==
    image = gtk.Image()
    image.set_from_stock(gtk.STOCK_OK, gtk.ICON_SIZE_BUTTON)
    button = gtk.Button()
    button.set_image(image)
    button.connect("clicked", toggle_print_mouse_infos)
    top_hbox.pack_start(button)
    # == Second HBox with some gtk thing on the left and the area for Panda on the right
    bottom_hbox = gtk.HBox()
    vbox.pack_end(bottom_hbox, expand = True)
    # ==
    left_panel = gtk.TreeView()
    left_panel.set_model(gtk.TreeStore(str, object, object))
    column = gtk.TreeViewColumn("Name", gtk.CellRendererText(), text = 0)
    column.set_sort_column_id(0)
    left_panel.append_column(column)
    # ==
    bottom_hbox.pack_start(left_panel, expand = False)
    # ==
    drawing_area = gtk.DrawingArea()
    bottom_hbox.pack_end(drawing_area)
    # ==
    def quit(*args, **kw) :
        window.destroy()
        sys.exit(0)
    # ==
    window.connect("delete-event", quit)
    window.show_all()
    # ==
    gtk_iteration() # Needed to create all the XWindows of the gtk widgets and be able to get the xid/handle
    # ==
    return window, drawing_area

def add_gtk_shortcuts(window, actions) :
    # Create the accel group and add it to the window
    accel_group = gtk.AccelGroup()
    window.add_accel_group(accel_group)
    # Create the action group
    action_group = gtk.ActionGroup('ActionGroup')
    # ==
    # Could give the list of action to add_toggle_actions method all at once and then connect each one
    for action in actions :
        action_group.add_toggle_actions([action])
        gtk_action = action_group.get_action(action[0])
        gtk_action.set_accel_group(accel_group)
        gtk_action.connect_accelerator()
    # ==
    return accel_group, action_group

# == INFO == #

mouse_info = False
def toggle_print_mouse_infos(*args, **kw) :
    global mouse_info
    mouse_info = not mouse_info
    print "\n\n Printing Mouse Infos : %s \n\n" % mouse_info

def mouse_info_task(*args, **kw) :
    global mouse_info
    if mouse_info :
        md = base.win.getPointer(0)
        print "Mouse in window: ", md.getInWindow()
        print "Mouse position : ", md.getX(), md.getY()
    # ==
    return Task.cont

def print_info(message) :
    print message

# == MAIN == #

def main() :
    setup()
    # ==
    import direct.directbase.DirectStart
    # ==
    size                 = (800, 600)
    window, drawing_area = launch_gtk_window(size)
    launch_panda_window(drawing_area, size)
    base.disableMouse()
    # ==
    # Here we create the shortcuts/actions.
    # For more details, see http://www.pygtk.org/pygtk2reference/class-gtkactiongroup.html#method-gtkactiongroup--add-toggle-actions
    #
    # action = (name, stock_id, label, accelerator(=key), tooltip, callback, active_flag (optional)
    shortcuts = [
                 ("move" , None, "move" , "m", "move" , lambda _ : move_pointer(window, drawing_area, (size[0] / 2.0, size[1] / 2.0))),
                 ("print", None, "print", "p", "print", lambda _ : print_info("GTK print shortcut")),
                ]
    accel_group, action_group = add_gtk_shortcuts(window, shortcuts)
    # ==
    direct_object = setup_panda_events(window)
    taskMgr.add(mouse_info_task, "print info")
    # ==
    taskMgr.add(gtk_iteration, "gtk")
    # ==
    run()

if __name__ == '__main__':
    main()

So if anyone has any idea about this lost focus, I think that astelix and I would be more than happy to try it :smiley:

Thanks
Benjamin

I’ll now close this thread because we succeeded with the SetFocus solution given in astelix’s thread (https://discourse.panda3d.org/viewtopic.php?t=6089)? And some hack on keyboard events.

In fact, when there is a mouse 1, 2 or 3 event caught by panda (so on the panda window), we force the focus to come back to Panda with this code :

if platform.system() == "Windows" :
    handle = gtk_widget_where_panda_is_displayed.window.handle
    win_handle = win32gui.FindWindowEx(handle,0,None, name_of_panda_window)
    win32gui.SetFocus(win_handle)

So we retrieve the wheel events.

Plus each time we add a shortcut for the keyboard, we add it in a Panda DirectObject and in the AccelGroup I talked about before. Like that it works wheter Panda has the focus or not.

That does the trick for us for now.
Thanks for your answers.
See ya in a next thread :smiley:
Benjamin