From d567469f4d6d40301e46dafe9a2cffdb292b07af Mon Sep 17 00:00:00 2001 From: hordepfo Date: Wed, 22 Jan 2014 20:16:27 +0000 Subject: [PATCH] Changed all showing/hiding logic It seems most GTK apps like mintMenu and the default Gnome2/Mate menu use the GDK grab focus functions to grab input (pointer/keyboard). These functions appear to suck all input to the app like a blackhole. This patch gives more interactivity to the mintMenu window, making it a first class object in the desktop. Things that work now: * When the window is open, hovering on other desktop icons highlights them * Clicking outside the window propagates the event (e.g. select another window) * Alt-tab and other events unfocus and hide the window * Scrollbars in the mintMenu finally work right * Probably several other small things --- usr/lib/linuxmint/mintMenu/keybinding.py | 27 ++- usr/lib/linuxmint/mintMenu/mintMenu.glade | 2 +- usr/lib/linuxmint/mintMenu/mintMenu.py | 159 +++++++++--------- .../mintMenu/plugins/applications.py | 29 ++-- usr/lib/linuxmint/mintMenu/pointerMonitor.py | 84 +++++++++ 5 files changed, 200 insertions(+), 101 deletions(-) create mode 100644 usr/lib/linuxmint/mintMenu/pointerMonitor.py diff --git a/usr/lib/linuxmint/mintMenu/keybinding.py b/usr/lib/linuxmint/mintMenu/keybinding.py index 80f19c0..2a8eec3 100644 --- a/usr/lib/linuxmint/mintMenu/keybinding.py +++ b/usr/lib/linuxmint/mintMenu/keybinding.py @@ -58,10 +58,11 @@ class GlobalKeyBinding(GObject.GObject, threading.Thread): self.keymap = capi.get_widget (gdk.gdk_keymap_get_default()) self.display = Display() self.screen = self.display.screen() - self.root = self.screen.root + self.window = self.screen.root self.ignored_masks = self.get_mask_combinations(X.LockMask | X.Mod2Mask | X.Mod5Mask) self.map_modifiers() self.raw_keyval = None + self.keytext = "" def is_hotkey(self, key, modifier): keymatch = False @@ -101,21 +102,39 @@ class GlobalKeyBinding(GObject.GObject, threading.Thread): self.modifiers = None return False + self.keytext = key self.keycode = self.get_keycode(keyval) self.modifiers = int(modifiers) catch = error.CatchError(error.BadAccess) for ignored_mask in self.ignored_masks: mod = modifiers | ignored_mask - result = self.root.grab_key(self.keycode, mod, True, X.GrabModeAsync, X.GrabModeSync, onerror=catch) - self.display.sync() + result = self.window.grab_key(self.keycode, mod, True, X.GrabModeAsync, X.GrabModeSync, onerror=catch) + self.display.flush() + # sync has been blocking. Don't know why. + #self.display.sync() if catch.get_error(): return False return True def ungrab(self): if self.keycode: - self.root.ungrab_key(self.keycode, X.AnyModifier, self.root) + self.window.ungrab_key(self.keycode, X.AnyModifier, self.window) + + def rebind(self, key): + self.ungrab() + if key != "": + self.grab(key) + else: + self.keytext = "" + + def set_focus_window(self, window = None): + self.ungrab() + if window is None: + self.window = self.screen.root + else: + self.window = self.display.create_resource_object("window", gdk.gdk_x11_drawable_get_xid(hash(window))) + self.grab(self.keytext) def get_mask_combinations(self, mask): return [x for x in xrange(mask+1) if not (x & ~mask)] diff --git a/usr/lib/linuxmint/mintMenu/mintMenu.glade b/usr/lib/linuxmint/mintMenu/mintMenu.glade index 641a9b8..c8ef20d 100644 --- a/usr/lib/linuxmint/mintMenu/mintMenu.glade +++ b/usr/lib/linuxmint/mintMenu/mintMenu.glade @@ -4,7 +4,7 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK - popup + toplevel False menu True diff --git a/usr/lib/linuxmint/mintMenu/mintMenu.py b/usr/lib/linuxmint/mintMenu/mintMenu.py index d0eafcb..2c1eb59 100755 --- a/usr/lib/linuxmint/mintMenu/mintMenu.py +++ b/usr/lib/linuxmint/mintMenu/mintMenu.py @@ -21,6 +21,7 @@ try: import capi import xdg.Config import keybinding + import pointerMonitor except Exception, e: print e sys.exit( 1 ) @@ -87,12 +88,9 @@ class MainWindow( object ): self.panesToColor = [ ] self.headingsToColor = [ ] - self.window.connect( "map-event", self.onMap ) - self.window.connect( "show", self.onShow ) - self.window.connect( "unmap-event", self.onUnmap ) - self.window.connect( "button-press-event", self.onButtonPress ) self.window.connect( "key-press-event", self.onKeyPress ) - self.window.connect( "grab-broken-event", self.onGrabBroken ) + self.window.connect( "focus-in-event", self.onFocusIn ) + self.loseFocusId = self.window.connect( "focus-out-event", self.onFocusOut ) self.window.stick() @@ -436,86 +434,59 @@ class MainWindow( object ): #print NAME+u" reloaded" + def onKeyPress( self, widget, event ): + if event.keyval == Gdk.KEY_Escape: + self.hide() + return True + return False def show( self ): self.window.present() - + self.window.window.focus( Gdk.CURRENT_TIME ) + # Hack for opacity not showing on first composited draw if self.firstTime: self.firstTime = False self.SetupMintMenuOpacity() + + for plugin in self.plugins.values(): + if hasattr( plugin, "onShowMenu" ): + plugin.onShowMenu() if ( "applications" in self.plugins ) and ( hasattr( self.plugins["applications"], "focusSearchEntry" ) ): if (self.startWithFavorites): self.plugins["applications"].changeTab(0) self.plugins["applications"].focusSearchEntry() - def grab( self ): - gdk.gdk_pointer_grab (hash(self.window.window), True, Gdk.EventMask.BUTTON_PRESS_MASK, None, None, Gdk.CURRENT_TIME) - Gdk.keyboard_grab( self.window.window, False, Gdk.CURRENT_TIME ) - Gtk.grab_add(self.window) - - def ungrab( self ): - Gtk.grab_remove(self.window) - self.window.hide() - Gdk.pointer_ungrab(Gdk.CURRENT_TIME) - Gdk.keyboard_ungrab(Gdk.CURRENT_TIME) - - def onMap( self, widget, event ): - self.grab() - - def onShow( self, widget ): - for plugin in self.plugins.values(): - if hasattr( plugin, "onShowMenu" ): - plugin.onShowMenu() - - def onUnmap( self, widget, event ): - self.ungrab() - + def hide( self, forceHide = False ): for plugin in self.plugins.values(): if hasattr( plugin, "onHideMenu" ): plugin.onHideMenu() - - def onKeyPress( self, widget, event ): - if event.keyval == Gdk.KEY_Escape or self.keybinder.is_hotkey(event.keyval, event.get_state()): - self.hide() - return True - return False - - def onButtonPress( self, widget, event ): - # Check if the pointer is within the menu, else hide the menu - winatptr = Gdk.window_at_pointer() - if winatptr: - win = winatptr[0] - while win: - if win == self.window.window: - break - win = capi.get_widget(gdk.gdk_window_get_parent (hash(win))) - if not win: - self.hide( True ) - else: - self.hide( True ) - - return True - - def onGrabBroken( self, widget, event ): - if event.grab_broken.grab_window: - try: - win = event.grab_broken.grab_window - data = c_void_p() - gdk.gdk_window_get_user_data(hash(win), byref(data)) - theft = capi.get_widget(ctypes.cast(data, POINTER(capi._PyGObject_Functions))) - theft.connect( "event", self.onGrabTheftEvent ) - except Exception, detail: - print detail - self.window.hide() - - def onGrabTheftEvent( self, widget, event ): - if event.type == Gdk.EventType.UNMAP or event.type == Gdk.EventType.SELECTION_CLEAR: - self.grab() - - def hide(self, forceHide = False): + self.window.hide() + + def onFocusIn( self, *args ): + def dummy( *args ): pass + + signalId = GObject.signal_lookup( "focus-out-event", self.window ) + while True: + result = GObject.signal_handler_find( self.window, + GObject.SignalMatchType.ID | GObject.SignalMatchType.UNBLOCKED, + signalId, 0, None, dummy, dummy ) + if result == 0: + self.window.handler_unblock( self.loseFocusId ) + else: + break + + return False + + def onFocusOut( self, *args): + if self.window.get_visible(): + self.hide() + return False + + def stopHiding( self ): + self.window.handler_block( self.loseFocusId ) class MenuWin( object ): def __init__( self, applet, iid ): @@ -542,8 +513,9 @@ class MenuWin( object ): self.applet.connect("enter-notify-event", self.enter_notify) self.applet.connect("leave-notify-event", self.leave_notify) self.mainwin = MainWindow( self.button_box, self.settings, self.keybinder ) - self.mainwin.window.connect( "map-event", lambda *args: self.applet.set_state( Gtk.StateType.SELECTED ) ) - self.mainwin.window.connect( "unmap-event", lambda *args: self.applet.set_state( Gtk.StateType.NORMAL ) ) + self.mainwin.window.connect( "map-event", self.onWindowMap ) + self.mainwin.window.connect( "unmap-event", self.onWindowUnmap ) + self.mainwin.window.connect( "realize", self.onRealize ) self.mainwin.window.connect( "size-allocate", lambda *args: self.positionMenu() ) self.mainwin.window.set_name("mintmenu") # Name used in Gtk RC files @@ -552,23 +524,46 @@ class MenuWin( object ): Gtk.Window.set_default_icon_name( self.mainwin.icon ) self.bind_hot_key() + self.applet.set_can_focus(False) + + self.pointerMonitor = pointerMonitor.PointerMonitor() + self.pointerMonitor.connect("activate", self.onPointerOutside) + + def onWindowMap( self, *args ): + self.applet.set_state( Gtk.StateType.SELECTED ) + self.keybinder.set_focus_window( self.mainwin.window.window ) + self.pointerMonitor.grabPointer() + return False + + def onWindowUnmap( self, *args ): + self.applet.set_state( Gtk.StateType.NORMAL ) + self.keybinder.set_focus_window() + self.pointerMonitor.ungrabPointer() + return False + + def onRealize( self, *args): + self.pointerMonitor.addWindowToMonitor( self.mainwin.window.window ) + self.pointerMonitor.addWindowToMonitor( self.applet.window ) + self.pointerMonitor.start() + return False + + def onPointerOutside(self, *args): + self.mainwin.hide() + return True def onBindingPress(self, binder): - try: - if self.mainwin.window.get_visible(): - self.mainwin.window.hide() - self.mainwin.toggle.set_active(False) - else: - MenuWin.showMenu(self,self.mainwin.toggle) - self.mainwin.window.show() - #self.mainwin.wTree.get_widget( 'PluginTabs' ).set_curremenu_editor = SetGconf( self.client, "string", "/apps/usp/menu_editor", "mozo" ) - except Exception, cause: - print cause + self.toggleMenu() + return True def enter_notify(self, applet, event): self.do_image(self.buttonIcon, True) def leave_notify(self, applet, event): + # Hack for mate-panel-test-applets focus issue (this can be commented) + if event.state & Gdk.ModifierType.BUTTON1_MASK and applet.state & Gtk.StateType.SELECTED: + if event.x >= 0 and event.y >= 0 and event.x < applet.window.get_width() and event.y < applet.window.get_height(): + self.mainwin.stopHiding() + self.do_image(self.buttonIcon, False) def do_image(self, image_file, saturate): @@ -694,10 +689,8 @@ class MenuWin( object ): pass def hotkeyChanged (self, schema, key): - self.keybinder.ungrab() self.hotkeyText = self.settings.get_string( "hot-key" ) - if self.hotkeyText != "": - self.keybinder.grab(self.hotkeyText) + self.keybinder.rebind(self.hotkeyText) def sizeButton( self ): if self.hideIcon: diff --git a/usr/lib/linuxmint/mintMenu/plugins/applications.py b/usr/lib/linuxmint/mintMenu/plugins/applications.py index 112a59a..b8d7b80 100755 --- a/usr/lib/linuxmint/mintMenu/plugins/applications.py +++ b/usr/lib/linuxmint/mintMenu/plugins/applications.py @@ -576,12 +576,22 @@ class pluginclass( object ): def Todos( self ): - + self.searchEntry.connect( "popup-menu", self.blockOnPopup ) + self.searchEntry.connect( "button-press-event", self.blockOnRightPress ) self.searchEntry.connect( "changed", self.Filter ) self.searchEntry.connect( "activate", self.Search ) self.showAllAppsButton.connect( "clicked", lambda widget: self.changeTab( 1 ) ) self.showFavoritesButton.connect( "clicked", lambda widget: self.changeTab( 0 ) ) self.buildButtonList() + + def blockOnPopup( self, *args ): + self.mintMenuWin.stopHiding() + return False + + def blockOnRightPress( self, widget, event ): + if event.button == 3: + self.mintMenuWin.stopHiding() + return False def focusSearchEntry( self ): # grab_focus() does select all text, @@ -951,10 +961,8 @@ class pluginclass( object ): mTree.append(propsMenuItem) mTree.show_all() + self.mintMenuWin.stopHiding() gtk.gtk_menu_popup(hash(mTree), None, None, None, None, ev.button, ev.time) - #self.mintMenuWin.grab() - mTree.connect( 'deactivate', self.onMenuPopupDeactivate) - else: mTree = Gtk.Menu() mTree.set_events(Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK | @@ -972,9 +980,8 @@ class pluginclass( object ): removeMenuItem.connect( "activate", self.onFavoritesRemove, widget ) insertSpaceMenuItem.connect( "activate", self.onFavoritesInsertSpace, widget, insertBefore ) insertSeparatorMenuItem.connect( "activate", self.onFavoritesInsertSeparator, widget, insertBefore ) + self.mintMenuWin.stopHiding() gtk.gtk_menu_popup(hash(mTree), None, None, None, None, ev.button, ev.time) - #self.mintMenuWin.grab() - mTree.connect( 'deactivate', self.onMenuPopupDeactivate) def menuPopup( self, widget, event ): if event.button == 3: @@ -1035,11 +1042,8 @@ class pluginclass( object ): startupMenuItem.set_active( False ) startupMenuItem.connect( "toggled", self.onAddToStartup, widget ) - mTree.connect( 'deactivate', self.onMenuPopupDeactivate) + self.mintMenuWin.stopHiding() gtk.gtk_menu_popup(hash(mTree), None, None, None, None, event.button, event.time) - - def onMenuPopupDeactivate( self, widget): - self.mintMenuWin.grab() def searchPopup( self, widget=None, event=None ): menu = Gtk.Menu() @@ -1114,14 +1118,13 @@ class pluginclass( object ): menu.append(menuItem) menu.show_all() - + + self.mintMenuWin.stopHiding() gtk.gtk_menu_popup(hash(menu), None, None, None, None, event.button, event.time) #menu.attach_to_widget(self.searchButton, None) #menu.reposition() #menu.reposition() - #self.mintMenuWin.grab() - menu.connect( 'deactivate', self.onMenuPopupDeactivate) return True def pos_func(self, menu=None): diff --git a/usr/lib/linuxmint/mintMenu/pointerMonitor.py b/usr/lib/linuxmint/mintMenu/pointerMonitor.py new file mode 100644 index 0000000..1a0a071 --- /dev/null +++ b/usr/lib/linuxmint/mintMenu/pointerMonitor.py @@ -0,0 +1,84 @@ +import gi +gi.require_version("Gtk", "2.0") + +from Xlib.display import Display +from Xlib import X, error +from gi.repository import Gtk, Gdk, GObject, GLib +import threading +import ctypes +from ctypes import * + +gdk = CDLL("libgdk-x11-2.0.so.0") +gtk = CDLL("libgtk-x11-2.0.so.0") + +class PointerMonitor(GObject.GObject, threading.Thread): + __gsignals__ = { + 'activate': (GObject.SignalFlags.RUN_LAST, None, ()), + } + + def __init__(self): + GObject.GObject.__init__ (self) + threading.Thread.__init__ (self) + self.setDaemon (True) + self.display = Display() + self.root = self.display.screen().root + self.windows = [] + self.topWindows = [] + + # Receives GDK windows + def addWindowToMonitor(self, window): + xWindow = self.display.create_resource_object("window", gdk.gdk_x11_drawable_get_xid(hash(window))) + self.windows.append(xWindow) + self.topWindows.append(self.getToplevelParent(xWindow)) + + def getToplevelParent(self, window): + parent = window.query_tree().parent + if parent == self.root: + return window + else: + return self.getToplevelParent(parent) + + def grabPointer(self): + self.root.grab_button(X.AnyButton, X.AnyModifier, True, X.ButtonPressMask, X.GrabModeSync, X.GrabModeAsync, 0, 0) + self.display.flush() + + def ungrabPointer(self): + self.root.ungrab_button(X.AnyButton, X.AnyModifier) + self.display.flush() + + def idle(self): + self.emit("activate") + return False + + def activate(self): + GLib.idle_add(self.run) + + def run(self): + self.running = True + while self.running: + event = self.display.next_event() + try: + if event.type == X.ButtonPress: + # Check if pointer is inside monitored windows + for w, topW in zip(self.windows, self.topWindows): + if event.child == topW: + if topW == w: + break + else: + p = w.query_pointer() + g = w.get_geometry() + if p.win_x >= 0 and p.win_y >= 0 and p.win_x <= g.width and p.win_y <= g.height: + break + else: + # Is outside, so activate + GLib.idle_add(self.idle) + + self.display.allow_events(X.ReplayPointer, event.time) + except Exception as e: + print "Unexpected error: " + str(e) + + def stop(self): + self.running = False + self.root.ungrab_button(X.AnyButton, X.AnyModifier) + self.display.close() +