pyglet 2.1.6__py3-none-any.whl → 2.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. pyglet/__init__.py +27 -42
  2. pyglet/app/base.py +2 -2
  3. pyglet/clock.py +1 -1
  4. pyglet/display/base.py +31 -21
  5. pyglet/display/cocoa.py +25 -1
  6. pyglet/display/headless.py +12 -1
  7. pyglet/display/win32.py +134 -18
  8. pyglet/display/xlib.py +285 -70
  9. pyglet/event.py +17 -1
  10. pyglet/experimental/README.md +1 -1
  11. pyglet/experimental/jobs.py +1 -1
  12. pyglet/experimental/multitexture_sprite.py +2 -2
  13. pyglet/font/__init__.py +1 -1
  14. pyglet/font/base.py +8 -5
  15. pyglet/font/dwrite/__init__.py +13 -8
  16. pyglet/font/dwrite/dwrite_lib.py +1 -1
  17. pyglet/font/user.py +1 -1
  18. pyglet/gl/base.py +8 -4
  19. pyglet/gl/cocoa.py +4 -0
  20. pyglet/gl/gl.py +4 -3
  21. pyglet/gl/gl.pyi +2320 -0
  22. pyglet/gl/gl_compat.py +7 -18
  23. pyglet/gl/gl_compat.pyi +3097 -0
  24. pyglet/gl/xlib.py +24 -0
  25. pyglet/graphics/shader.py +8 -1
  26. pyglet/graphics/vertexbuffer.py +1 -1
  27. pyglet/gui/frame.py +2 -2
  28. pyglet/gui/widgets.py +1 -1
  29. pyglet/image/__init__.py +3 -3
  30. pyglet/image/buffer.py +3 -3
  31. pyglet/input/base.py +22 -12
  32. pyglet/input/linux/evdev.py +96 -23
  33. pyglet/input/linux/evdev_constants.py +2 -1
  34. pyglet/input/win32/xinput.py +6 -3
  35. pyglet/libs/darwin/cocoapy/cocoalibs.py +3 -1
  36. pyglet/libs/ioctl.py +2 -2
  37. pyglet/libs/win32/__init__.py +12 -0
  38. pyglet/libs/win32/constants.py +4 -0
  39. pyglet/libs/win32/types.py +97 -0
  40. pyglet/libs/x11/xrandr.py +166 -0
  41. pyglet/libs/x11/xrender.py +43 -0
  42. pyglet/libs/x11/xsync.py +43 -0
  43. pyglet/math.py +65 -54
  44. pyglet/media/buffered_logger.py +1 -1
  45. pyglet/media/codecs/ffmpeg.py +18 -34
  46. pyglet/media/codecs/gstreamer.py +3 -3
  47. pyglet/media/codecs/pyogg.py +1 -1
  48. pyglet/media/codecs/wave.py +6 -0
  49. pyglet/media/codecs/wmf.py +33 -7
  50. pyglet/media/devices/win32.py +1 -1
  51. pyglet/media/drivers/base.py +1 -1
  52. pyglet/media/drivers/directsound/interface.py +4 -0
  53. pyglet/media/drivers/listener.py +2 -2
  54. pyglet/media/drivers/xaudio2/interface.py +6 -2
  55. pyglet/media/drivers/xaudio2/lib_xaudio2.py +1 -1
  56. pyglet/media/instrumentation.py +2 -2
  57. pyglet/media/player.py +2 -2
  58. pyglet/media/player_worker_thread.py +1 -1
  59. pyglet/media/synthesis.py +1 -1
  60. pyglet/model/codecs/gltf.py +1 -1
  61. pyglet/shapes.py +1 -1
  62. pyglet/sprite.py +1 -1
  63. pyglet/text/caret.py +44 -5
  64. pyglet/text/layout/base.py +3 -3
  65. pyglet/util.py +1 -1
  66. pyglet/window/__init__.py +54 -14
  67. pyglet/window/cocoa/__init__.py +27 -0
  68. pyglet/window/mouse.py +11 -1
  69. pyglet/window/win32/__init__.py +40 -14
  70. pyglet/window/xlib/__init__.py +21 -7
  71. {pyglet-2.1.6.dist-info → pyglet-2.1.9.dist-info}/METADATA +1 -1
  72. {pyglet-2.1.6.dist-info → pyglet-2.1.9.dist-info}/RECORD +74 -70
  73. {pyglet-2.1.6.dist-info → pyglet-2.1.9.dist-info}/LICENSE +0 -0
  74. {pyglet-2.1.6.dist-info → pyglet-2.1.9.dist-info}/WHEEL +0 -0
