Skip to content

Commit 36fc09c

Browse files
committed
release 0.10.4a2
1 parent 32e5bb2 commit 36fc09c

15 files changed

+486
-141
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ There are currently five basic ways to use py5. They are:
5252

5353
* **module mode**: create a sketch with `setup()` and `draw()` functions that call methods provided by the `py5` library. The above example is created in module mode.
5454
* **class mode**: create a Python class inherited from `py5.Sketch`. This mode supports multiple Sketches running at the same time.
55-
* **imported mode**: simplified code that omits the `py5.` prefix. This mode is supported by the py5 Jupyter notebook kernel and the `run_sketch` command line utility.
56-
* **static mode**: functionless code to create static images. This mode is supported by the py5bot Jupyter notebook kernel, the `%%py5bot` IPython magic, and the `run_sketch` command line utility.
55+
* **imported mode**: simplified code that omits the `py5.` prefix. This mode is supported by the py5 Jupyter notebook kernel and the `py5-run-sketch` command line utility.
56+
* **static mode**: functionless code to create static images. This mode is supported by the py5bot Jupyter notebook kernel, the `%%py5bot` IPython magic, and the `py5-run-sketch` command line utility.
5757
* **processing mode**: make calls to Python from a Processing (Java) Sketch. This mode enables py5 to function as bridge, connecting the Python and Java ecosystems through a new `callPython()` method.
5858

5959
## Source Code

py5/__init__.py

+84-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import inspect
2727
import os
28+
import platform
2829
import sys
2930
import warnings
3031
from io import BytesIO
@@ -35,16 +36,64 @@
3536
import numpy as np # noqa
3637
import numpy.typing as npt # noqa
3738
import py5_tools
39+
import py5_tools.environ # noqa
3840
from jpype import JClass # noqa
3941
from jpype.types import JArray, JChar, JFloat, JInt, JString # noqa
4042
from PIL import Image # noqa
4143

44+
_environ = py5_tools.environ.Environment()
45+
4246
if not py5_tools.is_jvm_running():
4347
base_path = (
4448
Path(getattr(sys, "_MEIPASS")) / "py5"
4549
if hasattr(sys, "_MEIPASS")
4650
else Path(__file__).absolute().parent
4751
)
52+
53+
if platform.system() == "Darwin":
54+
# Make sure Python appears on the MacOS Dock
55+
# This is necessary, otherwise MacOS will not like to let JAVA2D Sketches get focus
56+
try:
57+
from AppKit import (
58+
NSURL,
59+
NSApplication,
60+
NSApplicationActivationPolicyRegular,
61+
NSImage,
62+
)
63+
64+
# this adds a white square to the dock
65+
app = NSApplication.sharedApplication()
66+
app.setActivationPolicy_(NSApplicationActivationPolicyRegular)
67+
68+
# set the dock icon to the py5 logo
69+
icon_path = base_path.parent / "py5_tools/resources/logo.icns"
70+
icon_url = NSURL.fileURLWithPath_(str(icon_path))
71+
icon_image = NSImage.alloc().initWithContentsOfURL_(icon_url)
72+
app.setApplicationIconImage_(icon_image)
73+
74+
# cleanup
75+
del app, icon_path, icon_url, icon_image
76+
del NSURL, NSApplication, NSApplicationActivationPolicyRegular, NSImage
77+
except:
78+
pass
79+
80+
if platform.system() == "Windows":
81+
# This code is here so that later win32gui code works correctly. The
82+
# `focus_window(handle)` method in `Py5Bridge` is used to move Sketch
83+
# windows to the foreground
84+
try:
85+
from win32com import client as win32com_client
86+
87+
shell = win32com_client.Dispatch("WScript.Shell")
88+
89+
# send the most benign key possible. this can't possibly do anything
90+
shell.SendKeys(chr(0))
91+
92+
# cleanup
93+
del win32com_client, shell
94+
except:
95+
pass
96+
4897
# add py5 jars to the classpath first
4998
py5_tools.add_jars(str(base_path / "jars"))
5099
# if the cwd has a jars subdirectory, add that next
@@ -114,7 +163,7 @@
114163
pass
115164

116165

117-
__version__ = "0.10.3a1"
166+
__version__ = "0.10.4a2"
118167

119168
_PY5_USE_IMPORTED_MODE = py5_tools.get_imported_mode()
120169
py5_tools._lock_imported_mode()
@@ -8019,6 +8068,32 @@ def image_mode(mode: int, /) -> None:
80198068
return _py5sketch.image_mode(mode)
80208069

