1517 lines
60 KiB
Python
1517 lines
60 KiB
Python
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
|
|
|
|
# Do we have the November 2000 UTF8 extension?
|
|
_have_utf8 = hasattr(xlib._lib, 'Xutf8TextListToTextProperty')
|
|
|
|
# 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 <rj> 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"]
|