这是什么? 新的pyglet feature branch? merge一下!

This commit is contained in:
shenjack 2023-02-17 21:55:43 +08:00
parent 2f99b527d0
commit 3c341103ad
16 changed files with 535 additions and 573 deletions

View File

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

142
libs/pyglet/animation.py Normal file
View File

@ -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})"

View File

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

View File

@ -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.
"""
def __init__(self, order=0, parent=None):
"""Create a Group.
`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.
:Ivariables:
: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
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):
self._order = order
self.parent = parent
self._visible = True

View File

@ -645,19 +645,16 @@ class ShaderSource:
class Shader:
"""OpenGL Shader object"""
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'.
"""OpenGL shader.
Shader objects are compiled on instantiation.
You can reuse a Shader object in multiple `ShaderProgram`s.
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):
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`

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
@ -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 '<ObjCBoundMethod %s (%s)>' % (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)

View File

@ -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,

View File

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

View File

@ -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.

View File

@ -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
@ -79,9 +85,7 @@ 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.
@ -180,8 +184,6 @@ class CocoaWindow(BaseWindow):
self.set_vsync(self._vsync)
self.set_visible(self._visible)
pool.drain()
def _set_nice_window_location(self):
# Construct a list of all visible windows that aren't us.
visible_windows = [win for win in pyglet.app.windows if
@ -208,9 +210,7 @@ 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)
@ -243,7 +243,6 @@ class CocoaWindow(BaseWindow):
super(CocoaWindow, self).close()
self._was_closed = True
pool.drain()
def switch_to(self):
if self.context:
@ -261,7 +260,7 @@ class CocoaWindow(BaseWindow):
event = True
# Dequeue and process all of the pending Cocoa events.
pool = NSAutoreleasePool.new()
with AutoReleasePool():
NSApp = NSApplication.sharedApplication()
while event and self._nswindow and self._context:
event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_(
@ -280,8 +279,6 @@ class CocoaWindow(BaseWindow):
NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event)
NSApp.updateWindows()
pool.drain()
self._allow_dispatch_event = False
def dispatch_pending_events(self):
@ -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)

View File

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