shenjack
d84b490b99
Add | more formatter and some more Fix | type mis match sync pyglet Enhance | logger with Template add lib-not-dr as requirement sync pyglet sync pyglet Add | add lto=yes to nuitka_build just incase sync pyglet sync lib_not_dr Remove | external requirement lib-not-dr some logger sync lib-not-dr sync pyglet sync lib-not-dr sync lib-not-dr sync pyglet sync pyglet Fix | console thread been block Update DR rs and DR sdk sync lib not dr sync lib-not-dr sync lib-not-dr sync pyglet and lib-not-dr sync pyglet 0.1.8 sync lib not dr logger almost done? almost! sync pyglet (clicpboard support!) sync lib not dr sync lib not dr color code and sync pyglet do not show memory and progress building localy sync pyglet synclibs
1654 lines
66 KiB
Python
1654 lines
66 KiB
Python
import locale
|
|
import unicodedata
|
|
import urllib.parse
|
|
|
|
from ctypes import *
|
|
from functools import lru_cache
|
|
from typing import Optional
|
|
|
|
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
|
|
|
|
_debug = pyglet.options['debug_x11']
|
|
|
|
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
|
|
XA_STRING = 31
|
|
|
|
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()
|
|
|
|
# Store clipboard string to not query as much for pasting a lot.
|
|
self._clipboard_str: Optional[str] = None
|
|
|
|
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)
|
|
|
|
# Clipboard related atoms
|
|
self._clipboard_atom = xlib.XInternAtom(self._x_display, asbytes('CLIPBOARD'), False)
|
|
self._utf8_atom = xlib.XInternAtom(self._x_display, asbytes('UTF8_STRING'), False)
|
|
self._target_atom = xlib.XInternAtom(self._x_display, asbytes('TARGETS'), False)
|
|
self._incr_atom = xlib.XInternAtom(self._x_display, asbytes('INCR'), False)
|
|
|
|
# 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))
|
|
|
|
def set_clipboard_text(self, text: str):
|
|
xlib.XSetSelectionOwner(self._x_display,
|
|
self._clipboard_atom,
|
|
self._window,
|
|
xlib.CurrentTime)
|
|
|
|
if xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window:
|
|
self._clipboard_str = text
|
|
str_bytes = text.encode('utf-8')
|
|
size = len(str_bytes)
|
|
|
|
xlib.XChangeProperty(self._x_display, self._window,
|
|
self._clipboard_atom, self._utf8_atom, 8, xlib.PropModeReplace,
|
|
(c_ubyte * size).from_buffer_copy(str_bytes), size)
|
|
else:
|
|
if _debug:
|
|
print("X11: Couldn't become owner of clipboard.")
|
|
|
|
def get_clipboard_text(self) -> str:
|
|
if self._clipboard_str is not None:
|
|
return self._clipboard_str
|
|
|
|
owner = xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom)
|
|
|
|
if not owner:
|
|
return ''
|
|
|
|
text = ''
|
|
if owner == self._window:
|
|
data, size, actual_atom = self.get_single_property(self._window, self._clipboard_atom,
|
|
self._utf8_atom)
|
|
else:
|
|
notification = xlib.XEvent()
|
|
|
|
# Convert to selection notification.
|
|
xlib.XConvertSelection(self._x_display,
|
|
self._clipboard_atom,
|
|
self._utf8_atom,
|
|
self._clipboard_atom,
|
|
self._window,
|
|
xlib.CurrentTime)
|
|
|
|
while not xlib.XCheckTypedWindowEvent(self._x_display, self._window, xlib.SelectionNotify, byref(notification)):
|
|
self.dispatch_platform_event(notification)
|
|
|
|
if not notification.xselection.property:
|
|
return ''
|
|
|
|
data, size, actual_atom = self.get_single_property(notification.xselection.requestor, notification.xselection.property,
|
|
self._utf8_atom)
|
|
|
|
if actual_atom == self._incr_atom:
|
|
# Not implemented.
|
|
if _debug:
|
|
print("X11: Clipboard data is too large, not implemented.")
|
|
|
|
elif actual_atom == self._utf8_atom:
|
|
if data:
|
|
text_bytes = string_at(data, size)
|
|
|
|
text = text_bytes.decode('utf-8')
|
|
|
|
self._clipboard_str = text
|
|
|
|
xlib.XFree(data)
|
|
return text
|
|
|
|
# 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, data, and actual atom 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, actualAtom.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')
|
|
|
|
@XlibEventHandler(xlib.SelectionClear)
|
|
def _event_selection_clear(self, ev):
|
|
if ev.xselectionclear.selection == self._clipboard_atom:
|
|
# Another application cleared the clipboard.
|
|
self._clipboard_str = None
|
|
|
|
@XlibEventHandler(xlib.SelectionRequest)
|
|
def _event_selection_request(self, ev):
|
|
request = ev.xselectionrequest
|
|
|
|
if _debug:
|
|
rt = xlib.XGetAtomName(self._x_display, request.target)
|
|
rp = xlib.XGetAtomName(self._x_display, request.property)
|
|
print(f"X11 debug: request target {rt}")
|
|
print(f"X11 debug: request property {rp}")
|
|
|
|
out_event = xlib.XEvent()
|
|
out_event.xany.type = xlib.SelectionNotify
|
|
out_event.xselection.selection = request.selection
|
|
out_event.xselection.display = request.display
|
|
out_event.xselection.target = 0
|
|
out_event.xselection.property = 0
|
|
out_event.xselection.requestor = request.requestor
|
|
out_event.xselection.time = request.time
|
|
|
|
if (xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window and
|
|
ev.xselection.target == self._clipboard_atom):
|
|
if request.target == self._target_atom:
|
|
atoms_ar = (xlib.Atom * 1)(self._utf8_atom)
|
|
ptr = cast(pointer(atoms_ar), POINTER(c_ubyte))
|
|
|
|
xlib.XChangeProperty(self._x_display, request.requestor,
|
|
request.property, XA_ATOM, 32,
|
|
xlib.PropModeReplace,
|
|
ptr, sizeof(atoms_ar)//sizeof(c_ulong))
|
|
out_event.xselection.property = request.property
|
|
out_event.xselection.target = request.target
|
|
|
|
elif request.target == self._utf8_atom:
|
|
# We are being requested for a UTF-8 string.
|
|
text = self._clipboard_str.encode('utf-8')
|
|
size = len(self._clipboard_str)
|
|
xlib.XChangeProperty(self._x_display, request.requestor,
|
|
request.property, request.target, 8,
|
|
xlib.PropModeReplace,
|
|
(c_ubyte * size).from_buffer_copy(text), size)
|
|
|
|
out_event.xselection.property = request.property
|
|
out_event.xselection.target = request.target
|
|
|
|
# Send request event back to requestor with updated changes.
|
|
xlib.XSendEvent(self._x_display, request.requestor, 0, 0, byref(out_event))
|
|
|
|
# Seems to work find without it. May add later.
|
|
#xlib.XSync(self._x_display, False)
|
|
|
|
|
|
__all__ = ["XlibEventHandler", "XlibWindow"]
|