# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2007 Alex Holkner
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions 
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright 
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

__docformat__ = 'restructuredtext'
__version__ = '$Id: __init__.py 1571 2008-01-05 00:20:12Z Alex.Holkner $'

from ctypes import *
import unicodedata
import warnings

import pyglet
from pyglet.window import WindowException, NoSuchDisplayException, \
    MouseCursorException, Platform, Display, Screen, MouseCursor, \
    DefaultMouseCursor, ImageMouseCursor, BaseWindow, _PlatformEventHandler
from pyglet.window import event
from pyglet.window import key
from pyglet.window import mouse
from pyglet.event import EventDispatcher

from pyglet import gl
from pyglet.gl import gl_info
from pyglet.gl import glu_info
from pyglet.gl import glx
from pyglet.gl import glxext_arb
from pyglet.gl import glxext_mesa
from pyglet.gl import glx_info

import pyglet.window.xlib.xlib
from pyglet.window.xlib import cursorfont
try:
    import pyglet.window.xlib.xinerama
    _have_xinerama = True
except:
    _have_xinerama = False

class mwmhints_t(Structure):
    _fields_ = [
        ('flags', c_uint32),
        ('functions', c_uint32),
        ('decorations', c_uint32),
        ('input_mode', c_int32),
        ('status', c_uint32)
    ]

# 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.PAGEUP, True):     key.MOTION_BEGINNING_OF_FILE,
    (key.PAGEDOWN, True):   key.MOTION_END_OF_FILE,
    (key.BACKSPACE, False): key.MOTION_BACKSPACE,
    (key.DELETE, False):    key.MOTION_DELETE,
}

class XlibException(WindowException):
    '''An X11-specific exception.  This exception is probably a programming
    error in pyglet.'''
    pass

class XlibMouseCursor(MouseCursor):
    drawable = False

    def __init__(self, cursor):
        self.cursor = cursor

class XlibPlatform(Platform):
    def __init__(self):
        self._displays = {}

    def get_display(self, name):
        if name not in self._displays:
            self._displays[name] = XlibDisplayDevice(name)
        return self._displays[name]

    def get_default_display(self):
        return self.get_display('')

class XlibDisplayDevice(Display):
    _display = None     # POINTER(xlib.Display)

    def __init__(self, name):
        super(XlibDisplayDevice, self).__init__()

        self._display = xlib.XOpenDisplay(name)
        if not self._display:
            raise NoSuchDisplayException('Cannot connect to "%s"' % name)
        self.info = glx_info.GLXInfo(self._display)

        # Also set the default GLX display for future info queries
        glx_info.set_display(self._display.contents)

    def get_screens(self):
        x_screen = xlib.XDefaultScreen(self._display)
        if _have_xinerama and xinerama.XineramaIsActive(self._display):
            number = c_int()
            infos = xinerama.XineramaQueryScreens(self._display, 
                                                  byref(number))
            infos = cast(infos, 
                 POINTER(xinerama.XineramaScreenInfo * number.value)).contents
            result = []
            for info in infos:
                result.append(XlibScreen(self,
                                         x_screen,
                                         info.x_org,
                                         info.y_org,
                                         info.width,
                                         info.height,
                                         True))
            xlib.XFree(infos)
            return result
        else:
            # No xinerama
            screen_count = xlib.XScreenCount(self._display)
            result = []
            for i in range(screen_count):
                screen = xlib.XScreenOfDisplay(self._display, i)
                result.append(XlibScreen(self,
                                         i, 
                                         0, 0,
                                         screen.contents.width,
                                         screen.contents.height,
                                         False))
            # Move default screen to be first in list.
            s = result.pop(x_screen)
            result.insert(0, s)
            return result