pyglet/gl/xlib.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
+ from _ctypes import _Pointer
4
5
  from ctypes import POINTER, byref, c_int, c_uint, cast
5
6
  from typing import TYPE_CHECKING, NoReturn
6
7
 
@@ -13,6 +14,8 @@ from pyglet.gl import lib
13
14
  from pyglet.gl.base import Config, DisplayConfig, Context
14
15
 
15
16
  from pyglet.display.xlib import XlibCanvas
17
+ from pyglet.libs.x11 import xlib
18
+ from pyglet.libs.x11.xrender import XRenderFindVisualFormat
16
19
 
17
20
  if TYPE_CHECKING:
18
21
  from pyglet.libs.x11.xlib import Display
@@ -52,6 +55,10 @@ class XlibConfig(Config): # noqa: D101
52
55
 
53
56
  result = [XlibDisplayConfig(canvas, info, c, self) for c in configs]
54
57
 
58
+ # If we intend to have a transparent framebuffer.
59
+ if self.transparent_framebuffer:
60
+ result = [fb_cf for fb_cf in result if fb_cf.transparent]
61
+
55
62
  # Can't free array until all XlibGLConfig's are GC'd. Too much
56
63
  # hassle, live with leak. XXX
57
64
  # xlib.XFree(configs)
@@ -102,6 +109,7 @@ class XlibDisplayConfig(DisplayConfig): # noqa: D101
102
109
 
103
110
  self.glx_info = info
104
111
  self.fbconfig = fbconfig
112
+ self.transparent = False
105
113
 
106
114
  for name, attr in self.attribute_ids.items():
107
115
  value = c_int()
@@ -110,6 +118,22 @@ class XlibDisplayConfig(DisplayConfig): # noqa: D101
110
118
  if result >= 0:
111
119
  setattr(self, name, value.value)
112
120
 
121
+ # If user intends for a transparent framebuffer, the visual info needs to be
122
+ # queried for it. Even if a config supports alpha_size 8 and depth_size 32, there is no
123
+ # guarantee the visual info supports that same configuration.
124
+ if config.transparent_framebuffer:
125
+ xvi_ptr = glx.glXGetVisualFromFBConfig(canvas.display._display, self.fbconfig) # noqa: SLF001
126
+ if xvi_ptr:
127
+ self.transparent = self._is_visual_transparent(xvi_ptr.contents.visual) # noqa: SLF001
128
+ xlib.XFree(xvi_ptr)
129
+
130
+ def _is_visual_transparent(self, visual: _Pointer[xlib.Visual]) -> bool:
131
+ if not XRenderFindVisualFormat:
132
+ return False
133
+
134
+ xrender_format = XRenderFindVisualFormat(self.canvas.display._display, visual)
135
+ return xrender_format and xrender_format.contents.direct.alphaMask != 0
136
+
113
137
  def get_visual_info(self) -> glx.XVisualInfo:
114
138
  return glx.glXGetVisualFromFBConfig(self.canvas.display._display, self.fbconfig).contents # noqa: SLF001
115
139
 
pyglet/graphics/shader.py CHANGED
@@ -58,6 +58,7 @@ from pyglet.gl.gl import (
58
58
  glUnmapBuffer,
59
59
  glUseProgram,
60
60
  glVertexAttribDivisor,
61
+ glVertexAttribIPointer,
61
62
  glVertexAttribPointer,
62
63
  )
63
64
  from pyglet.graphics.vertexbuffer import AttributeBufferObject, BufferObject
@@ -257,6 +258,9 @@ class Attribute:
257
258
  self.element_size = sizeof(self.c_type)
258
259
  self.stride = count * self.element_size
259
260
 
261
+ self._is_int = gl_type in (gl.GL_INT, gl.GL_SHORT, gl.GL_BYTE, gl.GL_UNSIGNED_INT,
262
+ gl.GL_UNSIGNED_SHORT, gl.GL_UNSIGNED_BYTE) and self.normalize is False
263
+
260
264
  def enable(self) -> None:
261
265
  """Enable the attribute."""
262
266
  glEnableVertexAttribArray(self.location)
@@ -271,7 +275,10 @@ class Attribute:
271
275
  Pointer offset to the currently bound buffer for this attribute.
272
276
 
