diff --git a/libs/pyglet/__init__.py b/libs/pyglet/__init__.py index df76348..de635af 100644 --- a/libs/pyglet/__init__.py +++ b/libs/pyglet/__init__.py @@ -329,6 +329,7 @@ class _ModuleProxy: # Lazily load all modules, except if performing # type checking or code inspection. if TYPE_CHECKING: + from . import animation from . import app from . import canvas from . import clock @@ -349,6 +350,7 @@ if TYPE_CHECKING: from . import text from . import window else: + animation = _ModuleProxy('animation') app = _ModuleProxy('app') canvas = _ModuleProxy('canvas') clock = _ModuleProxy('clock') diff --git a/libs/pyglet/animation.py b/libs/pyglet/animation.py new file mode 100644 index 0000000..93fed35 --- /dev/null +++ b/libs/pyglet/animation.py @@ -0,0 +1,142 @@ +"""Animations + +Animations can be used by the :py:class:`~pyglet.sprite.Sprite` class in place +of static images. They are essentially containers for individual image frames, +with a duration per frame. They can be infinitely looping, or stop at the last +frame. You can load Animations from disk, such as from GIF files:: + + ani = pyglet.resource.animation('walking.gif') + sprite = pyglet.sprite.Sprite(img=ani) + +Alternatively, you can create your own Animations from a sequence of images +by using the :py:meth:`~Animation.from_image_sequence` method:: + + images = [pyglet.resource.image('walk_a.png'), + pyglet.resource.image('walk_b.png'), + pyglet.resource.image('walk_c.png')] + + ani = pyglet.image.Animation.from_image_sequence(images, duration=0.1, loop=True) + +You can also use an :py:class:`pyglet.image.ImageGrid`, which is iterable:: + + sprite_sheet = pyglet.resource.image('my_sprite_sheet.png') + image_grid = pyglet.image.ImageGrid(sprite_sheet, rows=1, columns=5) + + ani = pyglet.animation.Animation.from_image_sequence(image_grid, duration=0.1) + +In the above examples, all the Animation Frames have the same duration. +If you wish to adjust this, you can manually create the Animation from a list of +:py:class:`~AnimationFrame`:: + + image_a = pyglet.resource.image('walk_a.png') + image_b = pyglet.resource.image('walk_b.png') + image_c = pyglet.resource.image('walk_c.png') + + frame_a = AnimationFrame(image_a, duration=0.1) + frame_b = AnimationFrame(image_b, duration=0.2) + frame_c = AnimationFrame(image_c, duration=0.1) + + ani = pyglet.image.Animation(frames=[frame_a, frame_b, frame_c]) + +""" + +from pyglet import clock as _clock +from pyglet import event as _event + + +class AnimationController(_event.EventDispatcher): + + _frame_index: int = 0 + _next_dt: float = 0.0 + _paused: bool = False + + _animation: 'Animation' + + def _animate(self, dt): + """ + Subclasses of AnimationController should provide their own + _animate method. This method should determine + """ + raise NotImplementedError + + @property + def paused(self) -> bool: + """Pause/resume the Animation.""" + return self._paused + + @paused.setter + def paused(self, pause): + if not self._animation or pause == self._paused: + return + if pause is True: + _clock.unschedule(self._animate) + else: + frame = self._animation.frames[self._frame_index] + self._next_dt = frame.duration + if self._next_dt: + _clock.schedule_once(self._animate, self._next_dt) + self._paused = pause + + @property + def frame_index(self) -> int: + """The current AnimationFrame.""" + return self._frame_index + + @frame_index.setter + def frame_index(self, index): + # Bound to available number of frames + if self._animation is None: + return + self._frame_index = max(0, min(index, len(self._animation.frames)-1)) + + +AnimationController.register_event_type('on_animation_end') + + +class Animation: + """Sequence of AnimationFrames. + + If no frames of the animation have a duration of ``None``, the animation + loops continuously; otherwise the animation stops at the first frame with + duration of ``None``. + + :Ivariables: + `frames` : list of `~pyglet.animation.AnimationFrame` + The frames that make up the animation. + + """ + + __slots__ = 'frames' + + def __init__(self, frames: list): + """Create an animation directly from a list of frames.""" + assert len(frames) + self.frames = frames + + def get_duration(self) -> float: + """Get the total duration of the animation in seconds.""" + return sum([frame.duration for frame in self.frames if frame.duration is not None]) + + @classmethod + def from_sequence(cls, sequence: list, duration: float, loop: bool = True): + """Create an animation from a list of objects and a constant framerate.""" + frames = [AnimationFrame(image, duration) for image in sequence] + if not loop: + frames[-1].duration = None + return cls(frames) + + def __repr__(self): + return "Animation(frames={0})".format(len(self.frames)) + + +class AnimationFrame: + """A single frame of an animation.""" + + __slots__ = 'data', 'duration' + + def __init__(self, data, duration): + self.data = data + self.duration = duration + + def __repr__(self): + return f"AnimationFrame({self.data}, duration={self.duration})" diff --git a/libs/pyglet/app/cocoa.py b/libs/pyglet/app/cocoa.py index 4509f1c..5f1af49 100644 --- a/libs/pyglet/app/cocoa.py +++ b/libs/pyglet/app/cocoa.py @@ -1,26 +1,14 @@ from pyglet.app.base import PlatformEventLoop -from pyglet.libs.darwin import cocoapy +from pyglet.libs.darwin import cocoapy, AutoReleasePool NSApplication = cocoapy.ObjCClass('NSApplication') NSMenu = cocoapy.ObjCClass('NSMenu') NSMenuItem = cocoapy.ObjCClass('NSMenuItem') -NSAutoreleasePool = cocoapy.ObjCClass('NSAutoreleasePool') NSDate = cocoapy.ObjCClass('NSDate') -NSDate.dateWithTimeIntervalSinceNow_.no_cached_return() -NSDate.distantFuture.no_cached_return() - NSEvent = cocoapy.ObjCClass('NSEvent') NSUserDefaults = cocoapy.ObjCClass('NSUserDefaults') -class AutoReleasePool: - def __enter__(self): - self.pool = NSAutoreleasePool.alloc().init() - return self.pool - - def __exit__(self, exc_type, exc_value, traceback): - self.pool.drain() - del self.pool def add_menu_item(menu, title, action, key): @@ -33,8 +21,6 @@ def add_menu_item(menu, title, action, key): menu.addItem_(menuItem) # cleanup - title.release() - key.release() menuItem.release() diff --git a/libs/pyglet/graphics/__init__.py b/libs/pyglet/graphics/__init__.py index a365da3..972a126 100644 --- a/libs/pyglet/graphics/__init__.py +++ b/libs/pyglet/graphics/__init__.py @@ -5,53 +5,6 @@ Shaders and Buffers. It also provides classes for highly performant batched rendering and grouping. See the :ref:`guide_graphics` for details on how to use this graphics API. - -Batches and groups -================== - -Developers can make use of :py:class:`~pyglet.graphics.Batch` and -:py:class:`~pyglet.graphics.Group` objects to improve performance when -rendering a large number of objects. - -The :py:class:`~pyglet.sprite.Sprite`, :py:func:`~pyglet.text.Label`, -:py:func:`~pyglet.text.layout.TextLayout`, and other classes all accept a -``batch`` and ``group`` parameter in their constructors. A Batch manages -a set of objects that will be drawn all at once, and a Group can be used -to set OpenGL state and further sort the draw operation. - -The following example creates a batch, adds two sprites to the batch, and then -draws the entire batch:: - - batch = pyglet.graphics.Batch() - car = pyglet.sprite.Sprite(car_image, batch=batch) - boat = pyglet.sprite.Sprite(boat_image, batch=batch) - - def on_draw(): - batch.draw() - -Drawing a complete Batch is much faster than drawing the items in the batch -individually, especially when those items belong to a common group. - -Groups describe the OpenGL state required for an item. This is for the most -part managed by the sprite, text, and other classes, however you can also use -custom groups to ensure items are drawn in a particular order. For example, the -following example adds a background sprite which is guaranteed to be drawn -before the car and the boat:: - - batch = pyglet.graphics.Batch() - background = pyglet.graphics.Group(order=0) - foreground = pyglet.graphics.Group(order=1) - - background = pyglet.sprite.Sprite(background_image, batch=batch, group=background) - car = pyglet.sprite.Sprite(car_image, batch=batch, group=foreground) - boat = pyglet.sprite.Sprite(boat_image, batch=batch, group=foreground) - - def on_draw(): - batch.draw() - -It's preferable to manage pyglet objects within as few batches as possible. If -the drawing of sprites or text objects need to be interleaved with other -drawing that does not use the graphics API, multiple batches will be required. """ import ctypes @@ -255,15 +208,31 @@ def get_default_shader(): class Batch: - """Manage a collection of vertex lists for batched rendering. + """Manage a collection of drawables for batched rendering. - Vertex lists are added to a :py:class:`~pyglet.graphics.Batch` using the - `add` and `add_indexed` methods. An optional group can be specified along - with the vertex list, which gives the OpenGL state required for its rendering. - Vertex lists with shared mode and group are allocated into adjacent areas of - memory and sent to the graphics card in a single operation. + Many drawable pyglet objects accept an optional `Batch` argument in their + constructors. By giving a `Batch` to multiple objects, you can tell pyglet + that you expect to draw all of these objects at once, so it can optimise its + use of OpenGL. Hence, drawing a `Batch` is often much faster than drawing + each contained drawable separately. - Call `VertexList.delete` to remove a vertex list from the batch. + The following example creates a batch, adds two sprites to the batch, and + then draws the entire batch:: + + batch = pyglet.graphics.Batch() + car = pyglet.sprite.Sprite(car_image, batch=batch) + boat = pyglet.sprite.Sprite(boat_image, batch=batch) + + def on_draw(): + batch.draw() + + While any drawables can be added to a `Batch`, only those with the same + draw mode, shader program, and group can be optimised together. + + Internally, a `Batch` manages a set of VertexDomains along with + information about how the domains are to be drawn. To implement batching on + a custom drawable, get your vertex domains from the given batch instead of + setting them up yourself. """ def __init__(self): @@ -325,6 +294,7 @@ class Batch: vertex_list.migrate(domain) def get_domain(self, indexed, mode, group, program, attributes): + """Get, or create, the vertex domain corresponding to the given arguments.""" if group is None: group = ShaderGroup(program=program) @@ -496,27 +466,43 @@ class Batch: class Group: """Group of common OpenGL state. - Before a VertexList is rendered, its Group's OpenGL state is set. - This includes binding textures, shaders, or setting any other parameters. + `Group` provides extra control over how drawables are handled within a + `Batch`. When a batch draws a drawable, it ensures its group's state is set; + this can include binding textures, shaders, or setting any other parameters. + It also sorts the groups before drawing. + + In the following example, the background sprite is guaranteed to be drawn + before the car and the boat:: + + batch = pyglet.graphics.Batch() + background = pyglet.graphics.Group(order=0) + foreground = pyglet.graphics.Group(order=1) + + background = pyglet.sprite.Sprite(background_image, batch=batch, group=background) + car = pyglet.sprite.Sprite(car_image, batch=batch, group=foreground) + boat = pyglet.sprite.Sprite(boat_image, batch=batch, group=foreground) + + def on_draw(): + batch.draw() + + :Parameters: + `order` : int + Set the order to render above or below other Groups. + Lower orders are drawn first. + `parent` : `~pyglet.graphics.Group` + Group to contain this Group; its state will be set before this + Group's state. + + :Variables: + `visible` : bool + Determines whether this Group is visible in any of the Batches + it is assigned to. If ``False``, objects in this Group will not + be rendered. + `batches` : list + Read Only. A list of which Batches this Group is a part of. """ def __init__(self, order=0, parent=None): - """Create a Group. - :Parameters: - `order` : int - Set the order to render above or below other Groups. - `parent` : `~pyglet.graphics.Group` - Group to contain this Group; its state will be set before this - Group's state. - - :Ivariables: - `visible` : bool - Determines whether this Group is visible in any of the Batches - it is assigned to. If False, objects in this Group will not - be rendered. - `batches` : list - Read Only. A list of which Batches this Group is a part of. - """ self._order = order self.parent = parent self._visible = True diff --git a/libs/pyglet/graphics/shader.py b/libs/pyglet/graphics/shader.py index e9748c0..f14d0f6 100644 --- a/libs/pyglet/graphics/shader.py +++ b/libs/pyglet/graphics/shader.py @@ -645,19 +645,16 @@ class ShaderSource: class Shader: - """OpenGL Shader object""" + """OpenGL shader. + + Shader objects are compiled on instantiation. + You can reuse a Shader object in multiple ShaderPrograms. + + `shader_type` is one of ``'compute'``, ``'fragment'``, ``'geometry'``, + ``'tesscontrol'``, ``'tessevaluation'``, or ``'vertex'``. + """ def __init__(self, source_string: str, shader_type: str): - """Create an instance of a Shader object. - - Shader source code should be provided as a Python string. - The shader_type should be a string indicating the type. - Valid types are: 'compute', 'fragment', 'geometry', 'tesscontrol', - 'tessevaluation', and 'vertex'. - - Shader objects are compiled on instantiation. - You can reuse a Shader object in multiple `ShaderProgram`s. - """ self._id = None self.type = shader_type @@ -735,12 +732,11 @@ class Shader: class ShaderProgram: - """OpenGL Shader Program""" + """OpenGL shader program.""" __slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__' def __init__(self, *shaders: Shader): - """Create an OpenGL ShaderProgram from one or more Shader objects.""" assert shaders, "At least one Shader object is required." self._id = _link_program(*shaders) self._context = pyglet.gl.current_context @@ -825,6 +821,7 @@ class ShaderProgram: `mode` : int OpenGL drawing mode enumeration; for example, one of ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + This determines how the list is drawn in the given batch. `batch` : `~pyglet.graphics.Batch` Batch to add the VertexList to, or ``None`` if a Batch will not be used. Using a Batch is strongly recommended. @@ -867,6 +864,7 @@ class ShaderProgram: `mode` : int OpenGL drawing mode enumeration; for example, one of ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. + This determines how the list is drawn in the given batch. `indices` : sequence of int Sequence of integers giving indices into the vertex list. `batch` : `~pyglet.graphics.Batch` diff --git a/libs/pyglet/image/__init__.py b/libs/pyglet/image/__init__.py index 0af8426..8c19d14 100644 --- a/libs/pyglet/image/__init__.py +++ b/libs/pyglet/image/__init__.py @@ -97,19 +97,17 @@ import re import weakref from ctypes import * -from io import open, BytesIO +from io import open import pyglet -from pyglet.gl import * -from pyglet.gl import gl_info from pyglet.util import asbytes +from pyglet.animation import Animation -from .codecs import ImageEncodeException, ImageDecodeException from .codecs import registry as _codec_registry from .codecs import add_default_codecs as _add_default_codecs +from .codecs import ImageEncodeException, ImageDecodeException -from .animation import Animation, AnimationFrame from .buffer import * from . import atlas @@ -449,7 +447,7 @@ class AbstractImageSequence: .. versionadded:: 1.1 """ - return Animation.from_image_sequence(self, period, loop) + return Animation.from_sequence(self, period, loop) def __getitem__(self, slice): """Retrieve a (list of) image. diff --git a/libs/pyglet/image/animation.py b/libs/pyglet/image/animation.py deleted file mode 100644 index 169dd5e..0000000 --- a/libs/pyglet/image/animation.py +++ /dev/null @@ -1,179 +0,0 @@ -"""2D Animations - -Animations can be used by the :py:class:`~pyglet.sprite.Sprite` class in place -of static images. They are essentially containers for individual image frames, -with a duration per frame. They can be infinitely looping, or stop at the last -frame. You can load Animations from disk, such as from GIF files:: - - ani = pyglet.resource.animation('walking.gif') - sprite = pyglet.sprite.Sprite(img=ani) - -Alternatively, you can create your own Animations from a sequence of images -by using the :py:meth:`~Animation.from_image_sequence` method:: - - images = [pyglet.resource.image('walk_a.png'), - pyglet.resource.image('walk_b.png'), - pyglet.resource.image('walk_c.png')] - - ani = pyglet.image.Animation.from_image_sequence(images, duration=0.1, loop=True) - -You can also use an :py:class:`pyglet.image.ImageGrid`, which is iterable:: - - sprite_sheet = pyglet.resource.image('my_sprite_sheet.png') - image_grid = pyglet.image.ImageGrid(sprite_sheet, rows=1, columns=5) - - ani = pyglet.image.Animation.from_image_sequence(image_grid, duration=0.1) - -In the above examples, all of the Animation Frames have the same duration. -If you wish to adjust this, you can manually create the Animation from a list of -:py:class:`~AnimationFrame`:: - - image_a = pyglet.resource.image('walk_a.png') - image_b = pyglet.resource.image('walk_b.png') - image_c = pyglet.resource.image('walk_c.png') - - frame_a = pyglet.image.AnimationFrame(image_a, duration=0.1) - frame_b = pyglet.image.AnimationFrame(image_b, duration=0.2) - frame_c = pyglet.image.AnimationFrame(image_c, duration=0.1) - - ani = pyglet.image.Animation(frames=[frame_a, frame_b, frame_c]) - -""" - - -class Animation: - """Sequence of images with timing information. - - If no frames of the animation have a duration of ``None``, the animation - loops continuously; otherwise the animation stops at the first frame with - duration of ``None``. - - :Ivariables: - `frames` : list of `~pyglet.image.AnimationFrame` - The frames that make up the animation. - - """ - - def __init__(self, frames): - """Create an animation directly from a list of frames. - - :Parameters: - `frames` : list of `~pyglet.image.AnimationFrame` - The frames that make up the animation. - - """ - assert len(frames) - self.frames = frames - - def add_to_texture_bin(self, texture_bin, border=0): - """Add the images of the animation to a :py:class:`~pyglet.image.atlas.TextureBin`. - - The animation frames are modified in-place to refer to the texture bin - regions. - - :Parameters: - `texture_bin` : `~pyglet.image.atlas.TextureBin` - Texture bin to upload animation frames into. - `border` : int - Leaves specified pixels of blank space around - each image frame when adding to the TextureBin. - - """ - for frame in self.frames: - frame.image = texture_bin.add(frame.image, border) - - def get_transform(self, flip_x=False, flip_y=False, rotate=0): - """Create a copy of this animation applying a simple transformation. - - The transformation is applied around the image's anchor point of - each frame. The texture data is shared between the original animation - and the transformed animation. - - :Parameters: - `flip_x` : bool - If True, the returned animation will be flipped horizontally. - `flip_y` : bool - If True, the returned animation will be flipped vertically. - `rotate` : int - Degrees of clockwise rotation of the returned animation. Only - 90-degree increments are supported. - - :rtype: :py:class:`~pyglet.image.Animation` - """ - frames = [AnimationFrame(frame.image.get_texture().get_transform(flip_x, flip_y, rotate), - frame.duration) for frame in self.frames] - return Animation(frames) - - def get_duration(self): - """Get the total duration of the animation in seconds. - - :rtype: float - """ - return sum([frame.duration for frame in self.frames if frame.duration is not None]) - - def get_max_width(self): - """Get the maximum image frame width. - - This method is useful for determining texture space requirements: due - to the use of ``anchor_x`` the actual required playback area may be - larger. - - :rtype: int - """ - return max([frame.image.width for frame in self.frames]) - - def get_max_height(self): - """Get the maximum image frame height. - - This method is useful for determining texture space requirements: due - to the use of ``anchor_y`` the actual required playback area may be - larger. - - :rtype: int - """ - return max([frame.image.height for frame in self.frames]) - - @classmethod - def from_image_sequence(cls, sequence, duration, loop=True): - """Create an animation from a list of images and a constant framerate. - - :Parameters: - `sequence` : list of `~pyglet.image.AbstractImage` - Images that make up the animation, in sequence. - `duration` : float - Number of seconds to display each image. - `loop` : bool - If True, the animation will loop continuously. - - :rtype: :py:class:`~pyglet.image.Animation` - """ - frames = [AnimationFrame(image, duration) for image in sequence] - if not loop: - frames[-1].duration = None - return cls(frames) - - def __repr__(self): - return "Animation(frames={0})".format(len(self.frames)) - - -class AnimationFrame: - """A single frame of an animation.""" - - __slots__ = 'image', 'duration' - - def __init__(self, image, duration): - """Create an animation frame from an image. - - :Parameters: - `image` : `~pyglet.image.AbstractImage` - The image of this frame. - `duration` : float - Number of seconds to display the frame, or ``None`` if it is - the last frame in the animation. - - """ - self.image = image - self.duration = duration - - def __repr__(self): - return "AnimationFrame({0}, duration={1})".format(self.image, self.duration) diff --git a/libs/pyglet/image/codecs/gdkpixbuf2.py b/libs/pyglet/image/codecs/gdkpixbuf2.py index 8b88a2c..6c5753f 100644 --- a/libs/pyglet/image/codecs/gdkpixbuf2.py +++ b/libs/pyglet/image/codecs/gdkpixbuf2.py @@ -4,6 +4,7 @@ from pyglet.gl import * from pyglet.image import * from pyglet.image.codecs import * from pyglet.image.codecs import gif +from pyglet.animation import AnimationFrame import pyglet.lib import pyglet.window diff --git a/libs/pyglet/libs/darwin/cocoapy/__init__.py b/libs/pyglet/libs/darwin/cocoapy/__init__.py index 165d832..7fa2aee 100644 --- a/libs/pyglet/libs/darwin/cocoapy/__init__.py +++ b/libs/pyglet/libs/darwin/cocoapy/__init__.py @@ -35,3 +35,15 @@ from .runtime import ObjCClass, ObjCInstance, ObjCSubclass from .cocoatypes import * from .cocoalibs import * + +NSAutoreleasePool = ObjCClass('NSAutoreleasePool') + +class AutoReleasePool: + """Helper context function to more easily manage NSAutoreleasePool""" + def __enter__(self): + self.pool = NSAutoreleasePool.alloc().init() + return self.pool + + def __exit__(self, exc_type, exc_value, traceback): + self.pool.drain() + del self.pool diff --git a/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py b/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py index e6a26f6..1384a2d 100644 --- a/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py +++ b/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py @@ -47,8 +47,7 @@ cf.CFAttributedStringCreate.argtypes = [CFAllocatorRef, c_void_p, c_void_p] # Core Foundation type to Python type conversion functions def CFSTR(string): - return ObjCInstance(c_void_p(cf.CFStringCreateWithCString( - None, string.encode('utf8'), kCFStringEncodingUTF8))) + return cf.CFStringCreateWithCString(None, string.encode('utf8'), kCFStringEncodingUTF8) # Other possible names for this method: # at, ampersat, arobe, apenstaartje (little monkey tail), strudel, @@ -57,7 +56,7 @@ def CFSTR(string): # kukac (caterpillar). def get_NSString(string): """Autoreleased version of CFSTR""" - return CFSTR(string).autorelease() + return ObjCInstance(c_void_p(CFSTR(string))).autorelease() def cfstring_to_string(cfstring): length = cf.CFStringGetLength(cfstring) diff --git a/libs/pyglet/libs/darwin/cocoapy/runtime.py b/libs/pyglet/libs/darwin/cocoapy/runtime.py index ba23a06..77ffc55 100644 --- a/libs/pyglet/libs/darwin/cocoapy/runtime.py +++ b/libs/pyglet/libs/darwin/cocoapy/runtime.py @@ -409,7 +409,6 @@ objc.sel_isEqual.argtypes = [c_void_p, c_void_p] objc.sel_registerName.restype = c_void_p objc.sel_registerName.argtypes = [c_char_p] - ###################################################################### # Constants OBJC_ASSOCIATION_ASSIGN = 0 # Weak reference to the associated object. @@ -480,6 +479,8 @@ def should_use_fpret(restype): # change these values. restype should be a ctypes type # and argtypes should be a list of ctypes types for # the arguments of the message only. +# Note: kwarg 'argtypes' required if using args, or will fail on ARM64. + def send_message(receiver, selName, *args, **kwargs): if isinstance(receiver, str): receiver = get_class(receiver) @@ -624,11 +625,11 @@ def cfunctype_for_encoding(encoding): return cfunctype_table[encoding] # Otherwise, create a new CFUNCTYPE for the encoding. - typecodes = {b'c': c_char, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, - b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, - b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'*': c_char_p, - b'@': c_void_p, b'#': c_void_p, b':': c_void_p, NSPointEncoding: NSPoint, - NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange, + typecodes = {b'c': c_char, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, + b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, + b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'*': c_char_p, + b'@': c_void_p, b'#': c_void_p, b':': c_void_p, NSPointEncoding: NSPoint, + NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange, PyObjectEncoding: py_object} argtypes = [] for code in parse_type_encoding(encoding): @@ -704,12 +705,12 @@ class ObjCMethod: # Note, need to map 'c' to c_byte rather than c_char, because otherwise # ctypes converts the value into a one-character string which is generally # not what we want at all, especially when the 'c' represents a bool var. - typecodes = {b'c': c_byte, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, - b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, - b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'Vv': None, b'*': c_char_p, - b'@': c_void_p, b'#': c_void_p, b':': c_void_p, b'^v': c_void_p, b'?': c_void_p, - NSPointEncoding: NSPoint, NSSizeEncoding: NSSize, NSRectEncoding: NSRect, - NSRangeEncoding: NSRange, + typecodes = {b'c': c_byte, b'i': c_int, b's': c_short, b'l': c_long, b'q': c_longlong, + b'C': c_ubyte, b'I': c_uint, b'S': c_ushort, b'L': c_ulong, b'Q': c_ulonglong, + b'f': c_float, b'd': c_double, b'B': c_bool, b'v': None, b'Vv': None, b'*': c_char_p, + b'@': c_void_p, b'#': c_void_p, b':': c_void_p, b'^v': c_void_p, b'?': c_void_p, + NSPointEncoding: NSPoint, NSSizeEncoding: NSSize, NSRectEncoding: NSRect, + NSRangeEncoding: NSRange, PyObjectEncoding: py_object} cfunctype_table = {} @@ -717,7 +718,6 @@ class ObjCMethod: def __init__(self, method): """Initialize with an Objective-C Method pointer. We then determine the return type and argument type information of the method.""" - self.cache = True self.selector = c_void_p(objc.method_getName(method)) self.name = objc.sel_getName(self.selector) self.pyname = self.name.replace(b':', b'_') @@ -734,7 +734,7 @@ class ObjCMethod: try: self.argtypes = [self.ctype_for_encoding(t) for t in self.argument_types] except: - # print(f'no argtypes encoding for {self.name} ({self.argument_types})') + # print('no argtypes encoding for %s (%s)' % (self.name, self.argument_types)) self.argtypes = None # Get types for the return type. try: @@ -745,7 +745,7 @@ class ObjCMethod: else: self.restype = self.ctype_for_encoding(self.return_type) except: - # print(f'no restype encoding for {self.name} ({self.return_type})') + # print('no restype encoding for %s (%s)' % (self.name, self.return_type)) self.restype = None self.func = None @@ -803,7 +803,7 @@ class ObjCMethod: result = f(objc_id, self.selector, *args) # Convert result to python type if it is a instance or class pointer. if self.restype == ObjCInstance: - result = ObjCInstance(result, self.cache) + result = ObjCInstance(result) elif self.restype == ObjCClass: result = ObjCClass(result) return result @@ -826,13 +826,6 @@ class ObjCBoundMethod: self.method = method self.objc_id = objc_id - def no_cached_return(self): - """Disables the return type from being registered in DeallocationObserver. - Some return types do not get observed and will cause a memory leak. - Ex: NDate return types can be __NSTaggedDate - """ - self.method.cache = False - def __repr__(self): return '' % (self.method.name, self.objc_id) @@ -967,8 +960,49 @@ class ObjCClass: ###################################################################### + +class _AutoreleasepoolManager: + def __init__(self): + self.current = 0 # Current Pool ID. 0 is Global and not removed. + self.pools = [None] # List of NSAutoreleasePools. + + @property + def count(self): + """Number of total pools. Not including global.""" + return len(self.pools) - 1 + + def create(self, pool): + self.pools.append(pool) + self.current = self.pools.index(pool) + + def delete(self, pool): + self.pools.remove(pool) + self.current = len(self.pools) - 1 + + +_arp_manager = _AutoreleasepoolManager() + +_dealloc_argtype = [c_void_p] # Just to prevent list creation every call. + +def _set_dealloc_observer(objc_ptr): + # Create a DeallocationObserver and associate it with this object. + # When the Objective-C object is deallocated, the observer will remove + # the ObjCInstance corresponding to the object from the cached objects + # dictionary, effectively destroying the ObjCInstance. + observer = send_message('DeallocationObserver', 'alloc') + observer = send_message(observer, 'initWithObject:', objc_ptr, argtypes=_dealloc_argtype) + objc.objc_setAssociatedObject(objc_ptr, observer, observer, OBJC_ASSOCIATION_RETAIN) + + # The observer is retained by the object we associate it to. We release + # the observer now so that it will be deallocated when the associated + # object is deallocated. + send_message(observer, 'release') + + class ObjCInstance: """Python wrapper for an Objective-C instance.""" + pool = 0 # What pool id this belongs in. + retained = False # If instance is kept even if pool is wiped. _cached_objects = {} @@ -1004,17 +1038,16 @@ class ObjCInstance: if cache: cls._cached_objects[object_ptr.value] = objc_instance - # Create a DeallocationObserver and associate it with this object. - # When the Objective-C object is deallocated, the observer will remove - # the ObjCInstance corresponding to the object from the cached objects - # dictionary, effectively destroying the ObjCInstance. - observer = send_message(send_message('DeallocationObserver', 'alloc'), 'initWithObject:', objc_instance) - objc.objc_setAssociatedObject(objc_instance, observer, observer, OBJC_ASSOCIATION_RETAIN) - - # The observer is retained by the object we associate it to. We release - # the observer now so that it will be deallocated when the associated - # object is deallocated. - send_message(observer, 'release') + # Creation of NSAutoreleasePool instance does not technically mean it was allocated and initialized, but + # it's standard practice, so this should not be an issue. + if objc_instance.objc_class.name == b"NSAutoreleasePool": + _arp_manager.create(objc_instance) + objc_instance.pool = _arp_manager.current + _set_dealloc_observer(object_ptr) + elif _arp_manager.current: + objc_instance.pool = _arp_manager.current + else: + _set_dealloc_observer(object_ptr) return objc_instance @@ -1048,12 +1081,16 @@ class ObjCInstance: # Otherwise raise an exception. raise AttributeError('ObjCInstance %s has no attribute %s' % (self.objc_class.name, name)) + def get_cached_instances(): """For debug purposes, return a list of instance names. Useful for debugging if an object is leaking.""" return [obj.objc_class.name for obj in ObjCInstance._cached_objects.values()] + + ###################################################################### + def convert_method_arguments(encoding, args): """Used by ObjCSubclass to convert Objective-C method arguments to Python values before passing them on to the Python-defined method.""" @@ -1183,8 +1220,9 @@ class ObjCSubclass: def decorator(f): def objc_method(objc_self, objc_cmd, *args): - py_self = ObjCInstance(objc_self) + py_self = ObjCInstance(objc_self, True) py_self.objc_cmd = objc_cmd + py_self.retained = True args = convert_method_arguments(encoding, args) result = f(py_self, *args) if isinstance(result, ObjCClass): @@ -1246,17 +1284,15 @@ class DeallocationObserver_Implementation: DeallocationObserver.register() @DeallocationObserver.rawmethod('@@') - def initWithObject_(self, cmd, anObject): + def initWithObject_(self, cmd, objc_ptr): self = send_super(self, 'init') self = self.value - set_instance_variable(self, 'observed_object', anObject, c_void_p) + set_instance_variable(self, 'observed_object', objc_ptr, c_void_p) return self @DeallocationObserver.rawmethod('v') def dealloc(self, cmd): - anObject = get_instance_variable(self, 'observed_object', c_void_p) - ObjCInstance._cached_objects.pop(anObject, None) - send_super(self, 'dealloc') + _obj_observer_dealloc(self, 'dealloc') @DeallocationObserver.rawmethod('v') def finalize(self, cmd): @@ -1264,6 +1300,40 @@ class DeallocationObserver_Implementation: # (which would have to be explicitly started with # objc_startCollectorThread(), so probably not too much reason # to have this here, but I guess it can't hurt.) - anObject = get_instance_variable(self, 'observed_object', c_void_p) - ObjCInstance._cached_objects.pop(anObject, None) - send_super(self, 'finalize') + _obj_observer_dealloc(self, 'finalize') + + +def _obj_observer_dealloc(objc_obs, selector_name): + """Removes any cached ObjCInstances in Python to prevent memory leaks. + Manually break association as it's not implicitly mentioned that dealloc would break an association, + although we do not use the object after. + """ + objc_ptr = get_instance_variable(objc_obs, 'observed_object', c_void_p) + if objc_ptr: + objc.objc_setAssociatedObject(objc_ptr, objc_obs, None, OBJC_ASSOCIATION_ASSIGN) + objc_i = ObjCInstance._cached_objects.pop(objc_ptr, None) + if objc_i: + _clear_arp_objects(objc_i) + + send_super(objc_obs, selector_name) + + +def _clear_arp_objects(objc_i: ObjCInstance): + """Cleanup any ObjCInstance's created during an AutoreleasePool creation. + See discussion and investigation thanks to mrJean with leaks regarding pools: + https://github.com/mrJean1/PyCocoa/issues/6 + It was determined that objects in an AutoreleasePool are not guaranteed to call a dealloc, creating memory leaks. + The DeallocObserver relies on this to free memory in the ObjCInstance._cached_objects. + Solution is as follows: + 1) Do not observe any ObjCInstance's with DeallocObserver when non-global autorelease pool is in scope. + 2) Some objects such as ObjCSubclass's must be retained. + 3) When a pool is drained and dealloc'd, clear all ObjCInstances in that pool that are not retained. + """ + if objc_i.objc_class.name == b"NSAutoreleasePool": + pool_id = objc_i.pool + for cobjc_ptr in list(ObjCInstance._cached_objects.keys()): + cobjc_i = ObjCInstance._cached_objects[cobjc_ptr] + if cobjc_i.retained is False and cobjc_i.pool == pool_id: + del ObjCInstance._cached_objects[cobjc_ptr] + + _arp_manager.delete(objc_i) diff --git a/libs/pyglet/libs/win32/winkey.py b/libs/pyglet/libs/win32/winkey.py index a3ee3fe..9205b25 100644 --- a/libs/pyglet/libs/win32/winkey.py +++ b/libs/pyglet/libs/win32/winkey.py @@ -120,14 +120,14 @@ keymap = { VK_F14: key.F14, VK_F15: key.F15, VK_F16: key.F16, - # VK_F17: , - # VK_F18: , - # VK_F19: , - # VK_F20: , - # VK_F21: , - # VK_F22: , - # VK_F23: , - # VK_F24: , + VK_F17: key.F17, + VK_F18: key.F18, + VK_F19: key.F19, + VK_F20: key.F20, + VK_F21: key.F21, + VK_F22: key.F22, + VK_F23: key.F23, + VK_F24: key.F24, VK_NUMLOCK: key.NUMLOCK, VK_SCROLL: key.SCROLLLOCK, VK_LSHIFT: key.LSHIFT, diff --git a/libs/pyglet/sprite.py b/libs/pyglet/sprite.py index 31ad3e2..909ab8c 100644 --- a/libs/pyglet/sprite.py +++ b/libs/pyglet/sprite.py @@ -69,12 +69,14 @@ import sys import pyglet -from pyglet.gl import * from pyglet import clock -from pyglet import event from pyglet import graphics from pyglet import image +from pyglet.gl import * +from pyglet.animation import AnimationController, Animation + + _is_pyglet_doc_run = hasattr(sys, "is_pyglet_doc_run") and sys.is_pyglet_doc_run @@ -233,16 +235,13 @@ class SpriteGroup(graphics.Group): self.blend_src, self.blend_dest)) -class Sprite(event.EventDispatcher): +class Sprite(AnimationController): """Instance of an on-screen image. See the module documentation for usage. """ _batch = None - _animation = None - _frame_index = 0 - _paused = False _rotation = 0 _opacity = 255 _rgb = (255, 255, 255) @@ -263,7 +262,7 @@ class Sprite(event.EventDispatcher): """Create a sprite. :Parameters: - `img` : `~pyglet.image.AbstractImage` or `~pyglet.image.Animation` + `img` : `~pyglet.image.AbstractImage` or `~pyglet.animation.Animation` Image or animation to display. `x` : int X coordinate of the sprite. @@ -290,9 +289,9 @@ class Sprite(event.EventDispatcher): self._z = z self._img = img - if isinstance(img, image.Animation): + if isinstance(img, Animation): self._animation = img - self._texture = img.frames[0].image.get_texture() + self._texture = img.frames[0].data.get_texture() self._next_dt = img.frames[0].duration if self._next_dt: clock.schedule_once(self._animate, self._next_dt) @@ -345,7 +344,7 @@ class Sprite(event.EventDispatcher): return # Deleted in event handler. frame = self._animation.frames[self._frame_index] - self._set_texture(frame.image.get_texture()) + self._set_texture(frame.data.get_texture()) if frame.duration is not None: duration = frame.duration - (self._next_dt - dt) @@ -407,7 +406,7 @@ class Sprite(event.EventDispatcher): """Image or animation to display. :type: :py:class:`~pyglet.image.AbstractImage` or - :py:class:`~pyglet.image.Animation` + :py:class:`~pyglet.animation.Animation` """ if self._animation: return self._animation @@ -419,7 +418,7 @@ class Sprite(event.EventDispatcher): clock.unschedule(self._animate) self._animation = None - if isinstance(img, image.Animation): + if isinstance(img, Animation): self._animation = img self._frame_index = 0 self._set_texture(img.frames[0].image.get_texture()) @@ -730,51 +729,6 @@ class Sprite(event.EventDispatcher): self._visible = visible self._update_position() - @property - def paused(self): - """Pause/resume the Sprite's Animation - - If `Sprite.image` is an Animation, you can pause or resume - the animation by setting this property to True or False. - If not an Animation, this has no effect. - - :type: bool - """ - return self._paused - - @paused.setter - def paused(self, pause): - if not hasattr(self, '_animation') or pause == self._paused: - return - if pause is True: - clock.unschedule(self._animate) - else: - frame = self._animation.frames[self._frame_index] - self._next_dt = frame.duration - if self._next_dt: - clock.schedule_once(self._animate, self._next_dt) - self._paused = pause - - @property - def frame_index(self): - """The current Animation frame. - - If the `Sprite.image` is an `Animation`, - you can query or set the current frame. - If not an Animation, this will always - be 0. - - :type: int - """ - return self._frame_index - - @frame_index.setter - def frame_index(self, index): - # Bound to available number of frames - if self._animation is None: - return - self._frame_index = max(0, min(index, len(self._animation.frames)-1)) - def draw(self): """Draw the sprite at its current position. @@ -797,9 +751,6 @@ class Sprite(event.EventDispatcher): """ -Sprite.register_event_type('on_animation_end') - - class AdvancedSprite(pyglet.sprite.Sprite): """Is a sprite that lets you change the shader program during initialization and after For advanced users who understand shaders.""" @@ -837,7 +788,3 @@ class AdvancedSprite(pyglet.sprite.Sprite): self._group) self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, self._batch) self._program = program - - - - diff --git a/libs/pyglet/window/__init__.py b/libs/pyglet/window/__init__.py index e325d28..e7f8fa7 100644 --- a/libs/pyglet/window/__init__.py +++ b/libs/pyglet/window/__init__.py @@ -1011,7 +1011,7 @@ class BaseWindow(with_metaclass(_WindowMetaclass, EventDispatcher)): Once set, the user will not be able to resize the window smaller than the given dimensions. There is no way to remove the - minimum size constraint on a window (but you could set it to 0,0). + minimum size constraint on a window (but you could set it to 1, 1). The behaviour is undefined if the minimum size is set larger than the current size of the window. diff --git a/libs/pyglet/window/cocoa/__init__.py b/libs/pyglet/window/cocoa/__init__.py index c194e56..6b127ec 100644 --- a/libs/pyglet/window/cocoa/__init__.py +++ b/libs/pyglet/window/cocoa/__init__.py @@ -8,7 +8,7 @@ from pyglet.event import EventDispatcher from pyglet.canvas.cocoa import CocoaCanvas -from pyglet.libs.darwin import cocoapy, CGPoint +from pyglet.libs.darwin import cocoapy, CGPoint, AutoReleasePool from .systemcursor import SystemCursor from .pyglet_delegate import PygletDelegate @@ -17,7 +17,6 @@ from .pyglet_view import PygletView NSApplication = cocoapy.ObjCClass('NSApplication') NSCursor = cocoapy.ObjCClass('NSCursor') -NSAutoreleasePool = cocoapy.ObjCClass('NSAutoreleasePool') NSColor = cocoapy.ObjCClass('NSColor') NSEvent = cocoapy.ObjCClass('NSEvent') NSArray = cocoapy.ObjCClass('NSArray') @@ -42,6 +41,13 @@ class CocoaMouseCursor(MouseCursor): class CocoaWindow(BaseWindow): + def __init__(self, width=None, height=None, caption=None, resizable=False, style=BaseWindow.WINDOW_STYLE_DEFAULT, + fullscreen=False, visible=True, vsync=True, file_drops=False, display=None, screen=None, config=None, + context=None, mode=None): + with AutoReleasePool(): + super().__init__(width, height, caption, resizable, style, fullscreen, visible, vsync, file_drops, display, + screen, config, context, mode) + # NSWindow instance. _nswindow = None @@ -57,14 +63,14 @@ class CocoaWindow(BaseWindow): # NSWindow style masks. _style_masks = { - BaseWindow.WINDOW_STYLE_DEFAULT: cocoapy.NSTitledWindowMask | - cocoapy.NSClosableWindowMask | - cocoapy.NSMiniaturizableWindowMask, - BaseWindow.WINDOW_STYLE_DIALOG: cocoapy.NSTitledWindowMask | - cocoapy.NSClosableWindowMask, - BaseWindow.WINDOW_STYLE_TOOL: cocoapy.NSTitledWindowMask | - cocoapy.NSClosableWindowMask | - cocoapy.NSUtilityWindowMask, + BaseWindow.WINDOW_STYLE_DEFAULT: cocoapy.NSTitledWindowMask | + cocoapy.NSClosableWindowMask | + cocoapy.NSMiniaturizableWindowMask, + BaseWindow.WINDOW_STYLE_DIALOG: cocoapy.NSTitledWindowMask | + cocoapy.NSClosableWindowMask, + BaseWindow.WINDOW_STYLE_TOOL: cocoapy.NSTitledWindowMask | + cocoapy.NSClosableWindowMask | + cocoapy.NSUtilityWindowMask, BaseWindow.WINDOW_STYLE_BORDERLESS: cocoapy.NSBorderlessWindowMask, } @@ -79,108 +85,104 @@ class CocoaWindow(BaseWindow): self._create() def _create(self): - # Create a temporary autorelease pool for this method. - pool = NSAutoreleasePool.alloc().init() + with AutoReleasePool(): + if self._nswindow: + # The window is about the be recreated so destroy everything + # associated with the old window, then destroy the window itself. + nsview = self.canvas.nsview + self.canvas = None + self._nswindow.orderOut_(None) + self._nswindow.close() + self.context.detach() + self._nswindow.release() + self._nswindow = None + nsview.release() + self._delegate.release() + self._delegate = None - if self._nswindow: - # The window is about the be recreated so destroy everything - # associated with the old window, then destroy the window itself. - nsview = self.canvas.nsview - self.canvas = None - self._nswindow.orderOut_(None) - self._nswindow.close() - self.context.detach() - self._nswindow.release() - self._nswindow = None - nsview.release() - self._delegate.release() - self._delegate = None + # Determine window parameters. + content_rect = cocoapy.NSMakeRect(0, 0, self._width, self._height) + WindowClass = PygletWindow + if self._fullscreen: + style_mask = cocoapy.NSBorderlessWindowMask + else: + if self._style not in self._style_masks: + self._style = self.WINDOW_STYLE_DEFAULT + style_mask = self._style_masks[self._style] + if self._resizable: + style_mask |= cocoapy.NSResizableWindowMask + if self._style == BaseWindow.WINDOW_STYLE_TOOL: + WindowClass = PygletToolWindow - # Determine window parameters. - content_rect = cocoapy.NSMakeRect(0, 0, self._width, self._height) - WindowClass = PygletWindow - if self._fullscreen: - style_mask = cocoapy.NSBorderlessWindowMask - else: - if self._style not in self._style_masks: - self._style = self.WINDOW_STYLE_DEFAULT - style_mask = self._style_masks[self._style] - if self._resizable: - style_mask |= cocoapy.NSResizableWindowMask - if self._style == BaseWindow.WINDOW_STYLE_TOOL: - WindowClass = PygletToolWindow + # First create an instance of our NSWindow subclass. - # First create an instance of our NSWindow subclass. + # FIX ME: + # Need to use this initializer to have any hope of multi-monitor support. + # But currently causes problems on Mac OS X Lion. So for now, we initialize the + # window without including screen information. + # + # self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_screen_( + # content_rect, # contentRect + # style_mask, # styleMask + # NSBackingStoreBuffered, # backing + # False, # defer + # self.screen.get_nsscreen()) # screen - # FIX ME: - # Need to use this initializer to have any hope of multi-monitor support. - # But currently causes problems on Mac OS X Lion. So for now, we initialize the - # window without including screen information. - # - # self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_screen_( - # content_rect, # contentRect - # style_mask, # styleMask - # NSBackingStoreBuffered, # backing - # False, # defer - # self.screen.get_nsscreen()) # screen + self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_( + content_rect, # contentRect + style_mask, # styleMask + cocoapy.NSBackingStoreBuffered, # backing + False) # defer - self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_( - content_rect, # contentRect - style_mask, # styleMask - cocoapy.NSBackingStoreBuffered, # backing - False) # defer + if self._fullscreen: + # BUG: I suspect that this doesn't do the right thing when using + # multiple monitors (which would be to go fullscreen on the monitor + # where the window is located). However I've no way to test. + blackColor = NSColor.blackColor() + self._nswindow.setBackgroundColor_(blackColor) + self._nswindow.setOpaque_(True) + self.screen.capture_display() + self._nswindow.setLevel_(quartz.CGShieldingWindowLevel()) + self.context.set_full_screen() + self._center_window() + self._mouse_in_window = True + else: + self._set_nice_window_location() + self._mouse_in_window = self._mouse_in_content_rect() - if self._fullscreen: - # BUG: I suspect that this doesn't do the right thing when using - # multiple monitors (which would be to go fullscreen on the monitor - # where the window is located). However I've no way to test. - blackColor = NSColor.blackColor() - self._nswindow.setBackgroundColor_(blackColor) - self._nswindow.setOpaque_(True) - self.screen.capture_display() - self._nswindow.setLevel_(quartz.CGShieldingWindowLevel()) - self.context.set_full_screen() - self._center_window() - self._mouse_in_window = True - else: - self._set_nice_window_location() - self._mouse_in_window = self._mouse_in_content_rect() + # Then create a view and set it as our NSWindow's content view. + self._nsview = PygletView.alloc().initWithFrame_cocoaWindow_(content_rect, self) + self._nswindow.setContentView_(self._nsview) + self._nswindow.makeFirstResponder_(self._nsview) - # Then create a view and set it as our NSWindow's content view. - self._nsview = PygletView.alloc().initWithFrame_cocoaWindow_(content_rect, self) - self._nswindow.setContentView_(self._nsview) - self._nswindow.makeFirstResponder_(self._nsview) + # Create a canvas with the view as its drawable and attach context to it. + self.canvas = CocoaCanvas(self.display, self.screen, self._nsview) + self.context.attach(self.canvas) - # Create a canvas with the view as its drawable and attach context to it. - self.canvas = CocoaCanvas(self.display, self.screen, self._nsview) - self.context.attach(self.canvas) + # Configure the window. + self._nswindow.setAcceptsMouseMovedEvents_(True) + self._nswindow.setReleasedWhenClosed_(False) + self._nswindow.useOptimizedDrawing_(True) + self._nswindow.setPreservesContentDuringLiveResize_(False) - # Configure the window. - self._nswindow.setAcceptsMouseMovedEvents_(True) - self._nswindow.setReleasedWhenClosed_(False) - self._nswindow.useOptimizedDrawing_(True) - self._nswindow.setPreservesContentDuringLiveResize_(False) + # Set the delegate. + self._delegate = PygletDelegate.alloc().initWithWindow_(self) - # Set the delegate. - self._delegate = PygletDelegate.alloc().initWithWindow_(self) + # Configure CocoaWindow. + self.set_caption(self._caption) + if self._minimum_size is not None: + self.set_minimum_size(*self._minimum_size) + if self._maximum_size is not None: + self.set_maximum_size(*self._maximum_size) - # Configure CocoaWindow. - self.set_caption(self._caption) - if self._minimum_size is not None: - self.set_minimum_size(*self._minimum_size) - if self._maximum_size is not None: - self.set_maximum_size(*self._maximum_size) + if self._file_drops: + array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeURL) + self._nsview.registerForDraggedTypes_(array) - if self._file_drops: - array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeURL) - self._nsview.registerForDraggedTypes_(array) - - self.context.update_geometry() - self.switch_to() - self.set_vsync(self._vsync) - self.set_visible(self._visible) - - pool.drain() + self.context.update_geometry() + self.switch_to() + self.set_vsync(self._vsync) + self.set_visible(self._visible) def _set_nice_window_location(self): # Construct a list of all visible windows that aren't us. @@ -208,42 +210,39 @@ class CocoaWindow(BaseWindow): if self._was_closed: return - # Create a temporary autorelease pool for this method. - pool = NSAutoreleasePool.new() + with AutoReleasePool(): + # Restore cursor visibility + self.set_mouse_platform_visible(True) + self.set_exclusive_mouse(False) + self.set_exclusive_keyboard(False) - # Restore cursor visibility - self.set_mouse_platform_visible(True) - self.set_exclusive_mouse(False) - self.set_exclusive_keyboard(False) + # Remove the delegate object + if self._delegate: + self._nswindow.setDelegate_(None) + self._delegate.release() + self._delegate = None - # Remove the delegate object - if self._delegate: - self._nswindow.setDelegate_(None) - self._delegate.release() - self._delegate = None + # Remove window from display and remove its view. + if self._nswindow: + self._nswindow.orderOut_(None) + self._nswindow.setContentView_(None) + self._nswindow.close() - # Remove window from display and remove its view. - if self._nswindow: - self._nswindow.orderOut_(None) - self._nswindow.setContentView_(None) - self._nswindow.close() + # Restore screen mode. This also releases the display + # if it was captured for fullscreen mode. + self.screen.restore_mode() - # Restore screen mode. This also releases the display - # if it was captured for fullscreen mode. - self.screen.restore_mode() + # Remove view from canvas and then remove canvas. + if self.canvas: + self.canvas.nsview.release() + self.canvas.nsview = None + self.canvas = None - # Remove view from canvas and then remove canvas. - if self.canvas: - self.canvas.nsview.release() - self.canvas.nsview = None - self.canvas = None + # Do this last, so that we don't see white flash + # when exiting application from fullscreen mode. + super(CocoaWindow, self).close() - # Do this last, so that we don't see white flash - # when exiting application from fullscreen mode. - super(CocoaWindow, self).close() - - self._was_closed = True - pool.drain() + self._was_closed = True def switch_to(self): if self.context: @@ -261,26 +260,24 @@ class CocoaWindow(BaseWindow): event = True # Dequeue and process all of the pending Cocoa events. - pool = NSAutoreleasePool.new() - NSApp = NSApplication.sharedApplication() - while event and self._nswindow and self._context: - event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_( - cocoapy.NSAnyEventMask, None, cocoapy.NSEventTrackingRunLoopMode, True) + with AutoReleasePool(): + NSApp = NSApplication.sharedApplication() + while event and self._nswindow and self._context: + event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_( + cocoapy.NSAnyEventMask, None, cocoapy.NSEventTrackingRunLoopMode, True) - if event: - event_type = event.type() - # Pass on all events. - NSApp.sendEvent_(event) - # And resend key events to special handlers. - if event_type == cocoapy.NSKeyDown and not event.isARepeat(): - NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyDown:'), None, event) - elif event_type == cocoapy.NSKeyUp: - NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyUp:'), None, event) - elif event_type == cocoapy.NSFlagsChanged: - NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event) - NSApp.updateWindows() - - pool.drain() + if event: + event_type = event.type() + # Pass on all events. + NSApp.sendEvent_(event) + # And resend key events to special handlers. + if event_type == cocoapy.NSKeyDown and not event.isARepeat(): + NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyDown:'), None, event) + elif event_type == cocoapy.NSKeyUp: + NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyUp:'), None, event) + elif event_type == cocoapy.NSFlagsChanged: + NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event) + NSApp.updateWindows() self._allow_dispatch_event = False @@ -486,25 +483,25 @@ class CocoaWindow(BaseWindow): if name == self.CURSOR_DEFAULT: return DefaultMouseCursor() cursors = { - self.CURSOR_CROSSHAIR: 'crosshairCursor', - self.CURSOR_HAND: 'pointingHandCursor', - self.CURSOR_HELP: 'arrowCursor', - self.CURSOR_NO: 'operationNotAllowedCursor', # Mac OS 10.6 - self.CURSOR_SIZE: 'arrowCursor', - self.CURSOR_SIZE_UP: 'resizeUpCursor', - self.CURSOR_SIZE_UP_RIGHT: 'arrowCursor', - self.CURSOR_SIZE_RIGHT: 'resizeRightCursor', + self.CURSOR_CROSSHAIR: 'crosshairCursor', + self.CURSOR_HAND: 'pointingHandCursor', + self.CURSOR_HELP: 'arrowCursor', + self.CURSOR_NO: 'operationNotAllowedCursor', # Mac OS 10.6 + self.CURSOR_SIZE: 'arrowCursor', + self.CURSOR_SIZE_UP: 'resizeUpCursor', + self.CURSOR_SIZE_UP_RIGHT: 'arrowCursor', + self.CURSOR_SIZE_RIGHT: 'resizeRightCursor', self.CURSOR_SIZE_DOWN_RIGHT: 'arrowCursor', - self.CURSOR_SIZE_DOWN: 'resizeDownCursor', - self.CURSOR_SIZE_DOWN_LEFT: 'arrowCursor', - self.CURSOR_SIZE_LEFT: 'resizeLeftCursor', - self.CURSOR_SIZE_UP_LEFT: 'arrowCursor', - self.CURSOR_SIZE_UP_DOWN: 'resizeUpDownCursor', + self.CURSOR_SIZE_DOWN: 'resizeDownCursor', + self.CURSOR_SIZE_DOWN_LEFT: 'arrowCursor', + self.CURSOR_SIZE_LEFT: 'resizeLeftCursor', + self.CURSOR_SIZE_UP_LEFT: 'arrowCursor', + self.CURSOR_SIZE_UP_DOWN: 'resizeUpDownCursor', self.CURSOR_SIZE_LEFT_RIGHT: 'resizeLeftRightCursor', - self.CURSOR_TEXT: 'IBeamCursor', - self.CURSOR_WAIT: 'arrowCursor', # No wristwatch cursor in Cocoa - self.CURSOR_WAIT_ARROW: 'arrowCursor', # No wristwatch cursor in Cocoa - } + self.CURSOR_TEXT: 'IBeamCursor', + self.CURSOR_WAIT: 'arrowCursor', # No wristwatch cursor in Cocoa + self.CURSOR_WAIT_ARROW: 'arrowCursor', # No wristwatch cursor in Cocoa + } if name not in cursors: raise RuntimeError('Unknown cursor name "%s"' % name) return CocoaMouseCursor(cursors[name]) @@ -514,7 +511,7 @@ class CocoaWindow(BaseWindow): # If absolute, then x, y is given in global display coordinates # which sets (0,0) at top left corner of main display. It is possible # to warp the mouse position to a point inside of another display. - quartz.CGWarpMouseCursorPosition(CGPoint(x,y)) + quartz.CGWarpMouseCursorPosition(CGPoint(x, y)) else: # Window-relative coordinates: (x, y) are given in window coords # with (0,0) at bottom-left corner of window and y up. We find @@ -538,7 +535,7 @@ class CocoaWindow(BaseWindow): # Move mouse to center of window. frame = self._nswindow.frame() width, height = frame.size.width, frame.size.height - self.set_mouse_position(width/2, height/2) + self.set_mouse_position(width / 2, height / 2) quartz.CGAssociateMouseAndMouseCursorPosition(False) else: quartz.CGAssociateMouseAndMouseCursorPosition(True) diff --git a/libs/pyglet/window/key.py b/libs/pyglet/window/key.py index bde76d4..570e1f8 100644 --- a/libs/pyglet/window/key.py +++ b/libs/pyglet/window/key.py @@ -293,7 +293,10 @@ F17 = 0xffce F18 = 0xffcf F19 = 0xffd0 F20 = 0xffd1 - +F21 = 0xffd2 +F22 = 0xffd3 +F23 = 0xffd4 +F24 = 0xffd5 # Modifiers LSHIFT = 0xffe1 RSHIFT = 0xffe2