这是什么? 新的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 # Lazily load all modules, except if performing
# type checking or code inspection. # type checking or code inspection.
if TYPE_CHECKING: if TYPE_CHECKING:
from . import animation
from . import app from . import app
from . import canvas from . import canvas
from . import clock from . import clock
@ -349,6 +350,7 @@ if TYPE_CHECKING:
from . import text from . import text
from . import window from . import window
else: else:
animation = _ModuleProxy('animation')
app = _ModuleProxy('app') app = _ModuleProxy('app')
canvas = _ModuleProxy('canvas') canvas = _ModuleProxy('canvas')
clock = _ModuleProxy('clock') 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.app.base import PlatformEventLoop
from pyglet.libs.darwin import cocoapy from pyglet.libs.darwin import cocoapy, AutoReleasePool
NSApplication = cocoapy.ObjCClass('NSApplication') NSApplication = cocoapy.ObjCClass('NSApplication')
NSMenu = cocoapy.ObjCClass('NSMenu') NSMenu = cocoapy.ObjCClass('NSMenu')
NSMenuItem = cocoapy.ObjCClass('NSMenuItem') NSMenuItem = cocoapy.ObjCClass('NSMenuItem')
NSAutoreleasePool = cocoapy.ObjCClass('NSAutoreleasePool')
NSDate = cocoapy.ObjCClass('NSDate') NSDate = cocoapy.ObjCClass('NSDate')
NSDate.dateWithTimeIntervalSinceNow_.no_cached_return()
NSDate.distantFuture.no_cached_return()
NSEvent = cocoapy.ObjCClass('NSEvent') NSEvent = cocoapy.ObjCClass('NSEvent')
NSUserDefaults = cocoapy.ObjCClass('NSUserDefaults') 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): def add_menu_item(menu, title, action, key):
@ -33,8 +21,6 @@ def add_menu_item(menu, title, action, key):
menu.addItem_(menuItem) menu.addItem_(menuItem)
# cleanup # cleanup
title.release()
key.release()
menuItem.release() menuItem.release()

View File