273
277
  """
274
- glVertexAttribPointer(self.location, self.count, self.gl_type, self.normalize, self.stride, ptr)
278
+ if self._is_int:
279
+ glVertexAttribIPointer(self.location, self.count, self.gl_type, self.stride, ptr)
280
+ else:
281
+ glVertexAttribPointer(self.location, self.count, self.gl_type, self.normalize, self.stride, ptr)
275
282
 
276
283
  def set_divisor(self) -> None:
277
284
  glVertexAttribDivisor(self.location, 1)
@@ -1,6 +1,6 @@
1
1
  """OpenGL Buffer Objects.
2
2
 
3
- :py:class:`~BufferObject` and a :py:class:`~BackedBufferObject` are provied.
3
+ :py:class:`~BufferObject` and a :py:class:`~BackedBufferObject` are provided.
4
4
  The first is a lightweight abstraction over an OpenGL buffer, as created
5
5
  with ``glGenBuffers``. The backed buffer object is similar, but provides a
6
6
  full mirror of the data in CPU memory. This allows for delayed uploading of
pyglet/gui/frame.py CHANGED
@@ -24,7 +24,7 @@ class Frame:
24
24
 
25
25
  Args:
26
26
  window:
27
- The SpatialHash will recieve events from this Window.
27
+ The SpatialHash will receive events from this Window.
28
28
  Appropriate events will be passed on to all added Widgets.
29
29
  enable:
30
30
  Whether to enable frame.
@@ -168,7 +168,7 @@ class MovableFrame(Frame):
168
168
 
169
169
  Args:
170
170
  window:
171
- The SpatialHash will recieve events from this Window.
171
+ The SpatialHash will receive events from this Window.
172
172
  Appropriate events will be passed on to all added Widgets.
173
173
  enable:
174
174
  Whether to enable frame.
pyglet/gui/widgets.py CHANGED
@@ -601,7 +601,7 @@ class TextEntry(WidgetBase):
601
601
  self._caret.on_text_motion_select(motion)
602
602
 
603
603
  def on_commit(self, widget: TextEntry, text: str) -> None:
604
- """Event: dispatches the current text when commited via Enter/Return key."""
604
+ """Event: dispatches the current text when committed via Enter/Return key."""
605
605
 
606
606
 
607
607
  TextEntry.register_event_type('on_commit')
pyglet/image/__init__.py CHANGED
@@ -556,7 +556,7 @@ class ImageData(AbstractImage):
556
556
  Note:
557
557
  Conversion to another format is done on the CPU, and can be
558
558
  somewhat costly for larger images. Consider performing conversion
559
- at load time for framerate sensitive applictions.
559
+ at load time for framerate sensitive applications.
560
560
  """
561
561
  fmt = fmt or self._desired_format
562
562
  pitch = pitch or self._current_pitch
@@ -1201,7 +1201,7 @@ class Texture(AbstractImage):
1201
1201
  layer: int = 0, access: int = GL_READ_WRITE, fmt: int = GL_RGBA32F):
1202
1202
  """Bind as an ImageTexture for use with a :py:class:`~pyglet.shader.ComputeShaderProgram`.
1203
1203
 
1204
- .. note:: OpenGL 4.3, or 4.2 with the GL_ARB_compute_shader extention is required.
1204
+ .. note:: OpenGL 4.3, or 4.2 with the GL_ARB_compute_shader extension is required.
1205
1205
  """
1206
1206
  glBindImageTexture(unit, self.id, level, layered, layer, access, fmt)
1207
1207
 
@@ -1211,7 +1211,7 @@ class Texture(AbstractImage):
1211
1211
  fmt: int = GL_RGBA, blank_data: bool = True) -> Texture:
1212
1212
  """Create a Texture
1213
1213
 
1214
- Create a Texture with the specified dimentions, target and format.
1214
+ Create a Texture with the specified dimensions, target and format.
1215
1215
  On return, the texture will be bound.
1216
1216
 
1217
1217
  Args:
pyglet/image/buffer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """OpenGL Framebuffer abstractions.
2
2
 
3
3
  This module provides classes for working with Framebuffers & Renderbuffers
4
- and their attachments. Attachements can be pyglet Texture objects, which allows
4
+ and their attachments. Attachments can be pyglet Texture objects, which allows
5
5
  easily accessing their data, saving to disk, etc. Renderbuffers can be used
6
6
  if you don't need to access their data at a later time. For example::
7
7
 
@@ -40,7 +40,7 @@ if TYPE_CHECKING:
40
40
 
41
41
 
42
42
  def get_max_color_attachments() -> int:
43
- """Get the maximum allow Framebuffer Color attachements."""
43
+ """Get the maximum allow Framebuffer Color attachments."""
44
44
  number = GLint()
45
45
  glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, number)
46
46
  return number.value
@@ -144,7 +144,7 @@ class Framebuffer:
144
144
 
145
145
  Unbind should be called to prevent further rendering
146
146
  to the framebuffer, or if you wish to access data
147
- from its Texture atachments.
147
+ from its Texture attachments.
148
148
  """