class XlibScreen(Screen):
    def __init__(self, display, x_screen_id, x, y, width, height, xinerama):
        super(XlibScreen, self).__init__(x, y, width, height)
        self.display = display
        self._x_screen_id = x_screen_id
        self._xinerama = xinerama
 
    def get_matching_configs(self, template):
        x_display = self.display._display

        have_13 = self.display.info.have_version(1, 3)
        if have_13:
            config_class = XlibGLConfig13
        else:
            if 'ATI' in self.display.info.get_client_vendor():
                config_class = XlibGLConfig10ATI
            else:
                config_class = XlibGLConfig10
        
        # Construct array of attributes
        attrs = []
        for name, value in template.get_gl_attributes():
            attr = config_class.attribute_ids.get(name, None)
            if attr and value is not None:
                attrs.extend([attr, int(value)])

        if have_13:
            attrs.extend([glx.GLX_X_RENDERABLE, True])
        else:
            attrs.extend([glx.GLX_RGBA, True])

        if len(attrs):
            attrs.extend([0, 0])
            attrib_list = (c_int * len(attrs))(*attrs)
        else:
            attrib_list = None

        if have_13:
            elements = c_int()
            configs = glx.glXChooseFBConfig(x_display, self._x_screen_id,
                attrib_list, byref(elements))
            if not configs:
                return []

            configs = cast(configs, 
                           POINTER(glx.GLXFBConfig * elements.value)).contents

            result = [config_class(self, c) for c in configs]

            # Can't free array until all XlibGLConfig13's are GC'd.  Too much
            # hassle, live with leak. XXX
            #xlib.XFree(configs)

            return result
        else:
            try:
                return [config_class(self, attrib_list)]
            except gl.ContextException:
                return []

    def __repr__(self):
        return 'XlibScreen(screen=%d, x=%d, y=%d, ' \
               'width=%d, height=%d, xinerama=%d)' % \
            (self._x_screen_id, self.x, self.y, self.width, self.height,
             self._xinerama)

class XlibGLConfig(gl.Config):
    attribute_ids = {
        'buffer_size': glx.GLX_BUFFER_SIZE,
        'level': glx.GLX_LEVEL,     # Not supported
        'double_buffer': glx.GLX_DOUBLEBUFFER,
        'stereo': glx.GLX_STEREO,
        'aux_buffers': glx.GLX_AUX_BUFFERS,
        'red_size': glx.GLX_RED_SIZE,
        'green_size': glx.GLX_GREEN_SIZE,
        'blue_size': glx.GLX_BLUE_SIZE,
        'alpha_size': glx.GLX_ALPHA_SIZE,
        'depth_size': glx.GLX_DEPTH_SIZE,
        'stencil_size': glx.GLX_STENCIL_SIZE,
        'accum_red_size': glx.GLX_ACCUM_RED_SIZE,
        'accum_green_size': glx.GLX_ACCUM_GREEN_SIZE,
        'accum_blue_size': glx.GLX_ACCUM_BLUE_SIZE,
        'accum_alpha_size': glx.GLX_ACCUM_ALPHA_SIZE,
    }

    def create_context(self, share):
        context = self._create_glx_context(share)

        if context == glx.GLX_BAD_CONTEXT:
            raise gl.ContextException('Invalid context share')
        elif context == glx.GLXBadFBConfig:
            raise gl.ContextException('Invalid GL configuration')
        elif context < 0:
            raise gl.ContextException('Could not create GL context') 

        return XlibGLContext(self, context, share)

    def _create_glx_context(self, share):
        raise NotImplementedError('abstract')

    def is_complete(self):
        return True

    def get_visual_info(self):
        raise NotImplementedError('abstract')