@ -5,53 +5,6 @@ Shaders and Buffers. It also provides classes for highly performant batched
rendering and grouping. rendering and grouping.
See the :ref:`guide_graphics` for details on how to use this graphics API. 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 import ctypes
@ -255,15 +208,31 @@ def get_default_shader():
class Batch: 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 Many drawable pyglet objects accept an optional `Batch` argument in their
`add` and `add_indexed` methods. An optional group can be specified along constructors. By giving a `Batch` to multiple objects, you can tell pyglet
with the vertex list, which gives the OpenGL state required for its rendering. that you expect to draw all of these objects at once, so it can optimise its
Vertex lists with shared mode and group are allocated into adjacent areas of use of OpenGL. Hence, drawing a `Batch` is often much faster than drawing
memory and sent to the graphics card in a single operation. 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): def __init__(self):
@ -325,6 +294,7 @@ class Batch:
vertex_list.migrate(domain) vertex_list.migrate(domain)
def get_domain(self, indexed, mode, group, program, attributes): def get_domain(self, indexed, mode, group, program, attributes):
"""Get, or create, the vertex domain corresponding to the given arguments."""
if group is None: if group is None:
group = ShaderGroup(program=program) group = ShaderGroup(program=program)
@ -496,27 +466,43 @@ class Batch:
class Group: class Group:
"""Group of common OpenGL state. """Group of common OpenGL state.
Before a VertexList is rendered, its Group's OpenGL state is set. `Group` provides extra control over how drawables are handled within a
This includes binding textures, shaders, or setting any other parameters. `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): 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._order = order
self.parent = parent self.parent = parent
self._visible = True self._visible = True

View File

@ -645,19 +645,16 @@ class ShaderSource:
class Shader: 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): 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._id = None
self.type = shader_type self.type = shader_type
@ -735,12 +732,11 @@ class Shader:
class ShaderProgram: class ShaderProgram:
"""OpenGL Shader Program""" """OpenGL shader program."""
__slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__' __slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__'
def __init__(self, *shaders: Shader): def __init__(self, *shaders: Shader):
"""Create an OpenGL ShaderProgram from one or more Shader objects."""
assert shaders, "At least one Shader object is required." assert shaders, "At least one Shader object is required."
self._id = _link_program(*shaders) self._id = _link_program(*shaders)
self._context = pyglet.gl.current_context self._context = pyglet.gl.current_context
@ -825,6 +821,7 @@ class ShaderProgram:
`mode` : int `mode` : int
OpenGL drawing mode enumeration; for example, one of OpenGL drawing mode enumeration; for example, one of
``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc.
This determines how the list is drawn in the given batch.
`batch` : `~pyglet.graphics.Batch` `batch` : `~pyglet.graphics.Batch`
Batch to add the VertexList to, or ``None`` if a Batch will not be used. Batch to add the VertexList to, or ``None`` if a Batch will not be used.
Using a Batch is strongly recommended. Using a Batch is strongly recommended.
@ -867,6 +864,7 @@ class ShaderProgram:
`mode` : int `mode` : int
OpenGL drawing mode enumeration; for example, one of OpenGL drawing mode enumeration; for example, one of
``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc. ``GL_POINTS``, ``GL_LINES``, ``GL_TRIANGLES``, etc.
This determines how the list is drawn in the given batch.
`indices` : sequence of int `indices` : sequence of int
Sequence of integers giving indices into the vertex list. Sequence of integers giving indices into the vertex list.
`batch` : `~pyglet.graphics.Batch` `batch` : `~pyglet.graphics.Batch`

View File

@ -97,19 +97,17 @@ import re
import weakref import weakref
from ctypes import * from ctypes import *
from io import open, BytesIO from io import open
import pyglet import pyglet
from pyglet.gl import *
from pyglet.gl import gl_info
from pyglet.util import asbytes 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 registry as _codec_registry
from .codecs import add_default_codecs as _add_default_codecs from .codecs import add_default_codecs as _add_default_codecs
from .codecs import ImageEncodeException, ImageDecodeException
from .animation import Animation, AnimationFrame
from .buffer import * from .buffer import *
from . import atlas from . import atlas
@ -449,7 +447,7 @@ class AbstractImageSequence:
.. versionadded:: 1.1 .. versionadded:: 1.1
""" """
return Animation.from_image_sequence(self, period, loop) return Animation.from_sequence(self, period, loop)
def __getitem__(self, slice): def __getitem__(self, slice):
"""Retrieve a (list of) image. """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 import *
from pyglet.image.codecs import * from pyglet.image.codecs import *
from pyglet.image.codecs import gif from pyglet.image.codecs import gif
from pyglet.animation import AnimationFrame
import pyglet.lib import pyglet.lib
import pyglet.window import pyglet.window

View File

@ -35,3 +35,15 @@ from .runtime import ObjCClass, ObjCInstance, ObjCSubclass
from .cocoatypes import * from .cocoatypes import *
from .cocoalibs 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 # Core Foundation type to Python type conversion functions
def CFSTR(string): def CFSTR(string):
return ObjCInstance(c_void_p(cf.CFStringCreateWithCString( return cf.CFStringCreateWithCString(None, string.encode('utf8'), kCFStringEncodingUTF8)
None, string.encode('utf8'), kCFStringEncodingUTF8)))
# Other possible names for this method: # Other possible names for this method:
# at, ampersat, arobe, apenstaartje (little monkey tail), strudel, # at, ampersat, arobe, apenstaartje (little monkey tail), strudel,
@ -57,7 +56,7 @@ def CFSTR(string):
# kukac (caterpillar). # kukac (caterpillar).
def get_NSString(string): def get_NSString(string):
"""Autoreleased version of CFSTR""" """Autoreleased version of CFSTR"""
return CFSTR(string).autorelease() return ObjCInstance(c_void_p(CFSTR(string))).autorelease()
def cfstring_to_string(cfstring): def cfstring_to_string(cfstring):
length = cf.CFStringGetLength(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.restype = c_void_p
objc.sel_registerName.argtypes = [c_char_p] objc.sel_registerName.argtypes = [c_char_p]
###################################################################### ######################################################################
# Constants # Constants
OBJC_ASSOCIATION_ASSIGN = 0 # Weak reference to the associated object. 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 # change these values. restype should be a ctypes type
# and argtypes should be a list of ctypes types for # and argtypes should be a list of ctypes types for
# the arguments of the message only. # 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): def send_message(receiver, selName, *args, **kwargs):
if isinstance(receiver, str): if isinstance(receiver, str):
receiver = get_class(receiver) receiver = get_class(receiver)
@ -624,11 +625,11 @@ def cfunctype_for_encoding(encoding):
return cfunctype_table[encoding] return cfunctype_table[encoding]
# Otherwise, create a new CFUNCTYPE for the 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, 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'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'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, b'@': c_void_p, b'#': c_void_p, b':': c_void_p, NSPointEncoding: NSPoint,
NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange, NSSizeEncoding: NSSize, NSRectEncoding: NSRect, NSRangeEncoding: NSRange,
PyObjectEncoding: py_object} PyObjectEncoding: py_object}
argtypes = [] argtypes = []
for code in parse_type_encoding(encoding): 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 # 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 # 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. # 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, 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'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'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, 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, NSPointEncoding: NSPoint, NSSizeEncoding: NSSize, NSRectEncoding: NSRect,
NSRangeEncoding: NSRange, NSRangeEncoding: NSRange,
PyObjectEncoding: py_object} PyObjectEncoding: py_object}
cfunctype_table = {} cfunctype_table = {}
@ -717,7 +718,6 @@ class ObjCMethod:
def __init__(self, method): def __init__(self, method):
"""Initialize with an Objective-C Method pointer. We then determine """Initialize with an Objective-C Method pointer. We then determine
the return type and argument type information of the method.""" the return type and argument type information of the method."""
self.cache = True
self.selector = c_void_p(objc.method_getName(method)) self.selector = c_void_p(objc.method_getName(method))
self.name = objc.sel_getName(self.selector) self.name = objc.sel_getName(self.selector)
self.pyname = self.name.replace(b':', b'_') self.pyname = self.name.replace(b':', b'_')
@ -734,7 +734,7 @@ class ObjCMethod:
try: try:
self.argtypes = [self.ctype_for_encoding(t) for t in self.argument_types] self.argtypes = [self.ctype_for_encoding(t) for t in self.argument_types]
except: 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 self.argtypes = None
# Get types for the return type. # Get types for the return type.
try: try:
@ -745,7 +745,7 @@ class ObjCMethod:
else: else:
self.restype = self.ctype_for_encoding(self.return_type) self.restype = self.ctype_for_encoding(self.return_type)
except: 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.restype = None
self.func = None self.func = None
@ -803,7 +803,7 @@ class ObjCMethod:
result = f(objc_id, self.selector, *args) result = f(objc_id, self.selector, *args)
# Convert result to python type if it is a instance or class pointer. # Convert result to python type if it is a instance or class pointer.
if self.restype == ObjCInstance: if self.restype == ObjCInstance:
result = ObjCInstance(result, self.cache) result = ObjCInstance(result)
elif self.restype == ObjCClass: elif self.restype == ObjCClass:
result = ObjCClass(result) result = ObjCClass(result)
return result return result
@ -826,13 +826,6 @@ class ObjCBoundMethod:
self.method = method self.method = method
self.objc_id = objc_id 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): def __repr__(self):
return '<ObjCBoundMethod %s (%s)>' % (self.method.name, self.objc_id) 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: class ObjCInstance:
"""Python wrapper for an Objective-C instance.""" """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 = {} _cached_objects = {}
@ -1004,17 +1038,16 @@ class ObjCInstance:
if cache: if cache:
cls._cached_objects[object_ptr.value] = objc_instance cls._cached_objects[object_ptr.value] = objc_instance
# Create a DeallocationObserver and associate it with this object. # Creation of NSAutoreleasePool instance does not technically mean it was allocated and initialized, but
# When the Objective-C object is deallocated, the observer will remove # it's standard practice, so this should not be an issue.
# the ObjCInstance corresponding to the object from the cached objects if objc_instance.objc_class.name == b"NSAutoreleasePool":
# dictionary, effectively destroying the ObjCInstance. _arp_manager.create(objc_instance)
observer = send_message(send_message('DeallocationObserver', 'alloc'), 'initWithObject:', objc_instance) objc_instance.pool = _arp_manager.current
objc.objc_setAssociatedObject(objc_instance, observer, observer, OBJC_ASSOCIATION_RETAIN) _set_dealloc_observer(object_ptr)
elif _arp_manager.current:
# The observer is retained by the object we associate it to. We release objc_instance.pool = _arp_manager.current
# the observer now so that it will be deallocated when the associated else:
# object is deallocated. _set_dealloc_observer(object_ptr)
send_message(observer, 'release')
return objc_instance return objc_instance
@ -1048,12 +1081,16 @@ class ObjCInstance:
# Otherwise raise an exception. # Otherwise raise an exception.
raise AttributeError('ObjCInstance %s has no attribute %s' % (self.objc_class.name, name)) raise AttributeError('ObjCInstance %s has no attribute %s' % (self.objc_class.name, name))
def get_cached_instances(): def get_cached_instances():
"""For debug purposes, return a list of instance names. """For debug purposes, return a list of instance names.
Useful for debugging if an object is leaking.""" Useful for debugging if an object is leaking."""
return [obj.objc_class.name for obj in ObjCInstance._cached_objects.values()] return [obj.objc_class.name for obj in ObjCInstance._cached_objects.values()]
###################################################################### ######################################################################
def convert_method_arguments(encoding, args): def convert_method_arguments(encoding, args):
"""Used by ObjCSubclass to convert Objective-C method arguments to """Used by ObjCSubclass to convert Objective-C method arguments to
Python values before passing them on to the Python-defined method.""" Python values before passing them on to the Python-defined method."""
@ -1183,8 +1220,9 @@ class ObjCSubclass:
def decorator(f): def decorator(f):
def objc_method(objc_self, objc_cmd, *args): 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.objc_cmd = objc_cmd
py_self.retained = True
args = convert_method_arguments(encoding, args) args = convert_method_arguments(encoding, args)
result = f(py_self, *args) result = f(py_self, *args)
if isinstance(result, ObjCClass): if isinstance(result, ObjCClass):
@ -1246,17 +1284,15 @@ class DeallocationObserver_Implementation:
DeallocationObserver.register() DeallocationObserver.register()
@DeallocationObserver.rawmethod('@@') @DeallocationObserver.rawmethod('@@')
def initWithObject_(self, cmd, anObject): def initWithObject_(self, cmd, objc_ptr):
self = send_super(self, 'init') self = send_super(self, 'init')
self = self.value 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 return self
@DeallocationObserver.rawmethod('v') @DeallocationObserver.rawmethod('v')
def dealloc(self, cmd): def dealloc(self, cmd):
anObject = get_instance_variable(self, 'observed_object', c_void_p) _obj_observer_dealloc(self, 'dealloc')
ObjCInstance._cached_objects.pop(anObject, None)
send_super(self, 'dealloc')
@DeallocationObserver.rawmethod('v') @DeallocationObserver.rawmethod('v')
def finalize(self, cmd): def finalize(self, cmd):
@ -1264,6 +1300,40 @@ class DeallocationObserver_Implementation:
# (which would have to be explicitly started with # (which would have to be explicitly started with
# objc_startCollectorThread(), so probably not too much reason # objc_startCollectorThread(), so probably not too much reason
# to have this here, but I guess it can't hurt.) # to have this here, but I guess it can't hurt.)
anObject = get_instance_variable(self, 'observed_object', c_void_p) _obj_observer_dealloc(self, 'finalize')
ObjCInstance._cached_objects.pop(anObject, None)
send_super(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_F14: key.F14,
VK_F15: key.F15, VK_F15: key.F15,
VK_F16: key.F16, VK_F16: key.F16,
# VK_F17: , VK_F17: key.F17,
# VK_F18: , VK_F18: key.F18,
# VK_F19: , VK_F19: key.F19,
# VK_F20: , VK_F20: key.F20,
# VK_F21: , VK_F21: key.F21,
# VK_F22: , VK_F22: key.F22,
# VK_F23: , VK_F23: key.F23,
# VK_F24: , VK_F24: key.F24,
VK_NUMLOCK: key.NUMLOCK, VK_NUMLOCK: key.NUMLOCK,
VK_SCROLL: key.SCROLLLOCK, VK_SCROLL: key.SCROLLLOCK,
VK_LSHIFT: key.LSHIFT, VK_LSHIFT: key.LSHIFT,

View File

@ -69,12 +69,14 @@ import sys
import pyglet import pyglet
from pyglet.gl import *
from pyglet import clock from pyglet import clock
from pyglet import event
from pyglet import graphics from pyglet import graphics
from pyglet import image 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 _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)) self.blend_src, self.blend_dest))
class Sprite(event.EventDispatcher): class Sprite(AnimationController):
"""Instance of an on-screen image. """Instance of an on-screen image.
See the module documentation for usage. See the module documentation for usage.
""" """
_batch = None _batch = None
_animation = None
_frame_index = 0
_paused = False
_rotation = 0 _rotation = 0
_opacity = 255 _opacity = 255
_rgb = (255, 255, 255) _rgb = (255, 255, 255)
@ -263,7 +262,7 @@ class Sprite(event.EventDispatcher):
"""Create a sprite. """Create a sprite.
:Parameters: :Parameters:
`img` : `~pyglet.image.AbstractImage` or `~pyglet.image.Animation` `img` : `~pyglet.image.AbstractImage` or `~pyglet.animation.Animation`
Image or animation to display. Image or animation to display.
`x` : int `x` : int
X coordinate of the sprite. X coordinate of the sprite.
@ -290,9 +289,9 @@ class Sprite(event.EventDispatcher):
self._z = z self._z = z
self._img = img self._img = img
if isinstance(img, image.Animation): if isinstance(img, Animation):
self._animation = img 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 self._next_dt = img.frames[0].duration
if self._next_dt: if self._next_dt:
clock.schedule_once(self._animate, self._next_dt) clock.schedule_once(self._animate, self._next_dt)
@ -345,7 +344,7 @@ class Sprite(event.EventDispatcher):
return # Deleted in event handler. return # Deleted in event handler.
frame = self._animation.frames[self._frame_index] 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: if frame.duration is not None:
duration = frame.duration - (self._next_dt - dt) duration = frame.duration - (self._next_dt - dt)
@ -407,7 +406,7 @@ class Sprite(event.EventDispatcher):
"""Image or animation to display. """Image or animation to display.
:type: :py:class:`~pyglet.image.AbstractImage` or :type: :py:class:`~pyglet.image.AbstractImage` or
:py:class:`~pyglet.image.Animation` :py:class:`~pyglet.animation.Animation`
""" """
if self._animation: if self._animation:
return self._animation return self._animation
@ -419,7 +418,7 @@ class Sprite(event.EventDispatcher):
clock.unschedule(self._animate) clock.unschedule(self._animate)
self._animation = None self._animation = None
if isinstance(img, image.Animation): if isinstance(img, Animation):
self._animation = img self._animation = img
self._frame_index = 0 self._frame_index = 0
self._set_texture(img.frames[0].image.get_texture()) self._set_texture(img.frames[0].image.get_texture())
@ -730,51 +729,6 @@ class Sprite(event.EventDispatcher):
self._visible = visible self._visible = visible
self._update_position() 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): def draw(self):
"""Draw the sprite at its current position. """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): class AdvancedSprite(pyglet.sprite.Sprite):
"""Is a sprite that lets you change the shader program during initialization and after """Is a sprite that lets you change the shader program during initialization and after
For advanced users who understand shaders.""" For advanced users who understand shaders."""
@ -837,7 +788,3 @@ class AdvancedSprite(pyglet.sprite.Sprite):
self._group) self._group)
self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, self._batch) self._batch.migrate(self._vertex_list, GL_TRIANGLES, self._group, self._batch)
self._program = program 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 Once set, the user will not be able to resize the window smaller
than the given dimensions. There is no way to remove the 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 behaviour is undefined if the minimum size is set larger than
the current size of the window. 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.canvas.cocoa import CocoaCanvas
from pyglet.libs.darwin import cocoapy, CGPoint from pyglet.libs.darwin import cocoapy, CGPoint, AutoReleasePool
from .systemcursor import SystemCursor from .systemcursor import SystemCursor
from .pyglet_delegate import PygletDelegate from .pyglet_delegate import PygletDelegate
@ -17,7 +17,6 @@ from .pyglet_view import PygletView
NSApplication = cocoapy.ObjCClass('NSApplication') NSApplication = cocoapy.ObjCClass('NSApplication')
NSCursor = cocoapy.ObjCClass('NSCursor') NSCursor = cocoapy.ObjCClass('NSCursor')
NSAutoreleasePool = cocoapy.ObjCClass('NSAutoreleasePool')
NSColor = cocoapy.ObjCClass('NSColor') NSColor = cocoapy.ObjCClass('NSColor')
NSEvent = cocoapy.ObjCClass('NSEvent') NSEvent = cocoapy.ObjCClass('NSEvent')
NSArray = cocoapy.ObjCClass('NSArray') NSArray = cocoapy.ObjCClass('NSArray')
@ -42,6 +41,13 @@ class CocoaMouseCursor(MouseCursor):
class CocoaWindow(BaseWindow): 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 instance.
_nswindow = None _nswindow = None
@ -57,14 +63,14 @@ class CocoaWindow(BaseWindow):
# NSWindow style masks. # NSWindow style masks.
_style_masks = { _style_masks = {
BaseWindow.WINDOW_STYLE_DEFAULT: cocoapy.NSTitledWindowMask | BaseWindow.WINDOW_STYLE_DEFAULT: cocoapy.NSTitledWindowMask |
cocoapy.NSClosableWindowMask | cocoapy.NSClosableWindowMask |
cocoapy.NSMiniaturizableWindowMask, cocoapy.NSMiniaturizableWindowMask,
BaseWindow.WINDOW_STYLE_DIALOG: cocoapy.NSTitledWindowMask | BaseWindow.WINDOW_STYLE_DIALOG: cocoapy.NSTitledWindowMask |
cocoapy.NSClosableWindowMask, cocoapy.NSClosableWindowMask,
BaseWindow.WINDOW_STYLE_TOOL: cocoapy.NSTitledWindowMask | BaseWindow.WINDOW_STYLE_TOOL: cocoapy.NSTitledWindowMask |
cocoapy.NSClosableWindowMask | cocoapy.NSClosableWindowMask |
cocoapy.NSUtilityWindowMask, cocoapy.NSUtilityWindowMask,
BaseWindow.WINDOW_STYLE_BORDERLESS: cocoapy.NSBorderlessWindowMask, BaseWindow.WINDOW_STYLE_BORDERLESS: cocoapy.NSBorderlessWindowMask,
} }
@ -79,108 +85,104 @@ class CocoaWindow(BaseWindow):
self._create() self._create()
def _create(self): def _create(self):
# Create a temporary autorelease pool for this method. with AutoReleasePool():
pool = NSAutoreleasePool.alloc().init() 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: # Determine window parameters.
# The window is about the be recreated so destroy everything content_rect = cocoapy.NSMakeRect(0, 0, self._width, self._height)
# associated with the old window, then destroy the window itself. WindowClass = PygletWindow
nsview = self.canvas.nsview if self._fullscreen:
self.canvas = None style_mask = cocoapy.NSBorderlessWindowMask
self._nswindow.orderOut_(None) else:
self._nswindow.close() if self._style not in self._style_masks:
self.context.detach() self._style = self.WINDOW_STYLE_DEFAULT
self._nswindow.release() style_mask = self._style_masks[self._style]
self._nswindow = None if self._resizable:
nsview.release() style_mask |= cocoapy.NSResizableWindowMask
self._delegate.release() if self._style == BaseWindow.WINDOW_STYLE_TOOL:
self._delegate = None WindowClass = PygletToolWindow
# Determine window parameters. # First create an instance of our NSWindow subclass.
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. # 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: self._nswindow = WindowClass.alloc().initWithContentRect_styleMask_backing_defer_(
# Need to use this initializer to have any hope of multi-monitor support. content_rect, # contentRect
# But currently causes problems on Mac OS X Lion. So for now, we initialize the style_mask, # styleMask
# window without including screen information. cocoapy.NSBackingStoreBuffered, # backing
# False) # defer
# 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_( if self._fullscreen:
content_rect, # contentRect # BUG: I suspect that this doesn't do the right thing when using
style_mask, # styleMask # multiple monitors (which would be to go fullscreen on the monitor
cocoapy.NSBackingStoreBuffered, # backing # where the window is located). However I've no way to test.
False) # defer 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: # Then create a view and set it as our NSWindow's content view.
# BUG: I suspect that this doesn't do the right thing when using self._nsview = PygletView.alloc().initWithFrame_cocoaWindow_(content_rect, self)
# multiple monitors (which would be to go fullscreen on the monitor self._nswindow.setContentView_(self._nsview)
# where the window is located). However I've no way to test. self._nswindow.makeFirstResponder_(self._nsview)
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. # Create a canvas with the view as its drawable and attach context to it.
self._nsview = PygletView.alloc().initWithFrame_cocoaWindow_(content_rect, self) self.canvas = CocoaCanvas(self.display, self.screen, self._nsview)
self._nswindow.setContentView_(self._nsview) self.context.attach(self.canvas)
self._nswindow.makeFirstResponder_(self._nsview)
# Create a canvas with the view as its drawable and attach context to it. # Configure the window.
self.canvas = CocoaCanvas(self.display, self.screen, self._nsview) self._nswindow.setAcceptsMouseMovedEvents_(True)
self.context.attach(self.canvas) self._nswindow.setReleasedWhenClosed_(False)
self._nswindow.useOptimizedDrawing_(True)
self._nswindow.setPreservesContentDuringLiveResize_(False)
# Configure the window. # Set the delegate.
self._nswindow.setAcceptsMouseMovedEvents_(True) self._delegate = PygletDelegate.alloc().initWithWindow_(self)
self._nswindow.setReleasedWhenClosed_(False)
self._nswindow.useOptimizedDrawing_(True)
self._nswindow.setPreservesContentDuringLiveResize_(False)
# Set the delegate. # Configure CocoaWindow.
self._delegate = PygletDelegate.alloc().initWithWindow_(self) 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. if self._file_drops:
self.set_caption(self._caption) array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeURL)
if self._minimum_size is not None: self._nsview.registerForDraggedTypes_(array)
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: self.context.update_geometry()
array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeURL) self.switch_to()
self._nsview.registerForDraggedTypes_(array) self.set_vsync(self._vsync)
self.set_visible(self._visible)
self.context.update_geometry()
self.switch_to()
self.set_vsync(self._vsync)
self.set_visible(self._visible)
pool.drain()
def _set_nice_window_location(self): def _set_nice_window_location(self):
# Construct a list of all visible windows that aren't us. # Construct a list of all visible windows that aren't us.
@ -208,42 +210,39 @@ class CocoaWindow(BaseWindow):
if self._was_closed: if self._was_closed:
return return
# Create a temporary autorelease pool for this method. with AutoReleasePool():
pool = NSAutoreleasePool.new() # Restore cursor visibility
self.set_mouse_platform_visible(True)
self.set_exclusive_mouse(False)
self.set_exclusive_keyboard(False)
# Restore cursor visibility # Remove the delegate object
self.set_mouse_platform_visible(True) if self._delegate:
self.set_exclusive_mouse(False) self._nswindow.setDelegate_(None)
self.set_exclusive_keyboard(False) self._delegate.release()
self._delegate = None
# Remove the delegate object # Remove window from display and remove its view.
if self._delegate: if self._nswindow:
self._nswindow.setDelegate_(None) self._nswindow.orderOut_(None)
self._delegate.release() self._nswindow.setContentView_(None)
self._delegate = None self._nswindow.close()
# Remove window from display and remove its view. # Restore screen mode. This also releases the display
if self._nswindow: # if it was captured for fullscreen mode.
self._nswindow.orderOut_(None) self.screen.restore_mode()
self._nswindow.setContentView_(None)
self._nswindow.close()
# Restore screen mode. This also releases the display # Remove view from canvas and then remove canvas.
# if it was captured for fullscreen mode. if self.canvas:
self.screen.restore_mode() self.canvas.nsview.release()
self.canvas.nsview = None
self.canvas = None
# Remove view from canvas and then remove canvas. # Do this last, so that we don't see white flash
if self.canvas: # when exiting application from fullscreen mode.
self.canvas.nsview.release() super(CocoaWindow, self).close()
self.canvas.nsview = None
self.canvas = None
# Do this last, so that we don't see white flash self._was_closed = True
# when exiting application from fullscreen mode.
super(CocoaWindow, self).close()
self._was_closed = True
pool.drain()
def switch_to(self): def switch_to(self):
if self.context: if self.context:
@ -261,26 +260,24 @@ class CocoaWindow(BaseWindow):
event = True event = True
# Dequeue and process all of the pending Cocoa events. # Dequeue and process all of the pending Cocoa events.
pool = NSAutoreleasePool.new() with AutoReleasePool():
NSApp = NSApplication.sharedApplication() NSApp = NSApplication.sharedApplication()
while event and self._nswindow and self._context: while event and self._nswindow and self._context:
event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_( event = NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_(
cocoapy.NSAnyEventMask, None, cocoapy.NSEventTrackingRunLoopMode, True) cocoapy.NSAnyEventMask, None, cocoapy.NSEventTrackingRunLoopMode, True)
if event: if event:
event_type = event.type() event_type = event.type()
# Pass on all events. # Pass on all events.
NSApp.sendEvent_(event) NSApp.sendEvent_(event)
# And resend key events to special handlers. # And resend key events to special handlers.
if event_type == cocoapy.NSKeyDown and not event.isARepeat(): if event_type == cocoapy.NSKeyDown and not event.isARepeat():
NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyDown:'), None, event) NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyDown:'), None, event)
elif event_type == cocoapy.NSKeyUp: elif event_type == cocoapy.NSKeyUp:
NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyUp:'), None, event) NSApp.sendAction_to_from_(cocoapy.get_selector('pygletKeyUp:'), None, event)
elif event_type == cocoapy.NSFlagsChanged: elif event_type == cocoapy.NSFlagsChanged:
NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event) NSApp.sendAction_to_from_(cocoapy.get_selector('pygletFlagsChanged:'), None, event)
NSApp.updateWindows() NSApp.updateWindows()
pool.drain()
self._allow_dispatch_event = False self._allow_dispatch_event = False
@ -486,25 +483,25 @@ class CocoaWindow(BaseWindow):
if name == self.CURSOR_DEFAULT: if name == self.CURSOR_DEFAULT:
return DefaultMouseCursor() return DefaultMouseCursor()
cursors = { cursors = {
self.CURSOR_CROSSHAIR: 'crosshairCursor', self.CURSOR_CROSSHAIR: 'crosshairCursor',
self.CURSOR_HAND: 'pointingHandCursor', self.CURSOR_HAND: 'pointingHandCursor',
self.CURSOR_HELP: 'arrowCursor', self.CURSOR_HELP: 'arrowCursor',
self.CURSOR_NO: 'operationNotAllowedCursor', # Mac OS 10.6 self.CURSOR_NO: 'operationNotAllowedCursor', # Mac OS 10.6
self.CURSOR_SIZE: 'arrowCursor', self.CURSOR_SIZE: 'arrowCursor',
self.CURSOR_SIZE_UP: 'resizeUpCursor', self.CURSOR_SIZE_UP: 'resizeUpCursor',
self.CURSOR_SIZE_UP_RIGHT: 'arrowCursor', self.CURSOR_SIZE_UP_RIGHT: 'arrowCursor',
self.CURSOR_SIZE_RIGHT: 'resizeRightCursor', self.CURSOR_SIZE_RIGHT: 'resizeRightCursor',
self.CURSOR_SIZE_DOWN_RIGHT: 'arrowCursor', self.CURSOR_SIZE_DOWN_RIGHT: 'arrowCursor',
self.CURSOR_SIZE_DOWN: 'resizeDownCursor', self.CURSOR_SIZE_DOWN: 'resizeDownCursor',
self.CURSOR_SIZE_DOWN_LEFT: 'arrowCursor', self.CURSOR_SIZE_DOWN_LEFT: 'arrowCursor',
self.CURSOR_SIZE_LEFT: 'resizeLeftCursor', self.CURSOR_SIZE_LEFT: 'resizeLeftCursor',
self.CURSOR_SIZE_UP_LEFT: 'arrowCursor', self.CURSOR_SIZE_UP_LEFT: 'arrowCursor',
self.CURSOR_SIZE_UP_DOWN: 'resizeUpDownCursor', self.CURSOR_SIZE_UP_DOWN: 'resizeUpDownCursor',
self.CURSOR_SIZE_LEFT_RIGHT: 'resizeLeftRightCursor', self.CURSOR_SIZE_LEFT_RIGHT: 'resizeLeftRightCursor',
self.CURSOR_TEXT: 'IBeamCursor', self.CURSOR_TEXT: 'IBeamCursor',
self.CURSOR_WAIT: 'arrowCursor', # No wristwatch cursor in Cocoa self.CURSOR_WAIT: 'arrowCursor', # No wristwatch cursor in Cocoa
self.CURSOR_WAIT_ARROW: 'arrowCursor', # No wristwatch cursor in Cocoa self.CURSOR_WAIT_ARROW: 'arrowCursor', # No wristwatch cursor in Cocoa
} }
if name not in cursors: if name not in cursors:
raise RuntimeError('Unknown cursor name "%s"' % name) raise RuntimeError('Unknown cursor name "%s"' % name)
return CocoaMouseCursor(cursors[name]) return CocoaMouseCursor(cursors[name])
@ -514,7 +511,7 @@ class CocoaWindow(BaseWindow):
# If absolute, then x, y is given in global display coordinates # 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 # 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. # to warp the mouse position to a point inside of another display.
quartz.CGWarpMouseCursorPosition(CGPoint(x,y)) quartz.CGWarpMouseCursorPosition(CGPoint(x, y))
else: else:
# Window-relative coordinates: (x, y) are given in window coords # Window-relative coordinates: (x, y) are given in window coords
# with (0,0) at bottom-left corner of window and y up. We find # 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. # Move mouse to center of window.
frame = self._nswindow.frame() frame = self._nswindow.frame()
width, height = frame.size.width, frame.size.height 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) quartz.CGAssociateMouseAndMouseCursorPosition(False)
else: else:
quartz.CGAssociateMouseAndMouseCursorPosition(True) quartz.CGAssociateMouseAndMouseCursorPosition(True)

View File

@ -293,7 +293,10 @@ F17 = 0xffce
F18 = 0xffcf F18 = 0xffcf
F19 = 0xffd0 F19 = 0xffd0
F20 = 0xffd1 F20 = 0xffd1
F21 = 0xffd2
F22 = 0xffd3
F23 = 0xffd4
F24 = 0xffd5
# Modifiers # Modifiers
LSHIFT = 0xffe1 LSHIFT = 0xffe1
RSHIFT = 0xffe2 RSHIFT = 0xffe2