149
149
  glBindFramebuffer(self.target, 0)
150
150
 
pyglet/input/base.py CHANGED
@@ -71,7 +71,7 @@ class Device:
71
71
  Args:
72
72
  window:
73
73
  Optional window to associate with the device. The behaviour
74
- of this parameter is device and operating system dependant.
74
+ of this parameter is device and operating system dependent.
75
75
  It can usually be omitted for most devices.
76
76
  exclusive:
77
77
  If ``True`` the device will be opened exclusively so that no
@@ -103,7 +103,7 @@ class Device:
103
103
  string. This is generated from the hardware identifiers,
104
104
  and is in the same format as was popularized by SDL2.
105
105
  GUIDs differ between platforms, but are generally 32
106
- hexidecimal characters.
106
+ hexadecimal characters.
107
107
  """
108
108
  raise NotImplementedError('abstract')
109
109
 
@@ -506,7 +506,7 @@ class Controller(EventDispatcher):
506
506
  """
507
507
 
508
508
  def __init__(self, device: Device, mapping: dict):
509
- """Create a Controller instace mapped to a Device.
509
+ """Create a Controller instance mapped to a Device.
510
510
 
511
511
  .. versionadded:: 2.0
512
512
  """
@@ -520,6 +520,7 @@ class Controller(EventDispatcher):
520
520
  #: The unique guid for this Device
521
521
  self.guid: str = mapping.get('guid')
522
522
 
523
+ # Pollable
523
524
  self.a: bool = False
524
525
  self.b: bool = False
525
526
  self.x: bool = False
@@ -534,6 +535,10 @@ class Controller(EventDispatcher):
534
535
 
535
536
  self.lefttrigger: float = 0.0
536
537
  self.righttrigger: float = 0.0
538
+ self.dpad: Vec2 = Vec2()
539
+ self.leftanalog: Vec2 = Vec2()
540
+ self.rightanalog: Vec2 = Vec2()
541
+
537
542
  self.leftx: float = 0.0
538
543
  self.lefty: float = 0.0
539
544
  self.rightx: float = 0.0
@@ -575,7 +580,7 @@ class Controller(EventDispatcher):
575
580
  A string, currently one of "PS", "XB", or "GENERIC".
576
581
  """
577
582
  product_id = None
578
- # TODO: add more checks for vender hardware ids.
583
+ # TODO: add more checks for vendor hardware ids.
579
584
 
580
585
  # Windows
581
586
  if self.name == 'XINPUTCONTROLLER':
@@ -617,12 +622,14 @@ class Controller(EventDispatcher):
617
622
  @control.event
618
623
  def on_change(value):
619
624
  self.dpady = round(value * scale + bias) * sign # normalized
625
+ self.dpad = Vec2(self.dpadx, self.dpady)
620
626
  self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
621
627
 
622
628
  elif axis_name in ("dpleft", "dpright"):
623
629
  @control.event
624
630
  def on_change(value):
625
631
  self.dpadx = round(value * scale + bias) * sign # normalized
632
+ self.dpad = Vec2(self.dpadx, self.dpady)
626
633
  self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
627
634
 
628
635
  elif axis_name in ("lefttrigger", "righttrigger"):
@@ -637,14 +644,16 @@ class Controller(EventDispatcher):
637
644
  def on_change(value):
638
645
  normalized_value = value * scale + bias
639
646
  setattr(self, axis_name, normalized_value)
640
- self.dispatch_event('on_stick_motion', self, "leftstick", Vec2(self.leftx, -self.lefty))
647
+ self.left_analog = Vec2(self.leftx, -self.lefty)
648
+ self.dispatch_event('on_stick_motion', self, "leftstick", self.left_analog)
641
649
 
642
650
  elif axis_name in ("rightx", "righty"):
643
651
  @control.event
644
652
  def on_change(value):
645
653
  normalized_value = value * scale + bias
646
654
  setattr(self, axis_name, normalized_value)
647
- self.dispatch_event('on_stick_motion', self, "rightstick", Vec2(self.rightx, -self.righty))
655
+ self.right_analog = Vec2(self.rightx, -self.righty)
656
+ self.dispatch_event('on_stick_motion', self, "rightstick", self.right_analog)
648
657
 
649
658
  def _bind_button_control(self, relation: Relation, control: Button, button_name: str) -> None:
650
659
  if button_name in ("dpleft", "dpright", "dpup", "dpdown"):