class XlibGLConfig10(XlibGLConfig):
    def __init__(self, screen, attrib_list):
        self.screen = screen
        self._display = screen.display._display
        self._visual_info = glx.glXChooseVisual(self._display,
            screen._x_screen_id, attrib_list)
        if not self._visual_info:
            raise gl.ContextException('No conforming visual exists')

        for name, attr in self.attribute_ids.items():
            value = c_int()
            result = glx.glXGetConfig(self._display,
                self._visual_info, attr, byref(value))
            if result >= 0:
                setattr(self, name, value.value)
        self.sample_buffers = 0
        self.samples = 0

    def get_visual_info(self):
        return self._visual_info.contents

    def _create_glx_context(self, share):
        if share:
            return glx.glXCreateContext(self._display, self._visual_info,
                share._context, True)
        else:
            return glx.glXCreateContext(self._display, self._visual_info,
                None, True)

class XlibGLConfig10ATI(XlibGLConfig10):
    attribute_ids = XlibGLConfig.attribute_ids.copy()
    del attribute_ids['stereo']
    stereo = False

class XlibGLConfig13(XlibGLConfig):
    attribute_ids = XlibGLConfig.attribute_ids.copy()
    attribute_ids.update({
        'sample_buffers': glx.GLX_SAMPLE_BUFFERS,
        'samples': glx.GLX_SAMPLES,

        # Not supported in current pyglet API:
        'render_type': glx.GLX_RENDER_TYPE,
        'config_caveat': glx.GLX_CONFIG_CAVEAT,
        'transparent_type': glx.GLX_TRANSPARENT_TYPE,
        'transparent_index_value': glx.GLX_TRANSPARENT_INDEX_VALUE,
        'transparent_red_value': glx.GLX_TRANSPARENT_RED_VALUE,
        'transparent_green_value': glx.GLX_TRANSPARENT_GREEN_VALUE,
        'transparent_blue_value': glx.GLX_TRANSPARENT_BLUE_VALUE,
        'transparent_alpha_value': glx.GLX_TRANSPARENT_ALPHA_VALUE,

        # Used internally
        'x_renderable': glx.GLX_X_RENDERABLE,
    })

    def __init__(self, screen, fbconfig):
        super(XlibGLConfig13, self).__init__()
        self.screen = screen
        self._display = screen.display._display
        self._fbconfig = fbconfig
        for name, attr in self.attribute_ids.items():
            value = c_int()
            result = glx.glXGetFBConfigAttrib(
                self._display, self._fbconfig, attr, byref(value))
            if result >= 0:
                setattr(self, name, value)

    def get_visual_info(self):
        return glx.glXGetVisualFromFBConfig(
            self._display, self._fbconfig).contents

    def _create_glx_context(self, share):
        if share:
            return glx.glXCreateNewContext(self._display, self._fbconfig,
                glx.GLX_RGBA_TYPE, share._context, True)
        else:
            return glx.glXCreateNewContext(self._display, self._fbconfig,
                glx.GLX_RGBA_TYPE, None, True)

class XlibGLContext(gl.Context):
    def __init__(self, config, context, share):
        super(XlibGLContext, self).__init__(share)
        self.config = config
        self._context = context
        self._x_display = config.screen.display._display

    def destroy(self):
        super(XlibGLContext, self).destroy()
        glx.glXDestroyContext(self._x_display, self._context)

    def is_direct(self):
        return glx.glXIsDirect(self._x_display, self._context)

# Platform event data is single item, so use platform event handler directly.
XlibEventHandler = _PlatformEventHandler