80218070

8071+
def intercept_escape() -> None:
8072+
"""Prevent the Escape key from causing the Sketch to exit.
8073+
8074+
Notes
8075+
-----
8076+
8077+
Prevent the Escape key from causing the Sketch to exit. Normally hitting the
8078+
Escape key (`ESC`) will cause the Sketch to exit. In Processing, one can write
8079+
code to change the Escape key's behavior by changing the `key` value to
8080+
something else, perhaps with code similar to `py5.key = 'x'`. That code won't
8081+
work in py5 because py5 does not allow the user to alter the value of `key` like
8082+
Processing does. The `intercept_escape()` method was created to allow users to
8083+
achieve the same goal of preventing the Escape key from causing the Sketch to
8084+
exit.
8085+
8086+
The `intercept_escape()` method will only do something when `key` already equals
8087+
`ESC`. This function should only be called from the user event functions
8088+
`key_pressed()`, `key_typed()`, and `key_released()`.
8089+
8090+
This method will not alter the value of `key`. This method cannot prevent a
8091+
Sketch from exiting when the exit is triggered by any other means, such as a
8092+
call to `exit_sketch()` or the user closes the window.
8093+
"""
8094+
return _py5sketch.intercept_escape()
8095+
8096+
80228097
@overload
80238098
def lerp_color(c1: int, c2: int, amt: float, /) -> int:
80248099
"""Calculates a color between two colors at a specific increment.
@@ -23104,3 +23179,11 @@ def _prepare_dynamic_variables(caller_locals, caller_globals):
2310423179

2310523180

2310623181
_prepare_dynamic_variables(locals(), globals())
23182+
23183+
23184+
if platform.system() == "Darwin" and _environ.in_ipython_session:
23185+
if _environ.ipython_shell.active_eventloop != "osx":
23186+
print(
23187+
"Importing py5 on macOS but the necessary Jupyter macOS event loop has not been activated. I'll activate it for you, but next time, execute `%gui osx` before importing this library."
23188+
)
23189+
_environ.ipython_shell.run_line_magic("gui", "osx")

py5/bridge.py

+21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#
1919
# *****************************************************************************
2020
import inspect
21+
import platform
2122
import re
2223
import sys
2324
import traceback
@@ -367,6 +368,26 @@ def call_function(self, key, params):
367368
def py5_println(self, text, stderr):
368369
self._sketch.println(text, stderr=stderr)
369370

371+
@JOverride
372+
def focus_window(self, handle):
373+
try:
374+
if platform.system() == "Windows":
375+
import win32gui
376+
377+
# win32gui.ShowWindow(hwnd, 5)
378+
win32gui.SetForegroundWindow(handle)
379+
380+
elif platform.system() == "Darwin":
381+
# not yet implemented
382+
pass
383+
384+
elif platform.system() == "Linux":
385+
# not yet implemented
386+
pass
387+
388+
except:
389+
pass
390+
370391
@JOverride
371392
def shutdown(self):
372393
try:

py5/graphics.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -1783,15 +1783,15 @@ def color(self, *args) -> int:
17831783
WHITESPACE = " \t\n\r\f\u00a0"
17841784

17851785
def _get_height(self) -> int:
1786-
"""System variable that stores the height of the Py5Graphics drawing surface.
1786+
"""Variable that stores the height of the Py5Graphics drawing surface.
17871787

17881788
Underlying Processing field: PGraphics.height
17891789

17901790
Notes
17911791
-----
17921792

1793-
System variable that stores the height of the Py5Graphics drawing surface. This
1794-
value is set when creating the `Py5Graphics` object with the `create_graphics()`
1793+
Variable that stores the height of the Py5Graphics drawing surface. This value
1794+
is set when creating the `Py5Graphics` object with the `create_graphics()`
17951795
method. For example, `create_graphics(320, 240)` sets the `height` variable to
17961796
the value 240.
17971797

@@ -1802,15 +1802,15 @@ def _get_height(self) -> int:
18021802

18031803
height: int = property(
18041804
fget=_get_height,
1805-
doc="""System variable that stores the height of the Py5Graphics drawing surface.
1805+
doc="""Variable that stores the height of the Py5Graphics drawing surface.
18061806

18071807
Underlying Processing field: PGraphics.height
18081808

18091809
Notes
18101810
-----
18111811

1812-
System variable that stores the height of the Py5Graphics drawing surface. This
1813-
value is set when creating the `Py5Graphics` object with the `create_graphics()`
1812+
Variable that stores the height of the Py5Graphics drawing surface. This value
1813+
is set when creating the `Py5Graphics` object with the `create_graphics()`
18141814
method. For example, `create_graphics(320, 240)` sets the `height` variable to
18151815
the value 240.
18161816

@@ -1963,17 +1963,17 @@ def _get_pixel_width(self) -> int:
19631963
)
19641964

19651965
def _get_width(self) -> int:
1966-
"""System variable that stores the width of the Py5Graphics drawing surface.
1966+
"""Variable that stores the width of the Py5Graphics drawing surface.
19671967

19681968
Underlying Processing field: PGraphics.width
19691969

19701970
Notes
19711971
-----
19721972

1973-
System variable that stores the width of the Py5Graphics drawing surface. This
1974-
value is set when creating the `Py5Graphics` object with the `create_graphics()`
1975-
method. For example, `create_graphics(320, 240)` sets the `width` variable to
1976-
the value 320.
1973+
Variable that stores the width of the Py5Graphics drawing surface. This value is
1974+
set when creating the `Py5Graphics` object with the `create_graphics()` method.
1975+
For example, `create_graphics(320, 240)` sets the `width` variable to the value
1976+
320.
19771977

19781978
This field is the same as `width` but linked to a `Py5Graphics` object. To see
19791979
example code for how it can be used, see `width`.
@@ -1982,17 +1982,17 @@ def _get_width(self) -> int:
19821982

19831983
width: int = property(
19841984
fget=_get_width,
1985-
doc="""System variable that stores the width of the Py5Graphics drawing surface.
1985+
doc="""Variable that stores the width of the Py5Graphics drawing surface.
19861986

19871987
Underlying Processing field: PGraphics.width
19881988

19891989
Notes
19901990
-----
19911991

1992-
System variable that stores the width of the Py5Graphics drawing surface. This
1993-
value is set when creating the `Py5Graphics` object with the `create_graphics()`
1994-
method. For example, `create_graphics(320, 240)` sets the `width` variable to
1995-
the value 320.
1992+
Variable that stores the width of the Py5Graphics drawing surface. This value is
1993+
set when creating the `Py5Graphics` object with the `create_graphics()` method.
1994+
For example, `create_graphics(320, 240)` sets the `width` variable to the value
1995+
320.
19961996

19971997
This field is the same as `width` but linked to a `Py5Graphics` object. To see
19981998
example code for how it can be used, see `width`.""",

