import locale import unicodedata import urllib.parse from ctypes import * from functools import lru_cache import pyglet from pyglet.window import WindowException, MouseCursorException from pyglet.window import MouseCursor, DefaultMouseCursor, ImageMouseCursor from pyglet.window import BaseWindow, _PlatformEventHandler, _ViewEventHandler from pyglet.window import key from pyglet.window import mouse from pyglet.event import EventDispatcher from pyglet.canvas.xlib import XlibCanvas from pyglet.libs.x11 import xlib from pyglet.libs.x11 import cursorfont from pyglet.util import asbytes try: from pyglet.libs.x11 import xsync _have_xsync = True except ImportError: _have_xsync = False class mwmhints_t(Structure): _fields_ = [ ('flags', c_uint32), ('functions', c_uint32), ('decorations', c_uint32), ('input_mode', c_int32), ('status', c_uint32) ] # XXX: wraptypes can't parse the header this function is in yet XkbSetDetectableAutoRepeat = xlib._lib.XkbSetDetectableAutoRepeat XkbSetDetectableAutoRepeat.restype = c_int XkbSetDetectableAutoRepeat.argtypes = [POINTER(xlib.Display), c_int, POINTER(c_int)] _can_detect_autorepeat = None XA_CARDINAL = 6 # Xatom.h:14 XA_ATOM = 4 XDND_VERSION = 5 _have_utf8 = locale.getlocale()[1] == 'UTF-8' # symbol,ctrl -> motion mapping _motion_map = { (key.UP, False): key.MOTION_UP, (key.RIGHT, False): key.MOTION_RIGHT, (key.DOWN, False): key.MOTION_DOWN, (key.LEFT, False): key.MOTION_LEFT, (key.RIGHT, True): key.MOTION_NEXT_WORD, (key.LEFT, True): key.MOTION_PREVIOUS_WORD, (key.HOME, False): key.MOTION_BEGINNING_OF_LINE, (key.END, False): key.MOTION_END_OF_LINE, (key.PAGEUP, False): key.MOTION_PREVIOUS_PAGE, (key.PAGEDOWN, False): key.MOTION_NEXT_PAGE, (key.HOME, True): key.MOTION_BEGINNING_OF_FILE, (key.END, True): key.MOTION_END_OF_FILE, (key.BACKSPACE, False): key.MOTION_BACKSPACE, (key.DELETE, False): key.MOTION_DELETE, (key.C, True): key.MOTION_COPY, (key.V, True): key.MOTION_PASTE } class XlibException(WindowException): """An X11-specific exception. This exception is probably a programming error in pyglet.""" pass class XlibMouseCursor(MouseCursor): gl_drawable = False hw_drawable = True def __init__(self, cursor): self.cursor = cursor # Platform event data is single item, so use platform event handler directly. XlibEventHandler = _PlatformEventHandler ViewEventHandler = _ViewEventHandler class XlibWindow(BaseWindow): _x_display = None # X display connection _x_screen_id = None # X screen index _x_ic = None # X input context _window = None # Xlib window handle _override_redirect = False _x = 0 _y = 0 # Last known window position _mouse_exclusive_client = None # x,y of "real" mouse during exclusive _mouse_buttons = [False] * 6 # State of each xlib button _active = True _applied_mouse_exclusive = False _applied_keyboard_exclusive = False _mapped = False _lost_context = False _lost_context_state = False _enable_xsync = False _current_sync_value = None _current_sync_valid = False _default_event_mask = (0x1ffffff & ~xlib.PointerMotionHintMask & ~xlib.ResizeRedirectMask & ~xlib.SubstructureNotifyMask) def __init__(self, *args, **kwargs): # Bind event handlers self._event_handlers = {} self._view_event_handlers = {} for name in self._platform_event_names: if not hasattr(self, name): continue func = getattr(self, name) for message in func._platform_event_data: if hasattr(func, '_view'): self._view_event_handlers[message] = func else: self._event_handlers[message] = func super(XlibWindow, self).__init__(*args, **kwargs) global _can_detect_autorepeat if _can_detect_autorepeat is None: supported_rtrn = c_int() _can_detect_autorepeat = XkbSetDetectableAutoRepeat(self.display._display, c_int(1), byref(supported_rtrn)) if _can_detect_autorepeat: self.pressed_keys = set() def _recreate(self, changes): # If flipping to/from fullscreen, need to recreate the window. (This # is the case with both override_redirect method and # _NET_WM_STATE_FULLSCREEN). # # A possible improvement could be to just hide the top window, # destroy the GLX window, and reshow it again when leaving fullscreen. # This would prevent the floating window from being moved by the # WM. if 'fullscreen' in changes or 'resizable' in changes: # clear out the GLX context self.context.detach() xlib.XDestroyWindow(self._x_display, self._window) del self.display._window_map[self._window] del self.display._window_map[self._view] self._window = None self._mapped = False # TODO: detect state loss only by examining context share. if 'context' in changes: self._lost_context = True self._lost_context_state = True self._create() def _create_xdnd_atoms(self, display): self._xdnd_atoms = { 'XdndAware' : xlib.XInternAtom(display, asbytes('XdndAware'), False), 'XdndEnter' : xlib.XInternAtom(display, asbytes('XdndEnter'), False), 'XdndTypeList' : xlib.XInternAtom(display, asbytes('XdndTypeList'), False), 'XdndDrop' : xlib.XInternAtom(display, asbytes('XdndDrop'), False), 'XdndFinished' : xlib.XInternAtom(display, asbytes('XdndFinished'), False), 'XdndSelection' : xlib.XInternAtom(display, asbytes('XdndSelection'), False), 'XdndPosition' : xlib.XInternAtom(display, asbytes('XdndPosition'), False), 'XdndStatus' : xlib.XInternAtom(display, asbytes('XdndStatus'), False), 'XdndActionCopy' : xlib.XInternAtom(display, asbytes('XdndActionCopy'), False), 'text/uri-list' : xlib.XInternAtom(display, asbytes("text/uri-list"), False) } def _create(self): # Unmap existing window if necessary while we fiddle with it. if self._window and self._mapped: self._unmap() self._x_display = self.display._display self._x_screen_id = self.display.x_screen # Create X window if not already existing. if not self._window: root = xlib.XRootWindow(self._x_display, self._x_screen_id) visual_info = self.config.get_visual_info() if self.style in ('transparent', 'overlay'): xlib.XMatchVisualInfo(self._x_display, self._x_screen_id, 32, xlib.TrueColor, visual_info) visual = visual_info.visual visual_id = xlib.XVisualIDFromVisual(visual) default_visual = xlib.XDefaultVisual(self._x_display, self._x_screen_id) default_visual_id = xlib.XVisualIDFromVisual(default_visual) window_attributes = xlib.XSetWindowAttributes() if visual_id != default_visual_id: window_attributes.colormap = xlib.XCreateColormap(self._x_display, root, visual, xlib.AllocNone) else: window_attributes.colormap = xlib.XDefaultColormap(self._x_display, self._x_screen_id) window_attributes.bit_gravity = xlib.StaticGravity # Issue 287: Compiz on Intel/Mesa doesn't draw window decoration # unless CWBackPixel is given in mask. Should have # no effect on other systems, so it's set # unconditionally. mask = xlib.CWColormap | xlib.CWBitGravity | xlib.CWBackPixel if self.style in ('transparent', 'overlay'): mask |= xlib.CWBorderPixel window_attributes.border_pixel = 0 window_attributes.background_pixel = 0 if self._fullscreen: width, height = self.screen.width, self.screen.height self._view_x = (width - self._width) // 2 self._view_y = (height - self._height) // 2 else: width, height = self._width, self._height self._view_x = self._view_y = 0 self._window = xlib.XCreateWindow(self._x_display, root, 0, 0, width, height, 0, visual_info.depth, xlib.InputOutput, visual, mask, byref(window_attributes)) self._view = xlib.XCreateWindow(self._x_display, self._window, self._view_x, self._view_y, self._width, self._height, 0, visual_info.depth, xlib.InputOutput, visual, mask, byref(window_attributes)) xlib.XMapWindow(self._x_display, self._view) xlib.XSelectInput(self._x_display, self._view, self._default_event_mask) self.display._window_map[self._window] = self.dispatch_platform_event self.display._window_map[self._view] = self.dispatch_platform_event_view self.canvas = XlibCanvas(self.display, self._view) self.context.attach(self.canvas) self.context.set_vsync(self._vsync) # XXX ? # Setting null background pixmap disables drawing the background, # preventing flicker while resizing (in theory). # # Issue 287: Compiz on Intel/Mesa doesn't draw window decoration if # this is called. As it doesn't seem to have any # effect anyway, it's just commented out. # xlib.XSetWindowBackgroundPixmap(self._x_display, self._window, 0) self._enable_xsync = (pyglet.options['xsync'] and self.display._enable_xsync and self.config.double_buffer) # Set supported protocols protocols = [] protocols.append(xlib.XInternAtom(self._x_display, asbytes('WM_DELETE_WINDOW'), False)) if self._enable_xsync: protocols.append(xlib.XInternAtom(self._x_display, asbytes('_NET_WM_SYNC_REQUEST'), False)) protocols = (c_ulong * len(protocols))(*protocols) xlib.XSetWMProtocols(self._x_display, self._window, protocols, len(protocols)) # Create window resize sync counter if self._enable_xsync: value = xsync.XSyncValue() self._sync_counter = xlib.XID(xsync.XSyncCreateCounter(self._x_display, value)) atom = xlib.XInternAtom(self._x_display, asbytes('_NET_WM_SYNC_REQUEST_COUNTER'), False) ptr = pointer(self._sync_counter) xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL, 32, xlib.PropModeReplace, cast(ptr, POINTER(c_ubyte)), 1) # Atoms required for Xdnd self._create_xdnd_atoms(self._x_display) # Support for drag and dropping files needs to be enabled. if self._file_drops: # Some variables set because there are 4 different drop events that need shared data. self._xdnd_source = None self._xdnd_version = None self._xdnd_format = None self._xdnd_position = (0, 0) # For position callback. VERSION = c_ulong(int(XDND_VERSION)) ptr = pointer(VERSION) xlib.XChangeProperty(self._x_display, self._window, self._xdnd_atoms['XdndAware'], XA_ATOM, 32, xlib.PropModeReplace, cast(ptr, POINTER(c_ubyte)), 1) # Set window attributes attributes = xlib.XSetWindowAttributes() attributes_mask = 0 self._override_redirect = False if self._fullscreen: if pyglet.options['xlib_fullscreen_override_redirect']: # Try not to use this any more, it causes problems; disabled # by default in favour of _NET_WM_STATE_FULLSCREEN. attributes.override_redirect = self._fullscreen attributes_mask |= xlib.CWOverrideRedirect self._override_redirect = True else: self._set_wm_state('_NET_WM_STATE_FULLSCREEN') if self._fullscreen: xlib.XMoveResizeWindow(self._x_display, self._window, self.screen.x, self.screen.y, self.screen.width, self.screen.height) else: xlib.XResizeWindow(self._x_display, self._window, self._width, self._height) xlib.XChangeWindowAttributes(self._x_display, self._window, attributes_mask, byref(attributes)) # Set style styles = { self.WINDOW_STYLE_DEFAULT: '_NET_WM_WINDOW_TYPE_NORMAL', self.WINDOW_STYLE_DIALOG: '_NET_WM_WINDOW_TYPE_DIALOG', self.WINDOW_STYLE_TOOL: '_NET_WM_WINDOW_TYPE_UTILITY', } if self._style in styles: self._set_atoms_property('_NET_WM_WINDOW_TYPE', (styles[self._style],)) elif self._style in (self.WINDOW_STYLE_BORDERLESS, self.WINDOW_STYLE_OVERLAY): MWM_HINTS_DECORATIONS = 1 << 1 PROP_MWM_HINTS_ELEMENTS = 5 mwmhints = mwmhints_t() mwmhints.flags = MWM_HINTS_DECORATIONS mwmhints.decorations = 0 name = xlib.XInternAtom(self._x_display, asbytes('_MOTIF_WM_HINTS'), False) xlib.XChangeProperty(self._x_display, self._window, name, name, 32, xlib.PropModeReplace, cast(pointer(mwmhints), POINTER(c_ubyte)), PROP_MWM_HINTS_ELEMENTS) # Set resizeable if not self._resizable and not self._fullscreen: self.set_minimum_size(self._width, self._height) self.set_maximum_size(self._width, self._height) # Set caption self.set_caption(self._caption) # Set WM_CLASS for modern desktop environments self.set_wm_class(self._caption) # this is supported by some compositors (ie gnome-shell), and more to come # see: http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#idp6357888 _NET_WM_BYPASS_COMPOSITOR_HINT_ON = c_ulong(int(self._fullscreen)) name = xlib.XInternAtom(self._x_display, asbytes('_NET_WM_BYPASS_COMPOSITOR'), False) ptr = pointer(_NET_WM_BYPASS_COMPOSITOR_HINT_ON) xlib.XChangeProperty(self._x_display, self._window, name, XA_CARDINAL, 32, xlib.PropModeReplace, cast(ptr, POINTER(c_ubyte)), 1) # Create input context. A good but very outdated reference for this # is http://www.sbin.org/doc/Xlib/chapt_11.html if _have_utf8 and not self._x_ic: if not self.display._x_im: xlib.XSetLocaleModifiers(asbytes('@im=none')) self.display._x_im = xlib.XOpenIM(self._x_display, None, None, None) xlib.XFlush(self._x_display) # Need to set argtypes on this function because its vararg, # and ctypes guesses wrong. xlib.XCreateIC.argtypes = [xlib.XIM, c_char_p, c_int, c_char_p, xlib.Window, c_char_p, xlib.Window, c_void_p] self._x_ic = xlib.XCreateIC(self.display._x_im, asbytes('inputStyle'), xlib.XIMPreeditNothing | xlib.XIMStatusNothing, asbytes('clientWindow'), self._window, asbytes('focusWindow'), self._window, None) filter_events = c_ulong() xlib.XGetICValues(self._x_ic, 'filterEvents', byref(filter_events), None) self._default_event_mask |= filter_events.value xlib.XSetICFocus(self._x_ic) self.switch_to() if self._visible: self.set_visible(True) self.set_mouse_platform_visible() self._applied_mouse_exclusive = None self._update_exclusivity() def _map(self): if self._mapped: return # Map the window, wait for map event before continuing. xlib.XSelectInput(self._x_display, self._window, xlib.StructureNotifyMask) xlib.XMapRaised(self._x_display, self._window) e = xlib.XEvent() while True: xlib.XNextEvent(self._x_display, e) if e.type == xlib.ConfigureNotify: self._width = e.xconfigure.width self._height = e.xconfigure.height elif e.type == xlib.MapNotify: break xlib.XSelectInput(self._x_display, self._window, self._default_event_mask) self._mapped = True if self._override_redirect: # Possibly an override_redirect issue. self.activate() self._update_view_size() self.dispatch_event('on_resize', self._width, self._height) self.dispatch_event('on_show') self.dispatch_event('on_expose') def _unmap(self): if not self._mapped: return xlib.XSelectInput(self._x_display, self._window, xlib.StructureNotifyMask) xlib.XUnmapWindow(self._x_display, self._window) e = xlib.XEvent() while True: xlib.XNextEvent(self._x_display, e) if e.type == xlib.UnmapNotify: break xlib.XSelectInput(self._x_display, self._window, self._default_event_mask) self._mapped = False def _get_root(self): attributes = xlib.XWindowAttributes() xlib.XGetWindowAttributes(self._x_display, self._window, byref(attributes)) return attributes.root def _is_reparented(self): root = c_ulong() parent = c_ulong() children = pointer(c_ulong()) n_children = c_uint() xlib.XQueryTree(self._x_display, self._window, byref(root), byref(parent), byref(children), byref(n_children)) return root.value != parent.value def close(self): if not self._window: return self.context.destroy() self._unmap() if self._window: xlib.XDestroyWindow(self._x_display, self._window) del self.display._window_map[self._window] del self.display._window_map[self._view] self._window = None self._view_event_handlers.clear() self._event_handlers.clear() if _have_utf8: xlib.XDestroyIC(self._x_ic) self._x_ic = None super(XlibWindow, self).close() def switch_to(self): if self.context: self.context.set_current() def flip(self): self.draw_mouse_cursor() # TODO canvas.flip? if self.context: self.context.flip() self._sync_resize() def set_vsync(self, vsync: bool) -> None: if pyglet.options['vsync'] is not None: vsync = pyglet.options['vsync'] super().set_vsync(vsync) self.context.set_vsync(vsync) def set_caption(self, caption): if caption is None: caption = '' self._caption = caption self._set_text_property('WM_NAME', caption, allow_utf8=False) self._set_text_property('WM_ICON_NAME', caption, allow_utf8=False) self._set_text_property('_NET_WM_NAME', caption) self._set_text_property('_NET_WM_ICON_NAME', caption) def set_wm_class(self, name): # WM_CLASS can only contain Ascii characters try: name = name.encode('ascii') except UnicodeEncodeError: name = "pyglet" hint = xlib.XAllocClassHint() hint.contents.res_class = asbytes(name) hint.contents.res_name = asbytes(name.lower()) xlib.XSetClassHint(self._x_display, self._window, hint.contents) xlib.XFree(hint) def get_caption(self): return self._caption def set_size(self, width: int, height: int) -> None: super().set_size(width, height) if not self._resizable: self.set_minimum_size(width, height) self.set_maximum_size(width, height) xlib.XResizeWindow(self._x_display, self._window, width, height) self._update_view_size() self.dispatch_event('on_resize', width, height) def _update_view_size(self): xlib.XResizeWindow(self._x_display, self._view, self._width, self._height) def set_location(self, x, y): if self._is_reparented(): # Assume the window manager has reparented our top-level window # only once, in which case attributes.x/y give the offset from # the frame to the content window. Better solution would be # to use _NET_FRAME_EXTENTS, where supported. attributes = xlib.XWindowAttributes() xlib.XGetWindowAttributes(self._x_display, self._window, byref(attributes)) # XXX at least under KDE's WM these attrs are both 0 x -= attributes.x y -= attributes.y xlib.XMoveWindow(self._x_display, self._window, x, y) def get_location(self): child = xlib.Window() x = c_int() y = c_int() xlib.XTranslateCoordinates(self._x_display, self._window, self._get_root(), 0, 0, byref(x), byref(y), byref(child)) return x.value, y.value def activate(self): # Issue 218 if self._x_display and self._window: xlib.XSetInputFocus(self._x_display, self._window, xlib.RevertToParent, xlib.CurrentTime) def set_visible(self, visible: bool = True) -> None: super().set_visible(visible) if visible: self._map() else: self._unmap() def set_minimum_size(self, width: int, height: int) -> None: super().set_minimum_size(width, height) self._set_wm_normal_hints() def set_maximum_size(self, width: int, height: int) -> None: super().set_maximum_size(width, height) self._set_wm_normal_hints() def minimize(self): xlib.XIconifyWindow(self._x_display, self._window, self._x_screen_id) def maximize(self): self._set_wm_state('_NET_WM_STATE_MAXIMIZED_HORZ', '_NET_WM_STATE_MAXIMIZED_VERT') @staticmethod def _downsample_1bit(pixelarray): byte_list = [] value = 0 for i, pixel in enumerate(pixelarray): index = i % 8 if pixel: value |= 1 << index if index == 7: byte_list.append(value) value = 0 return bytes(byte_list) @lru_cache() def _create_cursor_from_image(self, cursor): """Creates platform cursor from an ImageCursor instance.""" texture = cursor.texture width = texture.width height = texture.height alpha_luma_bytes = texture.get_image_data().get_data('AL', -width * 2) mask_data = self._downsample_1bit(alpha_luma_bytes[0::2]) bmp_data = self._downsample_1bit(alpha_luma_bytes[1::2]) bitmap = xlib.XCreateBitmapFromData(self._x_display, self._window, bmp_data, width, height) mask = xlib.XCreateBitmapFromData(self._x_display, self._window, mask_data, width, height) white = xlib.XColor(red=65535, green=65535, blue=65535) # background color black = xlib.XColor() # foreground color # hot_x/y must be within the image dimension, or the cursor will not display: hot_x = min(max(0, int(self._mouse_cursor.hot_x)), width) hot_y = min(max(0, int(height - self._mouse_cursor.hot_y)), height) cursor = xlib.XCreatePixmapCursor(self._x_display, bitmap, mask, white, black, hot_x, hot_y) xlib.XFreePixmap(self._x_display, bitmap) xlib.XFreePixmap(self._x_display, mask) return cursor def set_mouse_platform_visible(self, platform_visible=None): if not self._window: return if platform_visible is None: platform_visible = self._mouse_visible and not self._mouse_cursor.gl_drawable if platform_visible is False: # Hide pointer by creating an empty cursor: black = xlib.XColor() bitmap = xlib.XCreateBitmapFromData(self._x_display, self._window, bytes(8), 8, 8) cursor = xlib.XCreatePixmapCursor(self._x_display, bitmap, bitmap, black, black, 0, 0) xlib.XDefineCursor(self._x_display, self._window, cursor) xlib.XFreeCursor(self._x_display, cursor) xlib.XFreePixmap(self._x_display, bitmap) elif isinstance(self._mouse_cursor, ImageMouseCursor) and self._mouse_cursor.hw_drawable: # Create a custom hardware cursor: cursor = self._create_cursor_from_image(self._mouse_cursor) xlib.XDefineCursor(self._x_display, self._window, cursor) else: # Restore standard hardware cursor: if isinstance(self._mouse_cursor, XlibMouseCursor): xlib.XDefineCursor(self._x_display, self._window, self._mouse_cursor.cursor) else: xlib.XUndefineCursor(self._x_display, self._window) def set_mouse_position(self, x, y): xlib.XWarpPointer(self._x_display, 0, # src window self._window, # dst window 0, 0, # src x, y 0, 0, # src w, h x, self._height - y) def _update_exclusivity(self): mouse_exclusive = self._active and self._mouse_exclusive keyboard_exclusive = self._active and self._keyboard_exclusive if mouse_exclusive != self._applied_mouse_exclusive: if mouse_exclusive: self.set_mouse_platform_visible(False) # Restrict to client area xlib.XGrabPointer(self._x_display, self._window, True, 0, xlib.GrabModeAsync, xlib.GrabModeAsync, self._window, 0, xlib.CurrentTime) # Move pointer to center of window x = self._width // 2 y = self._height // 2 self._mouse_exclusive_client = x, y self.set_mouse_position(x, y) elif self._fullscreen and not self.screen._xinerama: # Restrict to fullscreen area (prevent viewport scrolling) self.set_mouse_position(0, 0) r = xlib.XGrabPointer(self._x_display, self._view, True, 0, xlib.GrabModeAsync, xlib.GrabModeAsync, self._view, 0, xlib.CurrentTime) if r: # Failed to grab, try again later self._applied_mouse_exclusive = None return self.set_mouse_platform_visible() else: # Unclip xlib.XUngrabPointer(self._x_display, xlib.CurrentTime) self.set_mouse_platform_visible() self._applied_mouse_exclusive = mouse_exclusive if keyboard_exclusive != self._applied_keyboard_exclusive: if keyboard_exclusive: xlib.XGrabKeyboard(self._x_display, self._window, False, xlib.GrabModeAsync, xlib.GrabModeAsync, xlib.CurrentTime) else: xlib.XUngrabKeyboard(self._x_display, xlib.CurrentTime) self._applied_keyboard_exclusive = keyboard_exclusive def set_exclusive_mouse(self, exclusive=True): if exclusive == self._mouse_exclusive: return super().set_exclusive_mouse(exclusive) self._update_exclusivity() def set_exclusive_keyboard(self, exclusive=True): if exclusive == self._keyboard_exclusive: return super().set_exclusive_keyboard(exclusive) self._update_exclusivity() def get_system_mouse_cursor(self, name): if name == self.CURSOR_DEFAULT: return DefaultMouseCursor() # NQR means default shape is not pretty... surely there is another # cursor font? cursor_shapes = { self.CURSOR_CROSSHAIR: cursorfont.XC_crosshair, self.CURSOR_HAND: cursorfont.XC_hand2, self.CURSOR_HELP: cursorfont.XC_question_arrow, # NQR self.CURSOR_NO: cursorfont.XC_pirate, # NQR self.CURSOR_SIZE: cursorfont.XC_fleur, self.CURSOR_SIZE_UP: cursorfont.XC_top_side, self.CURSOR_SIZE_UP_RIGHT: cursorfont.XC_top_right_corner, self.CURSOR_SIZE_RIGHT: cursorfont.XC_right_side, self.CURSOR_SIZE_DOWN_RIGHT: cursorfont.XC_bottom_right_corner, self.CURSOR_SIZE_DOWN: cursorfont.XC_bottom_side, self.CURSOR_SIZE_DOWN_LEFT: cursorfont.XC_bottom_left_corner, self.CURSOR_SIZE_LEFT: cursorfont.XC_left_side, self.CURSOR_SIZE_UP_LEFT: cursorfont.XC_top_left_corner, self.CURSOR_SIZE_UP_DOWN: cursorfont.XC_sb_v_double_arrow, self.CURSOR_SIZE_LEFT_RIGHT: cursorfont.XC_sb_h_double_arrow, self.CURSOR_TEXT: cursorfont.XC_xterm, self.CURSOR_WAIT: cursorfont.XC_watch, self.CURSOR_WAIT_ARROW: cursorfont.XC_watch, # NQR } if name not in cursor_shapes: raise MouseCursorException('Unknown cursor name "%s"' % name) cursor = xlib.XCreateFontCursor(self._x_display, cursor_shapes[name]) return XlibMouseCursor(cursor) def set_icon(self, *images): # Careful! XChangeProperty takes an array of long when data type # is 32-bit (but long can be 64 bit!), so pad high bytes of format if # necessary. import sys fmt = {('little', 4): 'BGRA', ('little', 8): 'BGRAAAAA', ('big', 4): 'ARGB', ('big', 8): 'AAAAARGB'}[(sys.byteorder, sizeof(c_ulong))] data = asbytes('') for image in images: image = image.get_image_data() pitch = -(image.width * len(fmt)) s = c_buffer(sizeof(c_ulong) * 2) memmove(s, cast((c_ulong * 2)(image.width, image.height), POINTER(c_ubyte)), len(s)) data += s.raw + image.get_data(fmt, pitch) buffer = (c_ubyte * len(data))() memmove(buffer, data, len(data)) atom = xlib.XInternAtom(self._x_display, asbytes('_NET_WM_ICON'), False) xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL, 32, xlib.PropModeReplace, buffer, len(data)//sizeof(c_ulong)) # Private utility def _set_wm_normal_hints(self): hints = xlib.XAllocSizeHints().contents if self._minimum_size: hints.flags |= xlib.PMinSize hints.min_width, hints.min_height = self._minimum_size if self._maximum_size: hints.flags |= xlib.PMaxSize hints.max_width, hints.max_height = self._maximum_size xlib.XSetWMNormalHints(self._x_display, self._window, byref(hints)) def _set_text_property(self, name, value, allow_utf8=True): atom = xlib.XInternAtom(self._x_display, asbytes(name), False) if not atom: raise XlibException('Undefined atom "%s"' % name) text_property = xlib.XTextProperty() if _have_utf8 and allow_utf8: buf = create_string_buffer(value.encode('utf8')) result = xlib.Xutf8TextListToTextProperty(self._x_display, cast(pointer(buf), c_char_p), 1, xlib.XUTF8StringStyle, byref(text_property)) if result < 0: raise XlibException('Could not create UTF8 text property') else: buf = create_string_buffer(value.encode('ascii', 'ignore')) result = xlib.XStringListToTextProperty( cast(pointer(buf), c_char_p), 1, byref(text_property)) if result < 0: raise XlibException('Could not create text property') xlib.XSetTextProperty(self._x_display, self._window, byref(text_property), atom) # XXX Xlib doesn't like us freeing this # xlib.XFree(text_property.value) def _set_atoms_property(self, name, values, mode=xlib.PropModeReplace): name_atom = xlib.XInternAtom(self._x_display, asbytes(name), False) atoms = [] for value in values: atoms.append(xlib.XInternAtom(self._x_display, asbytes(value), False)) atom_type = xlib.XInternAtom(self._x_display, asbytes('ATOM'), False) if len(atoms): atoms_ar = (xlib.Atom * len(atoms))(*atoms) xlib.XChangeProperty(self._x_display, self._window, name_atom, atom_type, 32, mode, cast(pointer(atoms_ar), POINTER(c_ubyte)), len(atoms)) else: net_wm_state = xlib.XInternAtom(self._x_display, asbytes('_NET_WM_STATE'), False) if net_wm_state: xlib.XDeleteProperty(self._x_display, self._window, net_wm_state) def _set_wm_state(self, *states): # Set property net_wm_state = xlib.XInternAtom(self._x_display, asbytes('_NET_WM_STATE'), False) atoms = [] for state in states: atoms.append(xlib.XInternAtom(self._x_display, asbytes(state), False)) atom_type = xlib.XInternAtom(self._x_display, asbytes('ATOM'), False) if len(atoms): atoms_ar = (xlib.Atom * len(atoms))(*atoms) xlib.XChangeProperty(self._x_display, self._window, net_wm_state, atom_type, 32, xlib.PropModePrepend, cast(pointer(atoms_ar), POINTER(c_ubyte)), len(atoms)) else: xlib.XDeleteProperty(self._x_display, self._window, net_wm_state) # Nudge the WM e = xlib.XEvent() e.xclient.type = xlib.ClientMessage e.xclient.message_type = net_wm_state e.xclient.display = cast(self._x_display, POINTER(xlib.Display)) e.xclient.window = self._window e.xclient.format = 32 e.xclient.data.l[0] = xlib.PropModePrepend for i, atom in enumerate(atoms): e.xclient.data.l[i + 1] = atom xlib.XSendEvent(self._x_display, self._get_root(), False, xlib.SubstructureRedirectMask, byref(e)) # Event handling def dispatch_events(self): self.dispatch_pending_events() self._allow_dispatch_event = True e = xlib.XEvent() # Cache these in case window is closed from an event handler _x_display = self._x_display _window = self._window _view = self._view # Check for the events specific to this window while xlib.XCheckWindowEvent(_x_display, _window, 0x1ffffff, byref(e)): # Key events are filtered by the xlib window event # handler so they get a shot at the prefiltered event. if e.xany.type not in (xlib.KeyPress, xlib.KeyRelease): if xlib.XFilterEvent(e, 0): continue self.dispatch_platform_event(e) # Check for the events specific to this view while xlib.XCheckWindowEvent(_x_display, _view, 0x1ffffff, byref(e)): # Key events are filtered by the xlib window event # handler so they get a shot at the prefiltered event. if e.xany.type not in (xlib.KeyPress, xlib.KeyRelease): if xlib.XFilterEvent(e, 0): continue self.dispatch_platform_event_view(e) # Generic events for this window (the window close event). while xlib.XCheckTypedWindowEvent(_x_display, _window, xlib.ClientMessage, byref(e)): self.dispatch_platform_event(e) self._allow_dispatch_event = False def dispatch_pending_events(self): while self._event_queue: EventDispatcher.dispatch_event(self, *self._event_queue.pop(0)) # Dispatch any context-related events if self._lost_context: self._lost_context = False EventDispatcher.dispatch_event(self, 'on_context_lost') if self._lost_context_state: self._lost_context_state = False EventDispatcher.dispatch_event(self, 'on_context_state_lost') def dispatch_platform_event(self, e): if self._applied_mouse_exclusive is None: self._update_exclusivity() event_handler = self._event_handlers.get(e.type) if event_handler: event_handler(e) def dispatch_platform_event_view(self, e): event_handler = self._view_event_handlers.get(e.type) if event_handler: event_handler(e) @staticmethod def _translate_modifiers(state): modifiers = 0 if state & xlib.ShiftMask: modifiers |= key.MOD_SHIFT if state & xlib.ControlMask: modifiers |= key.MOD_CTRL if state & xlib.LockMask: modifiers |= key.MOD_CAPSLOCK if state & xlib.Mod1Mask: modifiers |= key.MOD_ALT if state & xlib.Mod2Mask: modifiers |= key.MOD_NUMLOCK if state & xlib.Mod4Mask: modifiers |= key.MOD_WINDOWS if state & xlib.Mod5Mask: modifiers |= key.MOD_SCROLLLOCK return modifiers # Event handlers """ def _event_symbol(self, event): # pyglet.self.key keysymbols are identical to X11 keysymbols, no # need to map the keysymbol. symbol = xlib.XKeycodeToKeysym(self._x_display, event.xkey.keycode, 0) if symbol == 0: # XIM event return None elif symbol not in key._key_names.keys(): symbol = key.user_key(event.xkey.keycode) return symbol """ def _event_text_symbol(self, ev): text = None symbol = xlib.KeySym() buffer = create_string_buffer(128) # Look up raw keysym before XIM filters it (default for keypress and # keyrelease) count = xlib.XLookupString(ev.xkey, buffer, len(buffer) - 1, byref(symbol), None) # Give XIM a shot filtered = xlib.XFilterEvent(ev, ev.xany.window) if ev.type == xlib.KeyPress and not filtered: status = c_int() if _have_utf8: encoding = 'utf8' count = xlib.Xutf8LookupString(self._x_ic, ev.xkey, buffer, len(buffer) - 1, byref(symbol), byref(status)) if status.value == xlib.XBufferOverflow: raise NotImplementedError('TODO: XIM buffer resize') else: encoding = 'ascii' count = xlib.XLookupString(ev.xkey, buffer, len(buffer) - 1, byref(symbol), None) if count: status.value = xlib.XLookupBoth if status.value & (xlib.XLookupChars | xlib.XLookupBoth): text = buffer.value[:count].decode(encoding) # Don't treat Unicode command codepoints as text, except Return. if text and unicodedata.category(text) == 'Cc' and text != '\r': text = None symbol = symbol.value # If the event is a XIM filtered event, the keysym will be virtual # (e.g., aacute instead of A after a dead key). Drop it, we don't # want these kind of key events. if ev.xkey.keycode == 0 and not filtered: symbol = None # pyglet.self.key keysymbols are identical to X11 keysymbols, no # need to map the keysymbol. For keysyms outside the pyglet set, map # raw key code to a user key. if symbol and symbol not in key._key_names and ev.xkey.keycode: # Issue 353: Symbol is uppercase when shift key held down. try: symbol = ord(chr(symbol).lower()) except ValueError: # Not a valid unichr, use the keycode symbol = key.user_key(ev.xkey.keycode) else: # If still not recognised, use the keycode if symbol not in key._key_names: symbol = key.user_key(ev.xkey.keycode) if filtered: # The event was filtered, text must be ignored, but the symbol is # still good. return None, symbol return text, symbol @staticmethod def _event_text_motion(symbol, modifiers): if modifiers & key.MOD_ALT: return None ctrl = modifiers & key.MOD_CTRL != 0 return _motion_map.get((symbol, ctrl), None) @ViewEventHandler @XlibEventHandler(xlib.KeyPress) @XlibEventHandler(xlib.KeyRelease) def _event_key_view(self, ev): # Try to detect autorepeat ourselves if the server doesn't support it # XXX: Doesn't always work, better off letting the server do it global _can_detect_autorepeat if not _can_detect_autorepeat and ev.type == xlib.KeyRelease: # Look in the queue for a matching KeyPress with same timestamp, # indicating an auto-repeat rather than actual key event. saved = [] while True: auto_event = xlib.XEvent() result = xlib.XCheckWindowEvent(self._x_display, self._window, xlib.KeyPress|xlib.KeyRelease, byref(auto_event)) if not result: break saved.append(auto_event) if auto_event.type == xlib.KeyRelease: # just save this off for restoration back to the queue continue if ev.xkey.keycode == auto_event.xkey.keycode: # Found a key repeat: dispatch EVENT_TEXT* event text, symbol = self._event_text_symbol(auto_event) modifiers = self._translate_modifiers(ev.xkey.state) modifiers_ctrl = modifiers & (key.MOD_CTRL | key.MOD_ALT) motion = self._event_text_motion(symbol, modifiers) if motion: if modifiers & key.MOD_SHIFT: self.dispatch_event('on_text_motion_select', motion) else: self.dispatch_event('on_text_motion', motion) elif text and not modifiers_ctrl: self.dispatch_event('on_text', text) ditched = saved.pop() for auto_event in reversed(saved): xlib.XPutBackEvent(self._x_display, byref(auto_event)) return else: # Key code of press did not match, therefore no repeating # is going on, stop searching. break # Whoops, put the events back, it's for real. for auto_event in reversed(saved): xlib.XPutBackEvent(self._x_display, byref(auto_event)) text, symbol = self._event_text_symbol(ev) modifiers = self._translate_modifiers(ev.xkey.state) modifiers_ctrl = modifiers & (key.MOD_CTRL | key.MOD_ALT) motion = self._event_text_motion(symbol, modifiers) if ev.type == xlib.KeyPress: if symbol and (not _can_detect_autorepeat or symbol not in self.pressed_keys): self.dispatch_event('on_key_press', symbol, modifiers) if _can_detect_autorepeat: self.pressed_keys.add(symbol) if motion: if modifiers & key.MOD_SHIFT: self.dispatch_event('on_text_motion_select', motion) else: self.dispatch_event('on_text_motion', motion) elif text and not modifiers_ctrl: self.dispatch_event('on_text', text) elif ev.type == xlib.KeyRelease: if symbol: self.dispatch_event('on_key_release', symbol, modifiers) if _can_detect_autorepeat and symbol in self.pressed_keys: self.pressed_keys.remove(symbol) @XlibEventHandler(xlib.KeyPress) @XlibEventHandler(xlib.KeyRelease) def _event_key(self, ev): return self._event_key_view(ev) @ViewEventHandler @XlibEventHandler(xlib.MotionNotify) def _event_motionnotify_view(self, ev): x = ev.xmotion.x y = self.height - ev.xmotion.y - 1 if self._mouse_in_window: dx = x - self._mouse_x dy = y - self._mouse_y else: dx = dy = 0 if self._applied_mouse_exclusive and (ev.xmotion.x, ev.xmotion.y) == self._mouse_exclusive_client: # Ignore events caused by XWarpPointer self._mouse_x = x self._mouse_y = y return if self._applied_mouse_exclusive: # Reset pointer position ex, ey = self._mouse_exclusive_client xlib.XWarpPointer(self._x_display, 0, self._window, 0, 0, 0, 0, ex, ey) self._mouse_x = x self._mouse_y = y self._mouse_in_window = True buttons = 0 if ev.xmotion.state & xlib.Button1MotionMask: buttons |= mouse.LEFT if ev.xmotion.state & xlib.Button2MotionMask: buttons |= mouse.MIDDLE if ev.xmotion.state & xlib.Button3MotionMask: buttons |= mouse.RIGHT # TODO: Determine how to implement drag support for mouse 4 and 5 if buttons: # Drag event modifiers = self._translate_modifiers(ev.xmotion.state) self.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers) else: # Motion event self.dispatch_event('on_mouse_motion', x, y, dx, dy) @XlibEventHandler(xlib.MotionNotify) def _event_motionnotify(self, ev): # Window motion looks for drags that are outside the view but within # the window. buttons = 0 if ev.xmotion.state & xlib.Button1MotionMask: buttons |= mouse.LEFT if ev.xmotion.state & xlib.Button2MotionMask: buttons |= mouse.MIDDLE if ev.xmotion.state & xlib.Button3MotionMask: buttons |= mouse.RIGHT # TODO: Determine how to implement drag support for mouse 4 and 5 if buttons: # Drag event x = ev.xmotion.x - self._view_x y = self._height - (ev.xmotion.y - self._view_y - 1) if self._mouse_in_window: dx = x - self._mouse_x dy = y - self._mouse_y else: dx = dy = 0 self._mouse_x = x self._mouse_y = y modifiers = self._translate_modifiers(ev.xmotion.state) self.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers) @XlibEventHandler(xlib.ClientMessage) def _event_clientmessage(self, ev): atom = ev.xclient.data.l[0] if atom == xlib.XInternAtom(ev.xclient.display, asbytes('WM_DELETE_WINDOW'), False): self.dispatch_event('on_close') elif (self._enable_xsync and atom == xlib.XInternAtom(ev.xclient.display, asbytes('_NET_WM_SYNC_REQUEST'), False)): lo = ev.xclient.data.l[2] hi = ev.xclient.data.l[3] self._current_sync_value = xsync.XSyncValue(hi, lo) elif ev.xclient.message_type == self._xdnd_atoms['XdndPosition']: self._event_drag_position(ev) elif ev.xclient.message_type == self._xdnd_atoms['XdndDrop']: self._event_drag_drop(ev) elif ev.xclient.message_type == self._xdnd_atoms['XdndEnter']: self._event_drag_enter(ev) def _event_drag_drop(self, ev): if self._xdnd_version > XDND_VERSION: return time = xlib.CurrentTime if self._xdnd_format: if self._xdnd_version >= 1: time = ev.xclient.data.l[2] # Convert to selection notification. xlib.XConvertSelection(self._x_display, self._xdnd_atoms['XdndSelection'], self._xdnd_format, self._xdnd_atoms['XdndSelection'], self._window, time) xlib.XFlush(self._x_display) elif self._xdnd_version >= 2: # If no format send finished with no data. e = xlib.XEvent() e.xclient.type = xlib.ClientMessage e.xclient.message_type = self._xdnd_atoms['XdndFinished'] e.xclient.display = cast(self._x_display, POINTER(xlib.Display)) e.xclient.window = self._window e.xclient.format = 32 e.xclient.data.l[0] = self._window e.xclient.data.l[1] = 0 e.xclient.data.l[2] = None xlib.XSendEvent(self._x_display, self._xdnd_source, False, xlib.NoEventMask, byref(e)) xlib.XFlush(self._x_display) def _event_drag_position(self, ev): if self._xdnd_version > XDND_VERSION: return xoff = (ev.xclient.data.l[2] >> 16) & 0xffff yoff = (ev.xclient.data.l[2]) & 0xffff # Need to convert the position to actual window coordinates with the screen offset child = xlib.Window() x = c_int() y = c_int() xlib.XTranslateCoordinates(self._x_display, self._get_root(), self._window, xoff, yoff, byref(x), byref(y), byref(child)) self._xdnd_position = (x.value, y.value) e = xlib.XEvent() e.xclient.type = xlib.ClientMessage e.xclient.message_type = self._xdnd_atoms['XdndStatus'] e.xclient.display = cast(self._x_display, POINTER(xlib.Display)) e.xclient.window = ev.xclient.data.l[0] e.xclient.format = 32 e.xclient.data.l[0] = self._window e.xclient.data.l[2] = 0 e.xclient.data.l[3] = 0 if self._xdnd_format: e.xclient.data.l[1] = 1 if self._xdnd_version >= 2: e.xclient.data.l[4] = self._xdnd_atoms['XdndActionCopy'] xlib.XSendEvent(self._x_display, self._xdnd_source, False, xlib.NoEventMask, byref(e)) xlib.XFlush(self._x_display) def _event_drag_enter(self, ev): self._xdnd_source = ev.xclient.data.l[0] self._xdnd_version = ev.xclient.data.l[1] >> 24 self._xdnd_format = None if self._xdnd_version > XDND_VERSION: return three_or_more = ev.xclient.data.l[1] & 1 # Search all of them (usually 8) if three_or_more: data, count = self.get_single_property(self._xdnd_source, self._xdnd_atoms['XdndTypeList'], XA_ATOM) data = cast(data, POINTER(xlib.Atom)) else: # Some old versions may only have 3? Needs testing. count = 3 data = ev.xclient.data.l + 2 # Check all of the properties we received from the dropped item and verify it support URI. for i in range(count): if data[i] == self._xdnd_atoms['text/uri-list']: self._xdnd_format = self._xdnd_atoms['text/uri-list'] break if data: xlib.XFree(data) def get_single_property(self, window, atom_property, atom_type): """ Returns the length and data of a window property. """ actualAtom = xlib.Atom() actualFormat = c_int() itemCount = c_ulong() bytesAfter = c_ulong() data = POINTER(c_ubyte)() xlib.XGetWindowProperty(self._x_display, window, atom_property, 0, 2147483647, False, atom_type, byref(actualAtom), byref(actualFormat), byref(itemCount), byref(bytesAfter), data) return data, itemCount.value @XlibEventHandler(xlib.SelectionNotify) def _event_selection_notification(self, ev): if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']: if self._xdnd_format: # This will get the data data, count = self.get_single_property(ev.xselection.requestor, ev.xselection.property, ev.xselection.target) buffer = create_string_buffer(count) memmove(buffer, data, count) formatted_paths = self.parse_filenames(buffer.value.decode()) e = xlib.XEvent() e.xclient.type = xlib.ClientMessage e.xclient.message_type = self._xdnd_atoms['XdndFinished'] e.xclient.display = cast(self._x_display, POINTER(xlib.Display)) e.xclient.window = self._window e.xclient.format = 32 e.xclient.data.l[0] = self._xdnd_source e.xclient.data.l[1] = 1 e.xclient.data.l[2] = self._xdnd_atoms['XdndActionCopy'] xlib.XSendEvent(self._x_display, self._get_root(), False, xlib.NoEventMask, byref(e)) xlib.XFlush(self._x_display) xlib.XFree(data) self.dispatch_event('on_file_drop', self._xdnd_position[0], self._height - self._xdnd_position[1], formatted_paths) @staticmethod def parse_filenames(decoded_string): """All of the filenames from file drops come as one big string with some special characters (%20), this will parse them out. """ import sys different_files = decoded_string.splitlines() parsed = [] for filename in different_files: if filename: filename = urllib.parse.urlsplit(filename).path encoding = sys.getfilesystemencoding() parsed.append(urllib.parse.unquote(filename, encoding)) return parsed def _sync_resize(self): if self._enable_xsync and self._current_sync_valid: if xsync.XSyncValueIsZero(self._current_sync_value): self._current_sync_valid = False return xsync.XSyncSetCounter(self._x_display, self._sync_counter, self._current_sync_value) self._current_sync_value = None self._current_sync_valid = False @ViewEventHandler @XlibEventHandler(xlib.ButtonPress) @XlibEventHandler(xlib.ButtonRelease) def _event_button(self, ev): x = ev.xbutton.x y = self.height - ev.xbutton.y button = ev.xbutton.button - 1 if button == 7 or button == 8: button -= 4 modifiers = self._translate_modifiers(ev.xbutton.state) if ev.type == xlib.ButtonPress: # override_redirect issue: manually activate this window if # fullscreen. if self._override_redirect and not self._active: self.activate() if ev.xbutton.button == 4: self.dispatch_event('on_mouse_scroll', x, y, 0, 1) elif ev.xbutton.button == 5: self.dispatch_event('on_mouse_scroll', x, y, 0, -1) elif ev.xbutton.button == 6: self.dispatch_event('on_mouse_scroll', x, y, -1, 0) elif ev.xbutton.button == 7: self.dispatch_event('on_mouse_scroll', x, y, 1, 0) elif button < 5: self.dispatch_event('on_mouse_press', x, y, 1 << button, modifiers) elif button < 5: self.dispatch_event('on_mouse_release', x, y, 1 << button, modifiers) @ViewEventHandler @XlibEventHandler(xlib.Expose) def _event_expose(self, ev): # Ignore all expose events except the last one. We could be told # about exposure rects - but I don't see the point since we're # working with OpenGL and we'll just redraw the whole scene. if ev.xexpose.count > 0: return self.dispatch_event('on_expose') @ViewEventHandler @XlibEventHandler(xlib.EnterNotify) def _event_enternotify(self, ev): # mouse position x = self._mouse_x = ev.xcrossing.x y = self._mouse_y = self.height - ev.xcrossing.y self._mouse_in_window = True # XXX there may be more we could do here self.dispatch_event('on_mouse_enter', x, y) @ViewEventHandler @XlibEventHandler(xlib.LeaveNotify) def _event_leavenotify(self, ev): x = self._mouse_x = ev.xcrossing.x y = self._mouse_y = self.height - ev.xcrossing.y self._mouse_in_window = False self.dispatch_event('on_mouse_leave', x, y) @XlibEventHandler(xlib.ConfigureNotify) def _event_configurenotify(self, ev): if self._enable_xsync and self._current_sync_value: self._current_sync_valid = True if self._fullscreen: return self.switch_to() w, h = ev.xconfigure.width, ev.xconfigure.height x, y = ev.xconfigure.x, ev.xconfigure.y if self._width != w or self._height != h: self._width = w self._height = h self._update_view_size() self.dispatch_event('on_resize', self._width, self._height) if self._x != x or self._y != y: self.dispatch_event('on_move', x, y) self._x = x self._y = y @XlibEventHandler(xlib.FocusIn) def _event_focusin(self, ev): self._active = True self._update_exclusivity() self.dispatch_event('on_activate') xlib.XSetICFocus(self._x_ic) @XlibEventHandler(xlib.FocusOut) def _event_focusout(self, ev): self._active = False self._update_exclusivity() self.dispatch_event('on_deactivate') xlib.XUnsetICFocus(self._x_ic) @XlibEventHandler(xlib.MapNotify) def _event_mapnotify(self, ev): self._mapped = True self.dispatch_event('on_show') self._update_exclusivity() @XlibEventHandler(xlib.UnmapNotify) def _event_unmapnotify(self, ev): self._mapped = False self.dispatch_event('on_hide') __all__ = ["XlibEventHandler", "XlibWindow"]