pyglet 2.1.13__py3-none-any.whl → 3.0.dev1__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 (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +5 -21
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +28 -8
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +154 -194
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.13.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {pyglet-2.1.13.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pyglet.libs.darwin import ObjCClass, ns_to_py, nsstr_to_py, AutoReleasePool, pystr_to_ns, py_to_ns
6
+ from pyglet.libs.darwin.cocoapy.runtime import get_callback_block, ObjCBlock
7
+ from pyglet.window.dialog.base import FileOpenDialogBase, FileSaveDialogBase
8
+
9
+ NSOpenPanel = ObjCClass('NSOpenPanel')
10
+ NSSavePanel = ObjCClass('NSSavePanel')
11
+ NSURL = ObjCClass('NSURL')
12
+
13
+
14
+ class MacOSFileOpenDialog(FileOpenDialogBase):
15
+ _cb_block: ObjCBlock | None = None
16
+ def __init__(
17
+ self, title: str="Open File", initial_dir: str | Path=Path.cwd(), initial_file: str | None=None,
18
+ filetypes: list[tuple[str, str]] | None=None, multiple: bool=False) -> None:
19
+ super().__init__(title, initial_dir, initial_file, filetypes, multiple)
20
+ self.blocking = False
21
+
22
+ def _dispatch_event(self, files):
23
+ self.dispatch_event("on_dialog_open", files)
24
+
25
+ def open(self):
26
+ """Initial_file is not supported on Mac."""
27
+ with AutoReleasePool():
28
+ url = Path(self.initial_dir).as_posix()
29
+ nsurl_path = NSURL.fileURLWithPath_(pystr_to_ns(url))
30
+ panel = NSOpenPanel.openPanel()
31
+ panel.setAllowsMultipleSelection_(self.multiple)
32
+ panel.setDirectoryURL_(nsurl_path)
33
+ panel.setMessage_(pystr_to_ns(self.title))
34
+
35
+ if self.filetypes:
36
+ allowed = []
37
+ for name, exts in self.filetypes:
38
+ for ext in exts.split(" "):
39
+ strip_endings = ext.lstrip("*").lstrip(".")
40
+ allowed.append(pystr_to_ns(strip_endings)) # mac wants extension without "."
41
+ panel.setAllowedFileTypes_(py_to_ns(allowed))
42
+
43
+ if self.blocking:
44
+ result = panel.runModal()
45
+
46
+ if result == 1:
47
+ files = [nsstr_to_py(_url.path()) for _url in ns_to_py(panel.URLs())]
48
+ else:
49
+ files = []
50
+ self._dispatch_event(files)
51
+ else:
52
+ if MacOSFileOpenDialog._cb_block is not None:
53
+ raise Exception("An existing dialog is already open.")
54
+
55
+ def handler(_result: int):
56
+ try:
57
+ if _result == 1:
58
+ files = [nsstr_to_py(_url.path()) for _url in ns_to_py(panel.URLs())]
59
+ else:
60
+ files = []
61
+ self._dispatch_event(files)
62
+ finally:
63
+ MacOSFileOpenDialog._cb_block = None
64
+
65
+ MacOSFileOpenDialog._cb_block = get_callback_block(handler, encoding=[b"v", b"l"])
66
+ panel.beginWithCompletionHandler_(MacOSFileOpenDialog._cb_block)
67
+
68
+
69
+ class MacOSFileSaveDialog(FileSaveDialogBase):
70
+ _cb_block = None
71
+ def __init__(self, title="Save As", initial_dir=Path.cwd(), initial_file=None, filetypes=None, default_ext=""):
72
+ super().__init__(title, initial_dir, initial_file, filetypes, default_ext)
73
+ self.blocking = False
74
+
75
+ def open(self):
76
+ """Initial_file is not supported on Mac."""
77
+ with AutoReleasePool():
78
+ url = Path(self.initial_dir).as_posix()
79
+ nsurl_path = NSURL.fileURLWithPath_(pystr_to_ns(url))
80
+ panel = NSSavePanel.savePanel()
81
+ panel.setDirectoryURL_(nsurl_path)
82
+ panel.setMessage_(pystr_to_ns(self.title))
83
+ if self.initial_file:
84
+ panel.setNameFieldStringValue_(pystr_to_ns(self.initial_file))
85
+
86
+ if self.filetypes:
87
+ allowed = []
88
+ for name, exts in self.filetypes:
89
+ for ext in exts.split(" "):
90
+ strip_endings = ext.lstrip("*").lstrip(".")
91
+ allowed.append(pystr_to_ns(strip_endings)) # mac wants extension without "."
92
+ panel.setAllowedFileTypes_(py_to_ns(allowed))
93
+ else:
94
+ panel.setAllowedFileTypes_(py_to_ns([pystr_to_ns(self.default_ext)]))
95
+ panel.setAllowsOtherFileTypes_(True)
96
+ panel.setShowsContentTypes_(True)
97
+ if self.blocking:
98
+ result = panel.runModal()
99
+
100
+ if result == 1:
101
+ self._dispatch_event(nsstr_to_py(panel.URL().path()))
102
+ else: # cancelled
103
+ self._dispatch_event("")
104
+ else:
105
+ if MacOSFileSaveDialog._cb_block is not None:
106
+ raise Exception("An existing dialog is already open.")
107
+
108
+ def handler(_result: int):
109
+ try:
110
+ if _result == 1:
111
+ self._dispatch_event(nsstr_to_py(panel.URL().path()))
112
+ else: # cancelled
113
+ self._dispatch_event([])
114
+ finally:
115
+ MacOSFileSaveDialog._cb_block = None
116
+
117
+ MacOSFileSaveDialog._cb_block = get_callback_block(handler, encoding=[b"v", b"l"])
118
+ panel.beginWithCompletionHandler_(MacOSFileSaveDialog._cb_block)
119
+
120
+ def _dispatch_event(self, filename: str):
121
+ self.dispatch_event("on_dialog_save", filename)
@@ -0,0 +1,72 @@
1
+ """Loads a file dialog utilizing TKinter.
2
+
3
+ Linux does not have any unified dialog API or utility across distros.
4
+
5
+ While many support a GTK or KDialog, it's not guaranteed. Because of this, TKinter is utilized
6
+ as the main caller. When Tkinter cannot create a system dialog, it will create a custom
7
+ created file explorer, thus guaranteeing a dialog on any Linux system.
8
+
9
+ This is done in a separate process as Tk has its own event loop.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from concurrent.futures import ProcessPoolExecutor as _ProcessPoolExecutor
14
+ from pathlib import Path
15
+
16
+ import pyglet
17
+ from pyglet.window.dialog.base import FileOpenDialogBase, FileSaveDialogBase
18
+
19
+
20
+ class _TkFileDialogBackend:
21
+
22
+ executor = _ProcessPoolExecutor(max_workers=1)
23
+ _dialog = None
24
+
25
+ @staticmethod
26
+ def _open_dialog(dialog):
27
+ import tkinter as tk
28
+ root = tk.Tk()
29
+ root.withdraw()
30
+ try:
31
+ return dialog.show()
32
+ finally:
33
+ root.destroy()
34
+
35
+ def open(self):
36
+ future = self.executor.submit(self._open_dialog, self._dialog)
37
+ future.add_done_callback(self._dispatch_event)
38
+
39
+ def _dispatch_event(self, future):
40
+ raise NotImplementedError
41
+
42
+ class TkFileOpenDialog(_TkFileDialogBackend, FileOpenDialogBase):
43
+ def __init__(
44
+ self, title: str="Open File", initial_dir: str | Path=Path.cwd(), initial_file: str | None=None,
45
+ filetypes: list[tuple[str, str]] | None=None, multiple: bool=False) -> None:
46
+ super().__init__(title, initial_dir, initial_file, filetypes, multiple)
47
+ from tkinter import filedialog
48
+ self._dialog = filedialog.Open(title=title,
49
+ initialdir=initial_dir,
50
+ initialfile=initial_file,
51
+ filetypes=filetypes or (),
52
+ multiple=multiple)
53
+
54
+ def _dispatch_event(self, future):
55
+ pyglet.app.platform_event_loop.post_event(self, "on_dialog_open", future.result())
56
+
57
+
58
+ class TkFileSaveDialog(_TkFileDialogBackend, FileSaveDialogBase):
59
+ def __init__(self, title="Save As", initial_dir=Path.cwd(), initial_file=None, filetypes=None, default_ext=""):
60
+ super().__init__(title, initial_dir, initial_file, filetypes, default_ext)
61
+ from tkinter import filedialog
62
+
63
+ self._dialog = filedialog.SaveAs(
64
+ title=title,
65
+ initialdir=initial_dir,
66
+ initialfile=initial_file or (),
67
+ filetypes=filetypes or (),
68
+ defaultextension=default_ext,
69
+ )
70
+
71
+ def _dispatch_event(self, future):
72
+ pyglet.app.platform_event_loop.post_event(self, "on_dialog_save", future.result())
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import threading
5
+ from pathlib import Path
6
+
7
+ from _ctypes import sizeof
8
+ from ctypes import create_unicode_buffer
9
+
10
+ import pyglet
11
+ from pyglet.libs.win32 import OPENFILENAMEW, _comdlg32
12
+ from pyglet.libs.win32.constants import (
13
+ OFN_EXPLORER,
14
+ OFN_PATHMUSTEXIST,
15
+ OFN_OVERWRITEPROMPT,
16
+ OFN_FILEMUSTEXIST,
17
+ OFN_ALLOWMULTISELECT,
18
+ )
19
+ from pyglet.window.dialog.base import FileOpenDialogBase, FileSaveDialogBase
20
+
21
+
22
+ def _build_filter_string(filetypes: list[tuple[str, str]]) -> str:
23
+ """Create filter string for filetypes.
24
+
25
+ Args:
26
+ filetypes:
27
+ e.g. [("PNG", "*.png"), ("24-bit Bitmap", "*.bmp"), ("Images", "*.png;*.bmp"]
28
+ """
29
+ parts: list[str] = []
30
+
31
+ for label, exts in filetypes:
32
+ # Normalize to a list
33
+ if isinstance(exts, str):
34
+ exts = [exts]
35
+
36
+ norm_patterns: list[str] = []
37
+ for ext in exts:
38
+ ext = ext.strip()
39
+ if not ext:
40
+ continue
41
+ if ext.startswith("*."):
42
+ pass # already "*.png"
43
+ elif ext.startswith("."): # Just incase, add * if someone didn't add.
44
+ ext = "*" + ext # ".png" -> "*.png"
45
+ else:
46
+ ext = "*." + ext # "png" -> "*.png"
47
+ norm_patterns.append(ext)
48
+
49
+ if not norm_patterns:
50
+ pattern_str = "*.*"
51
+ else:
52
+ pattern_str = ";".join(norm_patterns)
53
+
54
+ # Add pattern to the label.
55
+ display_label = f"{label} ({pattern_str})"
56
+
57
+ parts.append(display_label)
58
+ parts.append(pattern_str)
59
+
60
+ # Join pairs with single nulls and end with double null
61
+ return "\0".join(parts) + "\0\0"
62
+
63
+
64
+ def _parse_multiselect(buffer) -> list[str]:
65
+ text = buffer[:]
66
+
67
+ # Split on nulls
68
+ parts = text.split("\0")
69
+
70
+ # Remove empty entries
71
+ parts = [p for p in parts if p]
72
+
73
+ if not parts:
74
+ return []
75
+
76
+ # If only 1 part → it's a single file
77
+ if len(parts) == 1:
78
+ return [parts[0]]
79
+
80
+ # otherwise:
81
+ folder = parts[0]
82
+ files = parts[1:]
83
+
84
+ return [folder + "\\" + f for f in files]
85
+
86
+
87
+ def _build_ofn(hwnd, title, initialdir, initial_file, filetypes, multiple, for_save=False, buffer_chars=65536, default_ext=None,
88
+ ):
89
+ file_buffer = create_unicode_buffer(buffer_chars)
90
+
91
+ if initial_file:
92
+ file_buffer.value = initial_file
93
+
94
+ filter_str = _build_filter_string(filetypes) if filetypes else None
95
+
96
+ ofn = OPENFILENAMEW()
97
+ ofn.lStructSize = sizeof(ofn)
98
+ ofn.hwndOwner = hwnd
99
+ ofn.lpstrFilter = filter_str
100
+ ofn.nFilterIndex = 1
101
+
102
+ ofn.lpstrFile = ctypes.cast(file_buffer, ctypes.c_wchar_p)
103
+ ofn.nMaxFile = buffer_chars
104
+
105
+ # Optional fields
106
+ ofn.lpstrFileTitle = None
107
+ ofn.nMaxFileTitle = 0
108
+ if isinstance(initialdir, Path):
109
+ initialdir = initialdir.absolute()
110
+ ofn.lpstrInitialDir = str(initialdir)
111
+ ofn.lpstrTitle = title
112
+
113
+ # Base flags
114
+ flags = OFN_EXPLORER
115
+
116
+ if for_save:
117
+ flags |= OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT
118
+ else:
119
+ flags |= OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST
120
+
121
+ if multiple and not for_save:
122
+ flags |= OFN_ALLOWMULTISELECT
123
+
124
+ ofn.Flags = flags
125
+
126
+ # Default extension for save dialog (optional)
127
+ if for_save and default_ext:
128
+ # strip leading dot
129
+ if default_ext.startswith("."):
130
+ default_ext = default_ext[1:]
131
+ ofn.lpstrDefExt = default_ext
132
+
133
+ return ofn, file_buffer
134
+
135
+
136
+ class WindowsFileOpenDialog(FileOpenDialogBase):
137
+
138
+ def _open_dialog(self) -> list[str] | None:
139
+ ofn, file_buffer = _build_ofn(
140
+ hwnd=None,
141
+ title=self.title,
142
+ initialdir=self.initial_dir,
143
+ initial_file=self.initial_file,
144
+ filetypes=self.filetypes,
145
+ multiple=self.multiple,
146
+ for_save=False,
147
+ buffer_chars=65536,
148
+ )
149
+
150
+ if not _comdlg32.GetOpenFileNameW(ctypes.byref(ofn)):
151
+ return None
152
+
153
+ if self.multiple:
154
+ return _parse_multiselect(file_buffer.value)
155
+
156
+ return file_buffer.value
157
+
158
+ def open(self) -> None:
159
+ def run() -> None:
160
+ result = self._open_dialog()
161
+ if result is None:
162
+ result = []
163
+ assert isinstance(result, list)
164
+ pyglet.app.platform_event_loop.post_event(self, "on_dialog_open", result)
165
+
166
+ threading.Thread(target=run, daemon=True).start()
167
+
168
+
169
+
170
+ class WindowsFileSaveDialog(FileSaveDialogBase):
171
+ def _open_dialog(self) -> str:
172
+ ofn, file_buffer = _build_ofn(
173
+ hwnd=None,
174
+ title=self.title,
175
+ initialdir=self.initial_dir,
176
+ initial_file=self.initial_file,
177
+ filetypes=self.filetypes,
178
+ multiple=False,
179
+ for_save=True,
180
+ buffer_chars=65536,
181
+ default_ext=self.default_ext,
182
+ )
183
+
184
+ if not _comdlg32.GetSaveFileNameW(ctypes.byref(ofn)):
185
+ return ""
186
+
187
+ return file_buffer.value
188
+
189
+ def open(self) -> None:
190
+ def run() -> None:
191
+ result = self._open_dialog()
192
+ pyglet.app.platform_event_loop.post_event(self, "on_dialog_save", result)
193
+
194
+ threading.Thread(target=run, daemon=True).start()