py5/jars/py5.jar

613 Bytes
Binary file not shown.

py5/macos_problem.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# *****************************************************************************
2+
#
3+
# Part of the py5 library
4+
# Copyright (C) 2020-2024 Jim Schmitz
5+
#
6+
# This library is free software: you can redistribute it and/or modify it
7+
# under the terms of the GNU Lesser General Public License as published by
8+
# the Free Software Foundation, either version 2.1 of the License, or (at
9+
# your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful, but
12+
# WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14+
# General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this library. If not, see <https://www.gnu.org/licenses/>.
18+
#
19+
# *****************************************************************************
20+
import functools
21+
import platform
22+
23+
from py5_tools.environ import Environment
24+
25+
_environ = Environment()
26+
27+
_enforce_safety_check = (
28+
platform.system() == "Darwin"
29+
and (
30+
platform.processor() == "i386"
31+
or int(platform.mac_ver()[0].split(".", 1)[0]) < 14
32+
)
33+
and _environ.in_ipython_session
34+
)
35+
_first_renderer_opengl = None
36+
37+
38+
def disable_safety_check():
39+
global _enforce_safety_check
40+
_enforce_safety_check = False
41+
42+
43+
def enable_safety_check():
44+
global _enforce_safety_check
45+
_enforce_safety_check = True
46+
47+
48+
OPENGL_RENDERERS = [
49+
"processing.opengl.PGraphics2D",
50+
"processing.opengl.PGraphics3D",
51+
]
52+
53+
MESSAGE = """Sorry, but you can't use an OpenGL renderer in your Sketch right now. Doing so might cause Python to crash.
54+
55+
Here's the problem: On macOS machines with Intel CPUs and/or older macOS versions, this version of py5 seems to crash when you use an OpenGL renderer in an IPython or Jupyter session if the first Sketch run in that Python session used the default (JAVA2D) renderer. Sorry if that sounds crazy. This is an unfortunate side effect of an important code change that significantly improved py5 for all macOS users.
56+
57+
The root issue is somewhere in native macOS code that Processing and py5 both depend on. Hopefully in the future we will find a real fix or a better workaround.
58+
59+
You are seeing this message because this version of py5 has a safety feature that detects the sequence of events that might lead to this crash. However, if you'd like to disable this safety feature (and risk Python crashing), use the following code:
60+
61+
from py5 import macos_problem
62+
63+
macos_problem.disable_safety_check()
64+
65+
But before doing that, it would be great if you could do a quick test for us. Please run the following code in a new Jupyter Notebook or IPython REPL, with each line of code executed separately:
66+
67+
from py5 import macos_problem, test
68+
69+
macos_problem.disable_safety_check()
70+
71+
# run a Sketch with the default renderer
72+
test.test_java2d()
73+
74+
# run a Sketch with an opengl renderer
75+
# does this cause a crash???
76+
test.test_p2d()
77+
78+
Then report your findings to the below GitHub issue thread. Include your macOS version and CPU type. (For your convenience, this information will be displayed at the end of this message.) Your feedback will help us understand the problem better and more accurately calibrate this crash protection feature.
79+
80+
https://github.com/py5coding/py5generator/issues/578
81+
82+
If the above test code doesn't cause Python to crash on your machine, great! You can keep using that `disable_safety_check()` function so you never see this warning again. But please take the time report your findings to the GitHub issue thread. The next version of py5 will incorporate your feedback and the safety feature will be adjusted accordingly.
83+
84+
If the above test code does cause Python to crash on your machine, it's OK. If you really need to mix Java2D and OpenGL renderers together in one Python session, you just need to make sure that the first executed Sketch is always an OpenGL Sketch. For convenience, you can use the following code to open a quick Sketch right after importing py5. This will ensure the first Sketch is always an OpenGL Sketch, eliminating the problem (and this warning) entirely:
85+
86+
import py5
87+
from py5 import test
88+
89+
test.test_p2d()
90+
91+
If you'd like to read about our progress understanding this issue, please visit the above GitHub issue thread.
92+
93+
Sorry again for the weird limitation. We're doing our best to make py5 as stable as possible. This safety feature is here because we don't want users to become upset because Python crashed for confusing reasons. Thank you for your understanding.
94+
"""
95+
96+
97+
def _macos_safety_check(f):
98+
@functools.wraps(f)
99+
def decorated(self_, *args):
100+
global _first_renderer_opengl
101+
if _enforce_safety_check:
102+
if _first_renderer_opengl is None:
103+
# This is the first Sketch. Record if the renderer is OpenGL.
104+
if len(args) >= 3 and args[2] in OPENGL_RENDERERS:
105+
_first_renderer_opengl = True
106+
else:
107+
_first_renderer_opengl = False
108+
109+
elif _first_renderer_opengl is False:
110+
# The first Sketch was not OpenGL. OpenGL is not allowed now.
111+
if len(args) >= 3 and args[2] in OPENGL_RENDERERS:
112+
self_.println(MESSAGE)
113+
if platform.system() == "Darwin": # just in case
114+
self_.println("macOS version:", platform.mac_ver()[0])
115+
self_.println("macOS CPU type:", platform.processor())
116+
raise RuntimeError(
117+
"Halting Sketch startup to prevent Python from crashing"
118+
)
119+
120+
f(self_, *args)
121+
122+
return decorated

py5/reference.py

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
(('Sketch', 'hue'), ['(rgb: int, /) -> float']),
133133
(('Sketch', 'image'), ['(img: Py5Image, a: float, b: float, /) -> None', '(img: Py5Image, a: float, b: float, c: float, d: float, /) -> None', '(img: Py5Image, a: float, b: float, c: float, d: float, u1: int, v1: int, u2: int, v2: int, /) -> None']),
134134
(('Sketch', 'image_mode'), ['(mode: int, /) -> None']),
135+
(('Sketch', 'intercept_escape'), ['() -> None']),
135136
(('Sketch', 'lerp_color'), ['(c1: int, c2: int, amt: float, /) -> int', '(c1: int, c2: int, amt: float, mode: int, /) -> int']),
136137
(('Sketch', 'light_falloff'), ['(constant: float, linear: float, quadratic: float, /) -> None']),
137138
(('Sketch', 'light_specular'), ['(v1: float, v2: float, v3: float, /) -> None']),

0 commit comments

Comments
 (0)