@@ -655,7 +664,8 @@ class Controller(EventDispatcher):
655
664
  def on_change(value):
656
665
  target, bias = defaults[button_name]
657
666
  setattr(self, target, bias * value)
658
- self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
667
+ self.dpad = Vec2(self.dpadx, self.dpady)
668
+ self.dispatch_event('on_dpad_motion', self, self.dpad)
659
669
  else:
660
670
  @control.event
661
671
  def on_change(value):
@@ -672,7 +682,7 @@ class Controller(EventDispatcher):
672
682
  def _bind_dedicated_hat(self, relation: Relation, control: AbsoluteAxis) -> None:
673
683
  # 8-directional hat encoded as a single control (Windows/Mac)
674
684
  _vecs = (Vec2(0.0, 1.0), Vec2(1.0, 1.0), Vec2(1.0, 0.0), Vec2(1.0, -1.0), # n, ne, e, se
675
- Vec2(0.0, -1.0), Vec2(-1.0, -1.0), Vec2(-1.0, 0.0), Vec2(-1.0, 1.0)) # s, sw, w, nw
685
+ Vec2(0.0, -1.0), Vec2(-1.0, -1.0), Vec2(-1.0, 0.0), Vec2(-1.0, 1.0)) # s, sw, w, nw
676
686
  _input_map = {key: val for key, val in zip(range(int(control.min), int(control.max + 1)), _vecs)}
677
687
 
678
688
  # For some Directinput devices:
@@ -681,7 +691,7 @@ class Controller(EventDispatcher):
681
691
  @control.event
682
692
  def on_change(value):