class XlibWindow(BaseWindow):
    _x_display = None       # X display connection
    _x_screen_id = None     # X screen index
    _glx_context = None     # GLX context handle
    _glx_window = None      # GLX window handle
    _window = None          # Xlib window handle
    _minimum_size = None
    _maximum_size = None

    _x = 0
    _y = 0                  # Last known window position
    _width = 0
    _height = 0             # Last known window size
    _mouse_exclusive_client = None  # x,y of "real" mouse during exclusive
    _mouse_buttons = [False] * 6 # State of each xlib button
    _keyboard_exclusive = False
    _active = True
    _applied_mouse_exclusive = False
    _applied_keyboard_exclusive = False
    _mapped = False
    _lost_context = False
    _lost_context_state = False

    _default_event_mask = (0x1ffffff 
        & ~xlib.PointerMotionHintMask
        & ~xlib.ResizeRedirectMask)

    def __init__(self, *args, **kwargs):
        # Bind event handlers
        self._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:
                self._event_handlers[message] = func 

        super(XlibWindow, self).__init__(*args, **kwargs)
        
    def _recreate(self, changes):
        # If flipping to/from fullscreen and using override_redirect (we
        # always are, _NET_WM_FULLSCREEN doesn't work), need to recreate the
        # window.
        #
        # 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.switch_to()
            gl.glFlush()
            glx.glXMakeCurrent(self._x_display, 0, None)
            if self._glx_window:
                glx.glXDestroyWindow(self._x_display, self._glx_window)
            xlib.XDestroyWindow(self._x_display, self._window)
            self._glx_window = None
            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(self):
        # Unmap existing window if necessary while we fiddle with it.
        if self._window and self._mapped:
            self._unmap()

        self.context.window = self
        self._x_display = self.config._display
        self._x_screen_id = self.screen._x_screen_id
        self._glx_context = self.context._context

        self._glx_1_3 = self.display.info.have_version(1, 3)
        self._have_SGI_video_sync = \
            self.display.info.have_extension('GLX_SGI_video_sync')
        self._have_SGI_swap_control = \
            self.display.info.have_extension('GLX_SGI_swap_control')
        self._have_MESA_swap_control = \
            self.display.info.have_extension('GLX_MESA_swap_control')

        # In order of preference:
        # 1. GLX_MESA_swap_control (more likely to work where video_sync will
        #    not)
        # 2. GLX_SGI_video_sync (does not work on Intel 945GM, but that has
        #    MESA)
        # 3. GLX_SGI_swap_control (cannot be disabled once enabled).
        self._use_video_sync = (self._have_SGI_video_sync and 
                                not self._have_MESA_swap_control)

        # 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()

            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)
            self._window = xlib.XCreateWindow(self._x_display, root,
                0, 0, self._width, self._height, 0, visual_info.depth,
                xlib.InputOutput, visual, xlib.CWColormap, 
                byref(window_attributes))

            # Setting null background pixmap disables drawing the background,
            # preventing flicker while resizing (in theory).
            xlib.XSetWindowBackgroundPixmap(self._x_display, self._window, 0)

            # Enable WM_DELETE_WINDOW message
            wm_delete_window = xlib.XInternAtom(self._x_display,
                'WM_DELETE_WINDOW', False)
            wm_delete_window = c_ulong(wm_delete_window)
            xlib.XSetWMProtocols(self._x_display, self._window,
                byref(wm_delete_window), 1)

        # Set window attributes
        attributes = xlib.XSetWindowAttributes()
        attributes_mask = 0

        # Bypass the window manager in fullscreen.  This is the only reliable
        # technique (over _NET_WM_STATE_FULLSCREEN, Motif, KDE and Gnome
        # hints) that is pretty much guaranteed to work.  Unfortunately
        # we run into window activation and focus problems that require
        # attention.  Search for "override_redirect" for all occurences.
        attributes.override_redirect = self._fullscreen
        attributes_mask |= xlib.CWOverrideRedirect

        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 == self.WINDOW_STYLE_BORDERLESS:
            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, '_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:
            self.set_minimum_size(self._width, self._height)
            self.set_maximum_size(self._width, self._height)

        # Set caption
        self.set_caption(self._caption)

        self.switch_to()
        if self._visible:
            self.set_visible(True)

        # Create input context.  A good but very outdated reference for this
        # is http://www.sbin.org/doc/Xlib/chapt_11.html
        if _have_utf8:
            self._x_im = xlib.XOpenIM(self._x_display, None, None, None)
            # TODO select best input style.
            self._x_ic = xlib.XCreateIC(self._x_im, 
                'inputStyle', xlib.XIMPreeditNothing|xlib.XIMStatusNothing,
                None)
            xlib.XSetICFocus(self._x_ic)

    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.MapNotify:
                break
        xlib.XSelectInput(
            self._x_display, self._window, self._default_event_mask)
        self._mapped = True

        if self._fullscreen:
            # Possibly an override_redirect issue.
            self.activate()

        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 close(self):
        # clear out the GLX context.  Can fail if current context already
        # destroyed (on exit, say).
        try:
            gl.glFlush()
        except gl.GLException:
            pass
        glx.glXMakeCurrent(self._x_display, 0, None)

        self._unmap()
        if self._glx_window:
            glx.glXDestroyWindow(self._x_display, self._glx_window)
        if self._window:
            xlib.XDestroyWindow(self._x_display, self._window)

        self._window = None
        self._glx_window = None

        if _have_utf8:
            xlib.XDestroyIC(self._x_ic)
            xlib.XCloseIM(self._x_im)

    def switch_to(self):
        if self._glx_1_3:
            if not self._glx_window:
                self._glx_window = glx.glXCreateWindow(self._x_display,
                    self._config._fbconfig, self._window, None)
            glx.glXMakeContextCurrent(self._x_display,
                self._glx_window, self._glx_window, self._glx_context)
        else:
            glx.glXMakeCurrent(self._x_display, self._window, self._glx_context)

        self.set_vsync(self._vsync)

        self._context.set_current()
        gl_info.set_active_context()
        glu_info.set_active_context()

    def flip(self):
        self.draw_mouse_cursor()

        if self._vsync and self._have_SGI_video_sync and self._use_video_sync:
            count = c_uint()
            glxext_arb.glXGetVideoSyncSGI(byref(count))
            glxext_arb.glXWaitVideoSyncSGI(
                2, (count.value + 1) % 2, byref(count))

        if self._glx_1_3:
            if not self._glx_window:
                self._glx_window = glx.glXCreateWindow(self._x_display,
                    self._config._fbconfig, self._window, None)
            glx.glXSwapBuffers(self._x_display, self._glx_window)
        else:
            glx.glXSwapBuffers(self._x_display, self._window)

    def set_vsync(self, vsync):
        if pyglet.options['vsync'] is not None:
            vsync = pyglet.options['vsync']
        self._vsync = vsync
        if not self._use_video_sync:
            interval = vsync and 1 or 0
            if self._have_MESA_swap_control:
                glxext_mesa.glXSwapIntervalMESA(interval)
            elif self._have_SGI_swap_control and interval:
                # SGI_swap_control interval cannot be set to 0
                glxext_arb.glXSwapIntervalSGI(interval)

    def set_caption(self, 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 get_caption(self):
        return self._caption

    def set_size(self, width, height):
        if self._fullscreen:
            raise WindowException('Cannot set size of fullscreen window.')
        self._width = width
        self._height = 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.dispatch_event('on_resize', width, height)

    def get_size(self):
        # XGetGeometry and XWindowAttributes seem to always return the
        # original size of the window, which is wrong after the user
        # has resized it.
        # XXX this is probably fixed now, with fix of resize.
        return self._width, self._height

    def set_location(self, x, y):
        # 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):
        xlib.XSetInputFocus(self._x_display, self._window,
            xlib.RevertToParent, xlib.CurrentTime)

    def set_visible(self, visible=True):
        if visible:
            self._map()
        else:
            self._unmap()
        self._visible = visible

    def set_minimum_size(self, width, height):
        self._minimum_size = width, height
        self._set_wm_normal_hints()

    def set_maximum_size(self, width, height):
        self._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')

    def set_mouse_platform_visible(self, platform_visible=None):
        if platform_visible is None:
            platform_visible = self._mouse_visible and \
                               not self._mouse_cursor.drawable

        if not platform_visible:
            # Hide pointer by creating an empty cursor
            black = xlib.XBlackPixel(self._x_display, self._x_screen_id)
            black = xlib.XColor()
            bmp = xlib.XCreateBitmapFromData(self._x_display, self._window,
                c_buffer(8), 8, 8)
            cursor = xlib.XCreatePixmapCursor(self._x_display, bmp, bmp,
                black, black, 0, 0)
            xlib.XDefineCursor(self._x_display, self._window, cursor)
            xlib.XFreeCursor(self._x_display, cursor)
            xlib.XFreePixmap(self._x_display, bmp)
        else:
            # Restore 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 _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
                xlib.XWarpPointer(self._x_display,
                    0,              # src window
                    self._window,   # dst window
                    0, 0,           # src x, y
                    0, 0,           # src w, h
                    x, y)
            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 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

        self._mouse_exclusive = exclusive
        self._update_exclusivity()

    def set_exclusive_keyboard(self, exclusive=True):
        if exclusive == self._keyboard_exclusive:
            return
        
        self._keyboard_exclusive = 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
        format = {
            ('little', 4): 'BGRA',
            ('little', 8): 'BGRAAAAA',
            ('big', 4):    'ARGB',
            ('big', 8):    'AAAAARGB'
        }[(sys.byteorder, sizeof(c_ulong))]

        data = ''
        for image in images:
            image = image.image_data
            image.format = format
            image.pitch = -(image.width * len(image.format))
            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.data
        buffer = (c_ubyte * len(data))()
        memmove(buffer, data, len(data))
        atom = xlib.XInternAtom(self._x_display, '_NET_WM_ICON', False)
        XA_CARDINAL = 6 # Xatom.h:14
        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, name, False)
        if not atom:
            raise XlibException('Undefined atom "%s"' % name)
        assert type(value) in (str, unicode)
        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.XStdICCTextStyle, 
                byref(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(property))
            if result < 0:
                raise XlibException('Could not create text property')
        xlib.XSetTextProperty(self._x_display,
            self._window, byref(property), atom)
        # XXX <rj> Xlib doesn't like us freeing this
        #xlib.XFree(property.value)

    def _set_atoms_property(self, name, values, mode=xlib.PropModeReplace):
        name_atom = xlib.XInternAtom(self._x_display, name, False)
        atoms = []
        for value in values:
            atoms.append(xlib.XInternAtom(self._x_display, value, False))
        atom_type = xlib.XInternAtom(self._x_display, '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:
            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, '_NET_WM_STATE', False)
        atoms = []
        for state in states:
            atoms.append(xlib.XInternAtom(self._x_display, state, False))
        atom_type = xlib.XInternAtom(self._x_display, '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):
        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')

        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

        # Check for the events specific to this window
        while xlib.XCheckWindowEvent(_x_display, _window,
                0x1ffffff, byref(e)):
            if xlib.XFilterEvent(e, 0):
                continue
            event_handler = self._event_handlers.get(e.type)
            if event_handler:
                event_handler(e)

        # Generic events for this window (the window close event).
        while xlib.XCheckTypedWindowEvent(_x_display, _window, 
                xlib.ClientMessage, byref(e)):
            event_handler = self._event_handlers.get(e.type)
            if event_handler:
                event_handler(e)
        
        self._allow_dispatch_event = False

    @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 not in key._key_names.keys():
            symbol = key.user_key(event.xkey.keycode)
        return symbol

    def _event_text(self, event):
        if event.type == xlib.KeyPress:
            buffer = create_string_buffer(16)
            text = None
            if _have_utf8:
                count = xlib.Xutf8LookupString(self._x_ic,
                                               event.xkey,
                                               buffer, len(buffer),
                                               None, None)
                if count > 0:
                    text = buffer.value[:count].decode('utf8')
            else:
                count = xlib.XLookupString(event.xkey,
                                           buffer, len(buffer),
                                           None, None)
                if count > 0:
                    text = buffer.value[:count].decode('ascii', 'ignore')
            if text and unicodedata.category(text) != 'Cc' or text == '\r':
                return text
        return None

    def _event_text_motion(self, symbol, modifiers):
        if modifiers & key.MOD_ALT:
            return None
        ctrl = modifiers & key.MOD_CTRL != 0
        return _motion_map.get((symbol, ctrl), None)

    @XlibEventHandler(xlib.KeyPress)
    @XlibEventHandler(xlib.KeyRelease)
    def _event_key(self, ev):
        if 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
                    symbol = self._event_symbol(ev)
                    modifiers = self._translate_modifiers(ev.xkey.state)
                    text = self._event_text(auto_event)
                    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:
                        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))

        symbol = self._event_symbol(ev)
        modifiers = self._translate_modifiers(ev.xkey.state)
        text = self._event_text(ev)
        motion = self._event_text_motion(symbol, modifiers)

        if ev.type == xlib.KeyPress:
            self.dispatch_event('on_key_press', 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:
                self.dispatch_event('on_text', text)
        elif ev.type == xlib.KeyRelease:
            self.dispatch_event('on_key_release', symbol, modifiers)

    @XlibEventHandler(xlib.MotionNotify)
    def _event_motionnotify(self, ev):
        x = ev.xmotion.x
        y = self.height - ev.xmotion.y

        dx = x - self._mouse_x
        dy = y - self._mouse_y

        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

        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.ClientMessage)
    def _event_clientmessage(self, ev):
        wm_delete_window = xlib.XInternAtom(ev.xclient.display,
            'WM_DELETE_WINDOW', False)
        if ev.xclient.data.l[0] == wm_delete_window:
            self.dispatch_event('on_close')

    @XlibEventHandler(xlib.ButtonPress)
    @XlibEventHandler(xlib.ButtonRelease)
    def _event_button(self, ev):
        x = ev.xbutton.x
        y = self.height - ev.xbutton.y
        button = 1 << (ev.xbutton.button - 1)  # 1, 2, 3 -> 1, 2, 4
        modifiers = self._translate_modifiers(ev.xbutton.state)
        if ev.type == xlib.ButtonPress:
            # override_redirect issue: manually activate this window if
            # fullscreen.
            if self._fullscreen 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 < len(self._mouse_buttons):
                self._mouse_buttons[ev.xbutton.button] = True
                self.dispatch_event('on_mouse_press', 
                    x, y, button, modifiers)
        else:
            if ev.xbutton.button < 4:
                self._mouse_buttons[ev.xbutton.button] = False
                self.dispatch_event('on_mouse_release', 
                    x, y, button, modifiers)

    @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')

    @XlibEventHandler(xlib.EnterNotify)
    def _event_enternotify(self, ev):
        # figure active mouse buttons
        # XXX ignore modifier state?
        state = ev.xcrossing.state
        self._mouse_buttons[1] = state & xlib.Button1Mask
        self._mouse_buttons[2] = state & xlib.Button2Mask
        self._mouse_buttons[3] = state & xlib.Button3Mask
        self._mouse_buttons[4] = state & xlib.Button4Mask
        self._mouse_buttons[5] = state & xlib.Button5Mask

        # 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)

    @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):
        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.dispatch_event('on_resize', w, h)
            self.dispatch_event('on_expose')
        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')

    @XlibEventHandler(xlib.FocusOut)
    def _event_focusout(self, ev):
        self._active = False
        self._update_exclusivity()
        self.dispatch_event('on_deactivate')

    @XlibEventHandler(xlib.MapNotify)
    def _event_mapnotify(self, ev):
        self._mapped = True
        self.dispatch_event('on_show')

    @XlibEventHandler(xlib.UnmapNotify)
    def _event_unmapnotify(self, ev):
        self._mapped = False
        self.dispatch_event('on_hide')