683
693
  vector = _input_map.get(value // _scale, Vec2(0.0, 0.0))
684
- self.dpadx, self.dpady = vector
694
+ self.dpad = vector
685
695
  self.dispatch_event('on_dpad_motion', self, vector)
686
696
 
687
697
  def _initialize_controls(self) -> None:
@@ -730,8 +740,8 @@ class Controller(EventDispatcher):
730
740
 
731
741
  self._bind_axis_control(relation, control, dpname)
732
742
 
733
- except IndexError:
734
- warnings.warn(f"Could not map '{relation}' to '{name}'")
743
+ except (IndexError, AttributeError, KeyError):
744
+ warnings.warn(f"Could not map physical Control '{relation}' to '{name}'")
735
745
  continue
736
746
 
737
747
  def open(self, window: None | BaseWindow = None, exclusive: bool = False) -> None:
@@ -1096,7 +1106,7 @@ class ControllerManager(EventDispatcher):
1096
1106
 
1097
1107
  def on_connect(self, controller) -> Controller:
1098
1108
  """A Controller has been connected. If this is
1099
- a previously dissconnected Controller that is
1109
+ a previously disconnected Controller that is
1100
1110
  being re-connected, the same Controller instance
1101
1111
  will be returned.
1102
1112
  """
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import time
5
5
  import ctypes
6
+ import select
6
7
  import warnings
7
8
  import threading
8
9
 
@@ -11,6 +12,7 @@ from ctypes import c_int16 as _s16
11
12
  from ctypes import c_uint32 as _u32
12
13
  from ctypes import c_int32 as _s32
13
14
  from ctypes import c_int64 as _s64
15
+ from ctypes import c_byte as _c_byte
14
16
 
15
17
  import pyglet
16
18
 
@@ -31,6 +33,24 @@ except ImportError:
31
33
  return c.read(fd, buffers, 3072)
32
34
 
33
35
 
36
+ KeyMaxArray = _c_byte * ((KEY_MAX // 8) + 1)
37
+
38
+
39
+ class EvdevButton(Button):
40
+ event_type: int
41
+ event_code: int
42
+
43
+
44
+ class EvdevAbsoluteAxis(AbsoluteAxis):
45
+ event_type: int
46
+ event_code: int
47
+
48
+
49
+ class EvdevRelativeAxis(RelativeAxis):
50
+ event_type: int
51
+ event_code: int
52
+
53
+
34
54
  # Structures from /linux/blob/master/include/uapi/linux/input.h
35
55
 
36
56
  class Timeval(ctypes.Structure):
@@ -158,11 +178,13 @@ class FFEvent(ctypes.Structure):
158
178
  )
159
179
 
160
180
 
181
+ # Helper "macros" for file io:
161
182
  EVIOCGVERSION = _IOR('E', 0x01, ctypes.c_int)
162
183
  EVIOCGID = _IOR('E', 0x02, InputID)
163
184
  EVIOCGNAME = _IOR_str('E', 0x06)
164
185
  EVIOCGPHYS = _IOR_str('E', 0x07)
165
186
  EVIOCGUNIQ = _IOR_str('E', 0x08)
187
+ EVIOCGKEY = _IOR_len('E', 0x18)
166
188
  EVIOCSFF = _IOW('E', 0x80, FFEvent)
167
189
 
168
190
 
@@ -170,9 +192,14 @@ def EVIOCGBIT(fileno, ev, buffer):
170
192
  return _IOR_len('E', 0x20 + ev)(fileno, buffer)
171
193
 
172
194
 
173
- def EVIOCGABS(fileno, abs):
174
- buffer = InputABSInfo()
175
- return _IOR_len('E', 0x40 + abs)(fileno, buffer)
195
+ def EVIOCGABS(fileno, ev, buffer=InputABSInfo()):
196
+ print("absbuffer instance:", buffer)
197
+ return _IOR_len('E', 0x40 + ev)(fileno, buffer)
198
+
199
+
200
+ def get_key_state(fileno, event_code, buffer=KeyMaxArray()):
201
+ buffer = EVIOCGKEY(fileno, buffer)
202
+ return bool(buffer[event_code // 8] & (1 << (event_code % 8)))
176
203
 
177
204
 
178
205
  def get_set_bits(bytestring):
@@ -217,18 +244,16 @@ def _create_control(fileno, event_type, event_code):
217
244
  value = absinfo.value
218
245
  minimum = absinfo.minimum
219
246
  maximum = absinfo.maximum
220
- control = AbsoluteAxis(name, minimum, maximum, raw_name)
247
+ control = EvdevAbsoluteAxis(name, minimum, maximum, raw_name, inverted=name == 'hat_y')
221
248
  control.value = value
222
- if name == 'hat_y':
223
- control.inverted = True
224
249
  elif event_type == EV_REL:
225
250
  raw_name = rel_raw_names.get(event_code, f'EV_REL({event_code:x})')
226
251
  name = _rel_names.get(event_code)
227
- control = RelativeAxis(name, raw_name)
252
+ control = EvdevRelativeAxis(name, raw_name)
228
253
  elif event_type == EV_KEY:
229
254
  raw_name = key_raw_names.get(event_code, f'EV_KEY({event_code:x})')
230
255
  name = None
231
- control = Button(name, raw_name)
256
+ control = EvdevButton(name, raw_name)
232
257
  else:
233
258
  return None
234
259
  control.event_type = event_type
@@ -248,7 +273,8 @@ event_types = {
248
273
 
249
274
 
250
275
  class EvdevDevice(XlibSelectDevice, Device):
251
- _fileno = None
276
+ _fileno: int | None
277
+ _poll: "select.poll | None"
252
278
 
253
279
  def __init__(self, display, filename):
254
280
  self._filename = filename
@@ -301,11 +327,14 @@ class EvdevDevice(XlibSelectDevice, Device):
301
327
  self.control_map[(event_type, event_code)] = control
302
328
  self.controls.append(control)
303
329
 
304
- self.controls.sort(key=lambda c: c.event_code)
330
+ self.controls.sort(key=lambda ctrl: ctrl.event_code)
305
331
  os.close(fileno)
306
332
 
333
+ self._poll = select.poll()
307
334
  self._event_size = ctypes.sizeof(InputEvent)
308
335
  self._event_buffer = (InputEvent * 64)()
336
+ self._syn_dropped = False
337
+ self._event_queue = []
309
338
 
310
339
  super().__init__(display, name)
311
340
 
@@ -321,6 +350,7 @@ class EvdevDevice(XlibSelectDevice, Device):
321
350
  def open(self, window=None, exclusive=False):
322
351
  try:
323
352
  self._fileno = os.open(self._filename, os.O_RDWR | os.O_NONBLOCK)
353
+ self._poll.register(self._fileno, select.POLLIN | select.POLLPRI)
324
354
  except OSError as e:
325
355
  raise DeviceOpenException(e)
326
356
 
@@ -333,6 +363,9 @@ class EvdevDevice(XlibSelectDevice, Device):
333
363
  if not self._fileno:
334
364
  return
335
365
 
366
+ if self._poll:
367
+ self._poll.unregister(self._fileno)
368
+
336
369
  pyglet.app.platform_event_loop.select_devices.remove(self)
337
370
  os.close(self._fileno)
338
371
  self._fileno = None
@@ -340,6 +373,20 @@ class EvdevDevice(XlibSelectDevice, Device):
340
373
  def get_controls(self):
341
374
  return self.controls
342
375
 
376
+ def _resync_control_state(self):
377
+ """Manually resync all Control state.
378
+
379
+ This method queries and resets the state of each Control using the appropriate
380
+ ioctl calls. If this causes the Control value to change, the associated events
381
+ will be dispatched. This is a somewhat expensive operation, but it is necessary
382
+ to perform in some cases (such as when a SYN_DROPPED event is received).
383
+ """
384
+ for control in self.control_map.values():
385
+ if isinstance(control, EvdevButton):
386
+ control.value = get_key_state(self._fileno, control.event_code)
387
+ if isinstance(control, EvdevAbsoluteAxis):
388
+ control.value = EVIOCGABS(self._fileno, control.event_code).value
389
+
343
390
  # Force Feedback methods
344
391
 
345
392
  def ff_upload_effect(self, structure):
@@ -351,25 +398,51 @@ class EvdevDevice(XlibSelectDevice, Device):
351
398
  return self._fileno
352
399
 
353
400
  def poll(self):
354
- return False
401
+ return True if self._poll.poll(0) else False
355
402
 
356
403
  def select(self):
357
- if not self._fileno:
358
- return
359
-
404
+ """When the file descriptor is ready, read and process InputEvents.
405
+
406
+ This method has the following behavior:
407
+ - Read and queue all incoming input events.
408
+ - When a SYN_REPORT event is received, dispatch all queued events.
409
+ - If a SYN_DROPPED event is received, set a flag. When the next
410
+ SYN_REPORT event appears, drop all queued events & manually resync
411
+ all Control state.
412
+ """
360
413
  try:
361
414
  bytes_read = _readv(self._fileno, self._event_buffer)
415
+ n_events = bytes_read // self._event_size
362
416
  except OSError:
363
417
  self.close()
364
418
  return
365
419
 
366
- n_events = bytes_read // self._event_size
367
-
368
420
  for event in self._event_buffer[:n_events]:
369
- try:
370
- self.control_map[(event.type, event.code)].value = event.value
371
- except KeyError:
372
- pass
421
+
422
+ # Mark the current chain of events as invalid and continue:
423
+ if (event.type, event.code) == (EV_SYN, SYN_DROPPED):
424
+ self._syn_dropped = True
425
+ continue
426
+
427
+ # Dispatch queued events when SYN_REPORT comes in:
428
+ if (event.type, event.code) == (EV_SYN, SYN_REPORT):
429
+
430
+ # Unless a SYN_DROPPED event has been received,
431
+ # in which case discard all queued events and resync:
432
+ if self._syn_dropped:
433
+ self._event_queue.clear()
434
+ self._syn_dropped = False
435
+ self._resync_control_state()
436
+
437
+ # Dispatch all queued events, then clear the queue:
438
+ for queued_event in self._event_queue:
439
+ if control := self.control_map.get((queued_event.type, queued_event.code)):
440
+ control.value = queued_event.value
441
+ self._event_queue.clear()
442
+
443
+ # This is not a SYN_REPORT or SYN_DROPPED event, so it is probably
444
+ # an input event. Queue it until the next SYN_REPORT event comes in:
445
+ self._event_queue.append(event)
373
446
 
374
447
 
375
448
  class FFController(Controller):
@@ -471,7 +544,7 @@ class EvdevControllerManager(ControllerManager, XlibSelectDevice):
471
544
  if controller := self._controllers.get(name, _create_controller(device)):
472
545
  self._controllers[name] = controller
473
546
  # Dispatch event in main thread:
474
- pyglet.app.platform_event_loop.post_event(self, 'on_connect', controller)
547
+ self.post_event('on_connect', controller)
475
548
 
476
549
  def select(self):
477
550
  """Triggered whenever the devices_file changes."""
@@ -551,8 +624,8 @@ def _detect_controller_mapping(device):
551
624
  ABS_Z: 'lefttrigger', ABS_RZ: 'righttrigger',
552
625
  ABS_X: 'leftx', ABS_Y: 'lefty', ABS_RX: 'rightx', ABS_RY: 'righty'}
553
626
 
554
- button_controls = [control for control in device.controls if isinstance(control, Button)]
555
- axis_controls = [control for control in device.controls if isinstance(control, AbsoluteAxis)]
627
+ button_controls = [control for control in device.controls if isinstance(control, EvdevButton)]
628
+ axis_controls = [control for control in device.controls if isinstance(control, EvdevAbsoluteAxis)]
556
629
  hat_controls = [control for control in device.controls if control.name in ('hat_x', 'hat_y')]
557
630
 
558
631
  for i, control in enumerate(button_controls):
@@ -1,4 +1,4 @@
1
- """Event constants from /usr/include/linux/input.h """
1
+ """Event constants from /usr/include/linux/input-event-codes.h"""
2
2
 
3
3
  EV_SYN = 0x00
4
4
  EV_KEY = 0x01
@@ -17,6 +17,7 @@ EV_MAX = 0x1f
17
17
 
18
18
  SYN_REPORT = 0
19
19
  SYN_CONFIG = 1
20
+ SYN_DROPPED = 3
20
21
 
21
22
  # Keys and buttons
22
23
 
@@ -575,14 +575,16 @@ class XInputController(Controller):
575
575
  def on_change(value):
576
576
  normalized_value = value * scale + bias
577
577
  setattr(self, name, normalized_value)
578
- self.dispatch_event('on_stick_motion', self, "leftstick", Vec2(self.leftx, self.lefty))
578
+ self.leftanalog = Vec2(self.leftx, self.lefty)
579
+ self.dispatch_event('on_stick_motion', self, "leftstick", self.leftanalog)
579
580
 
580
581
  elif name in ("rightx", "righty"):
581
582
  @control.event
582
583
  def on_change(value):
583
584
  normalized_value = value * scale + bias
584
585
  setattr(self, name, normalized_value)
585
- self.dispatch_event('on_stick_motion', self, "rightstick", Vec2(self.rightx, self.righty))
586
+ self.rightanalog = Vec2(self.rightx, self.righty)
587
+ self.dispatch_event('on_stick_motion', self, "rightstick", self.rightanalog)
586
588
 
587
589
  def _add_button(self, control, name):
588
590
 
@@ -592,7 +594,8 @@ class XInputController(Controller):
592
594
  target, bias = {'dpleft': ('dpadx', -1.0), 'dpright': ('dpadx', 1.0),
593
595
  'dpdown': ('dpady', -1.0), 'dpup': ('dpady', 1.0)}[name]
594
596
  setattr(self, target, bias * value)
595
- self.dispatch_event('on_dpad_motion', self, Vec2(self.dpadx, self.dpady))
597
+ self.dpad = Vec2(self.dpadx, self.dpady)
598
+ self.dispatch_event('on_dpad_motion', self, self.dpad)
596
599
  else:
597
600
  @control.event
598
601
  def on_change(value):
@@ -232,6 +232,8 @@ NSDeviceSize = c_void_p.in_dll(appkit, 'NSDeviceSize')
232
232
  NSDeviceResolution = c_void_p.in_dll(appkit, 'NSDeviceResolution')
233
233
  NSDragOperationGeneric = 4
234
234
 
235
+ NSStatusWindowLevel = 25
236
+
235
237
 
236
238
  # /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h
237
239
  NSAnyEventMask = 0xFFFFFFFF # NSUIntegerMax
@@ -336,7 +338,7 @@ NSOpenGLProfileVersion3_2Core = 0x3200 # choose an OpenGL 3.2 Core Implementa
336
338
  NSOpenGLProfileVersion4_1Core = 0x4100 # choose an OpenGL 4.1 Core Implementation
337
339
 
338
340
  NSOpenGLCPSwapInterval = 222
339
-
341
+ NSOpenGLCPSurfaceOpacity = 236
340
342
 
341
343
  # /System/Library/Frameworks/ApplicationServices.framework/Frameworks/...
342
344
  # CoreGraphics.framework/Headers/CGImage.h
pyglet/libs/ioctl.py CHANGED
@@ -41,7 +41,7 @@ from typing import TYPE_CHECKING
41
41
  if TYPE_CHECKING:
42
42
  from ctypes import Structure, c_int, c_uint
43
43
  from typing import Callable, Union
44
- c_data = Union[type[Structure], c_int, c_uint]
44
+ c_data = type[Structure] | type[c_int] | type[c_uint]
45
45
 
46
46
 
47
47
  _IOC_NRBITS = 8
@@ -64,7 +64,7 @@ _IOC_READ = 2
64
64
  # 'code' instead of 'type' to indicate the ioctl "magic number" ('H', 'E', etc.).
65
65
 
66
66
 
67
- def _IOC(io_dir: _IOC_NONE | _IOC_READ | _IOC_WRITE, code: int, nr: int, size: int) -> int:
67
+ def _IOC(io_dir: Union[_IOC_NONE, _IOC_READ, _IOC_WRITE], code: int, nr: int, size: int) -> int:
68
68
  return (io_dir << _IOC_DIRSHIFT) | (code << _IOC_TYPESHIFT) | (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT)
69
69
 
70
70