├── thonnycontrib ├── thonny-py5mode │ ├── _version.py │ ├── py5colorpicker │ │ └── tkcolorpicker │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── limitvar.py │ │ │ ├── functions.py │ │ │ ├── gradientbar.py │ │ │ ├── alphabar.py │ │ │ ├── spinbox.py │ │ │ ├── colorsquare.py │ │ │ └── colorpicker.py │ ├── about_plugin.py │ ├── __init__.py │ └── install_jdk.py ├── kyanite_theme_ui │ └── __init__.py ├── kyanite_theme_syntax │ └── __init__.py └── backend │ └── py5_imported_mode_backend.py ├── setup.py ├── screenshots ├── 00-header.png ├── 02-start-splash.png ├── 04.02-download-jdk.png ├── 06.02-running-sketch.png ├── 03.01-manage-plug-ins.png ├── 03.02-install-plug-in.png ├── 03.02-install-plug-in2.png ├── 04.03-download-jdk-done.png ├── 06.01-imported-activated.png ├── 04.01-activate-imported-mode.png └── 05-apply-recommended-settings.png ├── Makefile ├── assets └── py5_quick_reference.pdf ├── LICENSE ├── pyproject.toml ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /thonnycontrib/thonny-py5mode/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.5.0rc4" 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /screenshots/00-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/00-header.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build_plugin 2 | 3 | build_plugin: 4 | rm -rf dist 5 | hatch build 6 | 7 | clean: 8 | rm -rf dist 9 | -------------------------------------------------------------------------------- /assets/py5_quick_reference.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/assets/py5_quick_reference.pdf -------------------------------------------------------------------------------- /screenshots/02-start-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/02-start-splash.png -------------------------------------------------------------------------------- /screenshots/04.02-download-jdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/04.02-download-jdk.png -------------------------------------------------------------------------------- /screenshots/06.02-running-sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/06.02-running-sketch.png -------------------------------------------------------------------------------- /screenshots/03.01-manage-plug-ins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/03.01-manage-plug-ins.png -------------------------------------------------------------------------------- /screenshots/03.02-install-plug-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/03.02-install-plug-in.png -------------------------------------------------------------------------------- /screenshots/03.02-install-plug-in2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/03.02-install-plug-in2.png -------------------------------------------------------------------------------- /screenshots/04.03-download-jdk-done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/04.03-download-jdk-done.png -------------------------------------------------------------------------------- /screenshots/06.01-imported-activated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/06.01-imported-activated.png -------------------------------------------------------------------------------- /screenshots/04.01-activate-imported-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/04.01-activate-imported-mode.png -------------------------------------------------------------------------------- /screenshots/05-apply-recommended-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/py5coding/thonny-py5mode/HEAD/screenshots/05-apply-recommended-settings.png -------------------------------------------------------------------------------- /thonnycontrib/kyanite_theme_ui/__init__.py: -------------------------------------------------------------------------------- 1 | '''kyanite ui theme 2 | theme inspired by processing 4.0b3 default theme, kyanite 3 | ''' 4 | 5 | from thonny import get_workbench 6 | from thonny.plugins.clean_ui_themes import clean 7 | 8 | 9 | def load_plugin() -> None: 10 | get_workbench().add_ui_theme( 11 | 'Kyanite UI', 12 | 'Clean Sepia', 13 | clean( 14 | frame_background='#6BA0C7', 15 | text_background='#FFFFF2', 16 | normal_detail='#C4E9FF', 17 | high_detail='#B4D9EF', 18 | low_detail='#A4C9DF', 19 | normal_foreground='#002233', 20 | high_foreground='#002233', 21 | low_foreground='#000066', 22 | custom_menubar=0, 23 | ), 24 | ) 25 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | """ 19 | 20 | 21 | from .colorpicker import ColorPicker, askcolor, modeless_colorpicker 22 | from .alphabar import AlphaBar 23 | from .gradientbar import GradientBar 24 | from .colorsquare import ColorSquare 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021-2025 Tristan Bunn 2 | Copyright 2025 py5coding organization 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "thonny-py5mode" 7 | dynamic = ["version"] 8 | description = "py5 mode plugin for Thonny" 9 | readme = "README.md" 10 | license = "MIT" 11 | requires-python = ">=3.10" 12 | authors = [ 13 | { name = "tabreturn", email = "thonny-py5mode@tabreturn.com" }, 14 | { name = "Jim Schmitz", email = "jim@ixora.io" }, 15 | { name = "Alexandre Villares", email = "abav@lugaralgum.com" }, 16 | ] 17 | classifiers = [ 18 | "Environment :: Plugins", 19 | "Topic :: Multimedia :: Graphics", 20 | "Topic :: Text Editors :: Integrated Development Environments (IDE)", 21 | ] 22 | dependencies = ["install-jdk>=1.1.0", "py5>=0.10.7a0", "pyperclip>=1.9.0"] 23 | 24 | [project.optional-dependencies] 25 | extras = ["py5[extras]>=0.10.7a0"] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/py5coding/thonny-py5mode" 29 | 30 | [tool.hatch.version] 31 | path = "thonnycontrib/thonny-py5mode/_version.py" 32 | 33 | [tool.hatch.build.targets.wheel] 34 | packages = ["/thonnycontrib"] 35 | 36 | [tool.hatch.build.targets.sdist] 37 | include = ["/thonnycontrib"] 38 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Example 20 | """ 21 | 22 | 23 | from .functions import tk, ttk 24 | from . import askcolor 25 | 26 | 27 | def select_color1(): 28 | print(askcolor(color="sky blue", parent=root)) 29 | 30 | 31 | def select_color2(): 32 | print(askcolor(color=(255, 120, 0, 100), parent=root, alpha=True)) 33 | 34 | 35 | root = tk.Tk() 36 | s = ttk.Style(root) 37 | s.theme_use('clam') 38 | ttk.Label(root, text='Color Selection:').pack(padx=4, pady=4) 39 | ttk.Button(root, text='solid color', 40 | command=select_color1).pack(fill='x', padx=4, pady=4) 41 | ttk.Button(root, text='with alpha channel', 42 | command=select_color2).pack(fill='x', padx=4, pady=4) 43 | root.mainloop() 44 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/limitvar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Limited StringVar 20 | """ 21 | 22 | 23 | from .functions import tk 24 | 25 | 26 | class LimitVar(tk.StringVar): 27 | def __init__(self, from_, to, master=None, value=None, name=None): 28 | tk.StringVar.__init__(self, master, value, name) 29 | try: 30 | self._from = int(from_) 31 | self._to = int(to) 32 | except ValueError: 33 | raise ValueError("from_ and to should be integers.") 34 | if self._from >= self._to: 35 | raise ValueError("from_ should be smaller than to.") 36 | # ensure that the initial value is valid 37 | val = self.get() 38 | self.set(val) 39 | 40 | def get(self): 41 | """ 42 | Convert the content to int between the limits of the variable. 43 | 44 | If the content is not an integer between the limits, the value is 45 | corrected and the corrected result is returned. 46 | """ 47 | val = tk.StringVar.get(self) 48 | try: 49 | val = int(val) 50 | if val < self._from: 51 | val = self._from 52 | self.set(val) 53 | elif val > self._to: 54 | val = self._to 55 | self.set(val) 56 | except ValueError: 57 | val = 0 58 | self.set(0) 59 | return val 60 | -------------------------------------------------------------------------------- /thonnycontrib/kyanite_theme_syntax/__init__.py: -------------------------------------------------------------------------------- 1 | '''kyanite syntax theme 2 | theme inspired by processing 4.0b3 default theme, kyanite 3 | ''' 4 | 5 | from thonny import get_workbench, workbench 6 | 7 | 8 | def kyanite_syntax() -> workbench.SyntaxThemeSettings: 9 | '''based on default_light (see thonny > plugins > base_syntax_themes)''' 10 | default_fg = '#111155' 11 | default_bg = '#FFFFF2' 12 | light_fg = '#94A4AF' 13 | string_fg = '#7D4793' 14 | open_string_bg = '#E2E7E1' 15 | gutter_foreground = '#A4B4BF' 16 | gutter_background = '#E2E7E1' 17 | 18 | return { 19 | 'TEXT': { 20 | 'foreground': default_fg, 21 | 'insertbackground': default_fg, 22 | 'background': default_bg, 23 | }, 24 | 'GUTTER': { 25 | 'foreground': gutter_foreground, 26 | 'background': gutter_background 27 | }, 28 | 'breakpoint': {'foreground': 'crimson'}, 29 | 'current_line': {'background': '#D9FAFF'}, 30 | 'definition': {'foreground': '#006699', 'font': 'BoldEditorFont'}, 31 | 'string': {'foreground': string_fg}, 32 | 'string3': { 33 | 'foreground': string_fg, 34 | 'background': None, 'font': 'EditorFont' 35 | }, 36 | 'open_string': {'foreground': string_fg, 'background': open_string_bg}, 37 | 'open_string3': { 38 | 'foreground': string_fg, 39 | 'background': open_string_bg, 40 | 'font': 'EditorFont', 41 | }, 42 | 'tab': {'background': '#F5ECD7'}, 43 | 'keyword': {'foreground': '#33997E', 'font': 'EditorFont'}, 44 | 'builtin': {'foreground': '#006699'}, 45 | 'number': {'foreground': '#B04600'}, 46 | 'comment': {'foreground': light_fg}, 47 | 'welcome': {'foreground': light_fg}, 48 | 'magic': {'foreground': light_fg}, 49 | 'prompt': {'foreground': string_fg, 'font': 'BoldEditorFont'}, 50 | 'stdin': {'foreground': 'Blue'}, 51 | 'stdout': {'foreground': 'Black'}, 52 | 'stderr': {'foreground': '#CC0000'}, # same as ANSI red 53 | 'value': {'foreground': 'DarkBlue'}, 54 | 'hyperlink': {'foreground': '#3A66DD', 'underline': True} 55 | } 56 | 57 | 58 | def load_plugin() -> None: 59 | get_workbench().add_syntax_theme( 60 | 'Kyanite Syntax', 61 | 'Default Light', 62 | kyanite_syntax 63 | ) 64 | -------------------------------------------------------------------------------- /thonnycontrib/backend/py5_imported_mode_backend.py: -------------------------------------------------------------------------------- 1 | '''thonny-py5mode backend 2 | interacts with thonny-py5mode frontend (thonny-py5mode > __init__.py) 3 | ''' 4 | 5 | import ast 6 | import os 7 | import pathlib 8 | import sys 9 | from py5_tools import imported 10 | from thonny import get_version 11 | from thonny.common import InlineCommand, InlineResponse 12 | try: # thonny 4 package layout 13 | from thonny import jedi_utils 14 | from thonny.plugins.cpython_backend import ( 15 | get_backend, 16 | MainCPythonBackend 17 | ) 18 | # add plug-in packages to packages path 19 | # https://groups.google.com/g/thonny/c/dhMOGXZHTDU 20 | from thonny import get_sys_path_directory_containg_plugins 21 | sys.path.append(get_sys_path_directory_containg_plugins()) 22 | except ImportError: # thonny 3 package layout 23 | from thonny.plugins.cpython.cpython_backend import ( 24 | get_backend, 25 | MainCPythonBackend 26 | ) 27 | 28 | 29 | def patched_editor_autocomplete( 30 | self: MainCPythonBackend, cmd: InlineCommand) -> InlineResponse: 31 | '''add py5 to autocompletion''' 32 | prefix = 'from py5 import *\n' 33 | cmd['source'] = prefix + cmd['source'] 34 | cmd['row'] += 1 35 | 36 | if int(get_version()[0]) >= 4: # thonny 4 package layout 37 | result = dict( 38 | source=cmd.source, 39 | row=cmd.row, 40 | column=cmd.column, 41 | filename=cmd.filename, 42 | ) 43 | result['completions'] = jedi_utils.get_script_completions( 44 | **result, sys_path=[get_sys_path_directory_containg_plugins()] 45 | ) 46 | else: 47 | result = get_backend()._original_editor_autocomplete(cmd) 48 | 49 | result['row'] -= 1 50 | result['source'] = result['source'][len(prefix):] 51 | return result 52 | 53 | 54 | def load_plugin() -> None: 55 | '''every thonny plug-in uses this function to load''' 56 | if os.environ.get('PY5_IMPORTED_MODE', 'False').lower() == 'false': 57 | return 58 | 59 | # note that _cmd_editor_autocomplete is not a public api 60 | # may need to treat different thonny versions differently 61 | # https://groups.google.com/g/thonny/c/wWCeXWpKy8c 62 | c_e_a = MainCPythonBackend._cmd_editor_autocomplete 63 | MainCPythonBackend._original_editor_autocomplete = c_e_a 64 | MainCPythonBackend._cmd_editor_autocomplete = patched_editor_autocomplete 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Windows 132 | Thumbs.db 133 | desktop.ini 134 | 135 | # OS X 136 | .DS_Store 137 | .Spotlight-V100 138 | .Trashes 139 | ._* 140 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Functions and constants 20 | """ 21 | 22 | 23 | try: 24 | import tkinter as tk 25 | from tkinter import ttk 26 | except ImportError: 27 | import Tkinter as tk 28 | import ttk 29 | from PIL import Image, ImageDraw, ImageTk 30 | from math import atan2, sqrt, pi 31 | import colorsys 32 | 33 | 34 | PALETTE = ("red", "dark red", "orange", "yellow", "green", "lightgreen", "blue", 35 | "royal blue", "sky blue", "purple", "magenta", "pink", "black", 36 | "white", "gray", "saddle brown", "lightgray", "wheat") 37 | 38 | 39 | # in some python versions round returns a float instead of an int 40 | if not isinstance(round(1.0), int): 41 | def round2(nb): 42 | """Round number to 0 digits and return an int.""" 43 | return int(nb + 0.5) # works because nb >= 0 44 | else: 45 | round2 = round 46 | 47 | 48 | # --- conversion functions 49 | def rgb_to_hsv(r, g, b): 50 | """Convert RGB color to HSV.""" 51 | h, s, v = colorsys.rgb_to_hsv(r / 255., g / 255., b / 255.) 52 | return round2(h * 360), round2(s * 100), round2(v * 100) 53 | 54 | 55 | def hsv_to_rgb(h, s, v): 56 | """Convert HSV color to RGB.""" 57 | r, g, b = colorsys.hsv_to_rgb(h / 360., s / 100., v / 100.) 58 | return round2(r * 255), round2(g * 255), round2(b * 255) 59 | 60 | 61 | def rgb_to_hexa(*args): 62 | """Convert RGB(A) color to hexadecimal.""" 63 | if len(args) == 3: 64 | return ("#%2.2x%2.2x%2.2x" % tuple(args)).upper() 65 | elif len(args) == 4: 66 | return ("#%2.2x%2.2x%2.2x%2.2x" % tuple(args)).upper() 67 | else: 68 | raise ValueError("Wrong number of arguments.") 69 | 70 | 71 | def hexa_to_rgb(color): 72 | """Convert hexadecimal color to RGB.""" 73 | r = int(color[1:3], 16) 74 | g = int(color[3:5], 16) 75 | b = int(color[5:7], 16) 76 | if len(color) == 7: 77 | return r, g, b 78 | elif len(color) == 9: 79 | return r, g, b, int(color[7:9], 16) 80 | else: 81 | raise ValueError("Invalid hexadecimal notation.") 82 | 83 | 84 | def col2hue(r, g, b): 85 | """Return hue value corresponding to given RGB color.""" 86 | return round2(180 / pi * atan2(sqrt(3) * (g - b), 2 * r - g - b) + 360) % 360 87 | 88 | 89 | def hue2col(h): 90 | """Return the color in RGB format corresponding to (h, 100, 100) in HSV.""" 91 | if h < 0 or h > 360: 92 | raise ValueError("Hue should be between 0 and 360") 93 | else: 94 | return hsv_to_rgb(h, 100, 100) 95 | 96 | 97 | # --- Fake transparent image creation with PIL 98 | def create_checkered_image(width, height, c1=(154, 154, 154, 255), 99 | c2=(100, 100, 100, 255), s=6): 100 | """ 101 | Return a checkered image of size width x height. 102 | 103 | Arguments: 104 | * width: image width 105 | * height: image height 106 | * c1: first color (RGBA) 107 | * c2: second color (RGBA) 108 | * s: size of the squares 109 | """ 110 | im = Image.new("RGBA", (width, height), c1) 111 | draw = ImageDraw.Draw(im, "RGBA") 112 | for i in range(s, width, 2 * s): 113 | for j in range(0, height, 2 * s): 114 | draw.rectangle(((i, j), ((i + s - 1, j + s - 1))), fill=c2) 115 | for i in range(0, width, 2 * s): 116 | for j in range(s, height, 2 * s): 117 | draw.rectangle(((i, j), ((i + s - 1, j + s - 1))), fill=c2) 118 | return im 119 | 120 | 121 | def overlay(image, color): 122 | """ 123 | Overlay a rectangle of color (RGBA) on the image and return the result. 124 | """ 125 | width, height = image.size 126 | im = Image.new("RGBA", (width, height), color) 127 | preview = Image.alpha_composite(image, im) 128 | return preview 129 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/gradientbar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | HSV gradient bar 20 | """ 21 | 22 | 23 | from .functions import tk, round2, rgb_to_hexa, hue2col 24 | 25 | 26 | class GradientBar(tk.Canvas): 27 | """HSV gradient colorbar with selection cursor.""" 28 | 29 | def __init__(self, parent, hue=0, height=11, width=256, variable=None, 30 | **kwargs): 31 | """ 32 | Create a GradientBar. 33 | 34 | Keyword arguments: 35 | * parent: parent window 36 | * hue: initially selected hue value 37 | * variable: IntVar linked to the alpha value 38 | * height, width, and any keyword argument accepted by a tkinter Canvas 39 | """ 40 | tk.Canvas.__init__(self, parent, width=width, height=height, **kwargs) 41 | 42 | self._variable = variable 43 | if variable is not None: 44 | try: 45 | hue = int(variable.get()) 46 | except Exception: 47 | pass 48 | else: 49 | self._variable = tk.IntVar(self) 50 | if hue > 360: 51 | hue = 360 52 | elif hue < 0: 53 | hue = 0 54 | self._variable.set(hue) 55 | try: 56 | self._variable.trace_add("write", self._update_hue) 57 | except Exception: 58 | self._variable.trace("w", self._update_hue) 59 | 60 | self.gradient = tk.PhotoImage(master=self, width=width, height=height) 61 | 62 | self.bind('', lambda e: self._draw_gradient(hue)) 63 | self.bind('', self._on_click) 64 | self.bind('', self._on_move) 65 | 66 | def _draw_gradient(self, hue): 67 | """Draw the gradient and put the cursor on hue.""" 68 | self.delete("gradient") 69 | self.delete("cursor") 70 | del self.gradient 71 | width = self.winfo_width() 72 | height = self.winfo_height() 73 | 74 | self.gradient = tk.PhotoImage(master=self, width=width, height=height) 75 | 76 | line = [] 77 | for i in range(width): 78 | line.append(rgb_to_hexa(*hue2col(float(i) / width * 360))) 79 | line = "{" + " ".join(line) + "}" 80 | self.gradient.put(" ".join([line for j in range(height)])) 81 | self.create_image(0, 0, anchor="nw", tags="gradient", 82 | image=self.gradient) 83 | self.lower("gradient") 84 | 85 | x = hue / 360. * width 86 | self.create_line(x, 0, x, height, width=2, tags='cursor') 87 | 88 | def _on_click(self, event): 89 | """Move selection cursor on click.""" 90 | x = event.x 91 | self.coords('cursor', x, 0, x, self.winfo_height()) 92 | self._variable.set(round2((360. * x) / self.winfo_width())) 93 | 94 | def _on_move(self, event): 95 | """Make selection cursor follow the cursor.""" 96 | w = self.winfo_width() 97 | x = min(max(event.x, 0), w) 98 | self.coords('cursor', x, 0, x, self.winfo_height()) 99 | self._variable.set(round2((360. * x) / w)) 100 | 101 | def _update_hue(self, *args): 102 | hue = int(self._variable.get()) 103 | if hue > 360: 104 | hue = 360 105 | elif hue < 0: 106 | hue = 0 107 | self.set(hue) 108 | self.event_generate("<>") 109 | 110 | def get(self): 111 | """Return hue of color under cursor.""" 112 | coords = self.coords('cursor') 113 | return round2(360 * coords[0] / self.winfo_width()) 114 | 115 | def set(self, hue): 116 | """Set cursor position on the color corresponding to the hue value.""" 117 | x = hue / 360. * self.winfo_width() 118 | self.coords('cursor', x, 0, x, self.winfo_height()) 119 | self._variable.set(hue) 120 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/about_plugin.py: -------------------------------------------------------------------------------- 1 | """about thonny-py5mode window 2 | accessed via the menu: py5 > about thonny-py5mode 3 | """ 4 | 5 | import sys 6 | import platform 7 | import tkinter as tk 8 | import webbrowser 9 | from jpype._jvmfinder import JVMNotFoundException 10 | from tkinter import ttk 11 | from thonny import get_version, get_workbench, ui_utils 12 | from thonny.common import get_python_version_string 13 | from thonny.languages import tr 14 | from ._version import __version__ 15 | 16 | 17 | _PY5_VERSION = "version details in Tools > Manage plug-ins" 18 | 19 | 20 | def get_os_word_size_guess() -> None: 21 | """check whether system is 34 or 64-bit""" 22 | if "32" in platform.machine() and "64" not in platform.machine(): 23 | return "(32-bit)" 24 | elif "64" in platform.machine() and "32" not in platform.machine(): 25 | return "(64-bit)" 26 | else: 27 | return "" 28 | 29 | 30 | class AboutDialog(ui_utils.CommonDialog): 31 | def __init__(self, master): 32 | super().__init__(master) 33 | # window/frame 34 | main_frame = ttk.Frame(self) 35 | main_frame.grid(sticky=tk.NSEW, ipadx=15, ipady=15) 36 | main_frame.rowconfigure(0, weight=1) 37 | main_frame.columnconfigure(0, weight=1) 38 | self.title(tr("About thonny-py5mode")) 39 | self.resizable(height=tk.FALSE, width=tk.FALSE) 40 | self.protocol("WM_DELETE_WINDOW", self._ok) 41 | # heading 42 | heading_font = tk.font.nametofont("TkHeadingFont").copy() 43 | heading_font.configure(size=14, weight="bold") 44 | heading_label = ttk.Label( 45 | main_frame, 46 | text="thonny-py5mode\n" + __version__, 47 | font=heading_font, 48 | justify="center", 49 | ) 50 | heading_label.grid() 51 | # thonny-py5mode url 52 | url = "https://github.com/py5coding/thonny-py5mode" 53 | url_font = tk.font.nametofont("TkDefaultFont").copy() 54 | url_font.configure(underline=1) 55 | url_label = ttk.Label( 56 | main_frame, text=url, style="Url.TLabel", cursor="hand2", font=url_font 57 | ) 58 | url_label.grid(pady=(0, 20)) 59 | url_label.bind("", lambda _: webbrowser.open(url)) 60 | # os/distro check 61 | if sys.platform == "linux": 62 | try: 63 | import distro 64 | 65 | system_desc = distro.name(True) 66 | except ImportError: 67 | system_desc = "Linux" 68 | if "32" not in system_desc and "64" not in system_desc: 69 | system_desc += " " + get_os_word_size_guess() 70 | else: 71 | system_desc = ( 72 | platform.system() 73 | + " " 74 | + platform.release() 75 | + " " 76 | + get_os_word_size_guess() 77 | ) 78 | # list system description and versions of python, py5, thonny 79 | platform_label = ttk.Label( 80 | main_frame, 81 | justify=tk.CENTER, 82 | text=system_desc 83 | + "\n Python " 84 | + get_python_version_string() 85 | + "\n py5 " 86 | + _PY5_VERSION 87 | + "\n Thonny " 88 | + get_version(), 89 | ) 90 | platform_label.grid() 91 | # credits 92 | credits_label = ttk.Label( 93 | main_frame, 94 | text=tr("Built with py5"), 95 | style="Url.TLabel", 96 | cursor="hand2", 97 | font=url_font, 98 | justify="center", 99 | ) 100 | credits_label.grid() 101 | credits_label.bind( 102 | "", 103 | lambda _: webbrowser.open("https://py5coding.org/"), 104 | ) 105 | credits_label.grid(pady=20) 106 | # buttons 107 | ok_button = ttk.Button( 108 | main_frame, text=tr("OK"), command=self._ok, default="active" 109 | ) 110 | ok_button.grid(pady=(0, 15)) 111 | ok_button.focus_set() 112 | self.bind("", self._ok, True) 113 | self.bind("", self._ok, True) 114 | 115 | def _ok(self, event=None) -> None: 116 | """call when closing window, responsible for handling all cleanup""" 117 | self.destroy() 118 | 119 | 120 | def open_about_plugin() -> None: 121 | """call to display about thonny-py5mode window""" 122 | ui_utils.show_dialog(AboutDialog(get_workbench())) 123 | 124 | 125 | def add_about_py5mode_command(group: int) -> None: 126 | """add about thonny-py5mode to py5 menu""" 127 | get_workbench().add_command( 128 | "about_thonny-py5mode", 129 | "py5", 130 | tr("About thonny-py5mode"), 131 | open_about_plugin, 132 | group=group, 133 | ) 134 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/alphabar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Alpha channel gradient bar 20 | """ 21 | 22 | 23 | from PIL import Image, ImageTk 24 | from .functions import tk, round2, rgb_to_hsv 25 | from .functions import create_checkered_image 26 | 27 | 28 | class AlphaBar(tk.Canvas): 29 | """Bar to select alpha value.""" 30 | 31 | def __init__(self, parent, alpha=255, color=(255, 0, 0), height=11, 32 | width=256, variable=None, **kwargs): 33 | """ 34 | Create a bar to select the alpha value. 35 | 36 | Keyword arguments: 37 | * parent: parent window 38 | * alpha: initially selected alpha value 39 | * color: gradient color 40 | * variable: IntVar linked to the alpha value 41 | * height, width, and any keyword argument accepted by a tkinter Canvas 42 | """ 43 | tk.Canvas.__init__(self, parent, width=width, height=height, **kwargs) 44 | self.gradient = tk.PhotoImage(master=self, width=width, height=height) 45 | 46 | self._variable = variable 47 | if variable is not None: 48 | try: 49 | alpha = int(variable.get()) 50 | except Exception: 51 | pass 52 | else: 53 | self._variable = tk.IntVar(self) 54 | if alpha > 255: 55 | alpha = 255 56 | elif alpha < 0: 57 | alpha = 0 58 | self._variable.set(alpha) 59 | try: 60 | self._variable.trace_add("write", self._update_alpha) 61 | except Exception: 62 | self._variable.trace("w", self._update_alpha) 63 | 64 | self.bind('', lambda e: self._draw_gradient(alpha, color)) 65 | self.bind('', self._on_click) 66 | self.bind('', self._on_move) 67 | 68 | def _draw_gradient(self, alpha, color): 69 | """Draw the gradient and put the cursor on alpha.""" 70 | self.delete("gradient") 71 | self.delete("cursor") 72 | del self.gradient 73 | width = self.winfo_width() 74 | height = self.winfo_height() 75 | 76 | bg = create_checkered_image(width, height) 77 | r, g, b = color 78 | w = width - 1. 79 | gradient = Image.new("RGBA", (width, height)) 80 | for i in range(width): 81 | for j in range(height): 82 | gradient.putpixel((i, j), (r, g, b, round2(i / w * 255))) 83 | self.gradient = ImageTk.PhotoImage(Image.alpha_composite(bg, gradient), 84 | master=self) 85 | 86 | self.create_image(0, 0, anchor="nw", tags="gardient", 87 | image=self.gradient) 88 | self.lower("gradient") 89 | 90 | x = alpha / 255. * width 91 | h, s, v = rgb_to_hsv(r, g, b) 92 | if v < 50: 93 | fill = "gray80" 94 | else: 95 | fill = 'black' 96 | self.create_line(x, 0, x, height, width=2, tags='cursor', fill=fill) 97 | 98 | def _on_click(self, event): 99 | """Move selection cursor on click.""" 100 | x = event.x 101 | self.coords('cursor', x, 0, x, self.winfo_height()) 102 | self._variable.set(round2((255. * x) / self.winfo_width())) 103 | 104 | def _on_move(self, event): 105 | """Make selection cursor follow the cursor.""" 106 | w = self.winfo_width() 107 | x = min(max(event.x, 0), w) 108 | self.coords('cursor', x, 0, x, self.winfo_height()) 109 | self._variable.set(round2((255. * x) / w)) 110 | 111 | def _update_alpha(self, *args): 112 | alpha = int(self._variable.get()) 113 | if alpha > 255: 114 | alpha = 255 115 | elif alpha < 0: 116 | alpha = 0 117 | self.set(alpha) 118 | self.event_generate("<>") 119 | 120 | def get(self): 121 | """Return hue of color under cursor.""" 122 | coords = self.coords('cursor') 123 | return round2((255. * coords[0]) / self.winfo_width()) 124 | 125 | def set(self, alpha): 126 | """Set cursor position on the color corresponding to the hue value.""" 127 | x = alpha / 255. * self.winfo_width() 128 | self.coords('cursor', x, 0, x, self.winfo_height()) 129 | self._variable.set(alpha) 130 | 131 | def set_color(self, color): 132 | """Set gradient color to color in RGB(A).""" 133 | if len(color) == 3: 134 | alpha = self.get() 135 | else: 136 | alpha = color[3] 137 | self._draw_gradient(alpha, color[:3]) 138 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/spinbox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Nicer Spinbox than the tk.Spinbox 20 | """ 21 | 22 | 23 | from .functions import tk, ttk 24 | 25 | 26 | class Spinbox(tk.Spinbox): 27 | """Spinbox closer to ttk look (designed to be used with clam).""" 28 | 29 | def __init__(self, parent, **kwargs): 30 | """ 31 | Create a Spinbox. 32 | 33 | The keyword arguments are the same as for a tk.Spinbox. 34 | """ 35 | self.style = ttk.Style(parent) 36 | self.frame = ttk.Frame(parent, class_="ttkSpinbox", 37 | relief=kwargs.get("relief", "sunken"), 38 | borderwidth=1) 39 | self.style.configure("%s.spinbox.TFrame" % self.frame, 40 | background=self.style.lookup("TSpinbox", 41 | "fieldbackground", 42 | default='white')) 43 | self.frame.configure(style="%s.spinbox.TFrame" % self.frame) 44 | kwargs["relief"] = "flat" 45 | kwargs["highlightthickness"] = 0 46 | kwargs["selectbackground"] = self.style.lookup("TSpinbox", 47 | "selectbackground", 48 | ("focus",)) 49 | kwargs["selectforeground"] = self.style.lookup("TSpinbox", 50 | "selectforeground", 51 | ("focus",)) 52 | kwargs["background"] = self.style.lookup("TSpinbox", 53 | "fieldbackground", 54 | default='white') 55 | kwargs["foreground"] = self.style.lookup("TSpinbox", 56 | "foreground") 57 | kwargs["buttonbackground"] = self.style.lookup("TSpinbox", 58 | "background") 59 | tk.Spinbox.__init__(self, self.frame, **kwargs) 60 | tk.Spinbox.pack(self, padx=1, pady=1) 61 | self.frame.spinbox = self 62 | 63 | # pack/place/grid methods 64 | self.pack = self.frame.pack 65 | self.pack_slaves = self.frame.pack_slaves 66 | self.pack_propagate = self.frame.pack_propagate 67 | self.pack_configure = self.frame.pack_configure 68 | self.pack_info = self.frame.pack_info 69 | self.pack_forget = self.frame.pack_forget 70 | 71 | self.grid = self.frame.grid 72 | self.grid_slaves = self.frame.grid_slaves 73 | self.grid_size = self.frame.grid_size 74 | self.grid_rowconfigure = self.frame.grid_rowconfigure 75 | self.grid_remove = self.frame.grid_remove 76 | self.grid_propagate = self.frame.grid_propagate 77 | self.grid_info = self.frame.grid_info 78 | self.grid_location = self.frame.grid_location 79 | self.grid_columnconfigure = self.frame.grid_columnconfigure 80 | self.grid_configure = self.frame.grid_configure 81 | self.grid_forget = self.frame.grid_forget 82 | self.grid_bbox = self.frame.grid_bbox 83 | try: 84 | self.grid_anchor = self.frame.grid_anchor 85 | except AttributeError: 86 | pass 87 | 88 | self.place = self.frame.place 89 | self.place_configure = self.frame.place_configure 90 | self.place_forget = self.frame.place_forget 91 | self.place_info = self.frame.place_info 92 | self.place_slaves = self.frame.place_slaves 93 | 94 | self.bind('<1>', lambda e: self.focus_set()) 95 | 96 | self.frame.bind("", self.focusin) 97 | self.frame.bind("", self.focusout) 98 | 99 | def focusout(self, event): 100 | """Change style on focus out events.""" 101 | bc = self.style.lookup("TEntry", "bordercolor", ("!focus",)) 102 | dc = self.style.lookup("TEntry", "darkcolor", ("!focus",)) 103 | lc = self.style.lookup("TEntry", "lightcolor", ("!focus",)) 104 | self.style.configure("%s.spinbox.TFrame" % self.frame, bordercolor=bc, 105 | darkcolor=dc, lightcolor=lc) 106 | 107 | def focusin(self, event): 108 | """Change style on focus in events.""" 109 | self.old_value = self.get() 110 | bc = self.style.lookup("TEntry", "bordercolor", ("focus",)) 111 | dc = self.style.lookup("TEntry", "darkcolor", ("focus",)) 112 | lc = self.style.lookup("TEntry", "lightcolor", ("focus",)) 113 | self.style.configure("%s.spinbox.TFrame" % self.frame, bordercolor=bc, 114 | darkcolor=dc, lightcolor=lc) 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/colorsquare.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Color square gradient with selection cross 20 | """ 21 | 22 | 23 | from .functions import tk, round2, rgb_to_hexa, hue2col, rgb_to_hsv 24 | 25 | 26 | class ColorSquare(tk.Canvas): 27 | """Square color gradient with selection cross.""" 28 | 29 | def __init__(self, parent, hue, color=None, height=256, width=256, **kwargs): 30 | """ 31 | Create a ColorSquare. 32 | 33 | Keyword arguments: 34 | * parent: parent window 35 | * hue: color square gradient for given hue (color in top right corner 36 | is (hue, 100, 100) in HSV 37 | * color: initially selected color given in HSV 38 | * width, height and any keyword option accepted by a tkinter Canvas 39 | """ 40 | tk.Canvas.__init__(self, parent, height=height, width=width, **kwargs) 41 | self.bg = tk.PhotoImage(width=width, height=height, master=self) 42 | self._hue = hue 43 | if not color: 44 | color = hue2col(self._hue) 45 | self.bind('', lambda e: self._draw(color)) 46 | self.bind('', self._on_click) 47 | self.bind('', self._on_move) 48 | 49 | def _fill(self): 50 | """Create the gradient.""" 51 | r, g, b = hue2col(self._hue) 52 | width = self.winfo_width() 53 | height = self.winfo_height() 54 | h = float(height - 1) 55 | w = float(width - 1) 56 | if height: 57 | c = [(r + i / h * (255 - r), g + i / h * (255 - g), b + i / h * (255 - b)) for i in range(height)] 58 | data = [] 59 | for i in range(height): 60 | line = [] 61 | for j in range(width): 62 | rij = round2(j / w * c[i][0]) 63 | gij = round2(j / w * c[i][1]) 64 | bij = round2(j / w * c[i][2]) 65 | color = rgb_to_hexa(rij, gij, bij) 66 | line.append(color) 67 | data.append("{" + " ".join(line) + "}") 68 | self.bg.put(" ".join(data)) 69 | 70 | def _draw(self, color): 71 | """Draw the gradient and the selection cross on the canvas.""" 72 | width = self.winfo_width() 73 | height = self.winfo_height() 74 | self.delete("bg") 75 | self.delete("cross_h") 76 | self.delete("cross_v") 77 | del self.bg 78 | self.bg = tk.PhotoImage(width=width, height=height, master=self) 79 | self._fill() 80 | self.create_image(0, 0, image=self.bg, anchor="nw", tags="bg") 81 | self.tag_lower("bg") 82 | h, s, v = color 83 | x = v / 100. 84 | y = (1 - s / 100.) 85 | self.create_line(0, y * height, width, y * height, tags="cross_h", 86 | fill="#C2C2C2") 87 | self.create_line(x * width, 0, x * width, height, tags="cross_v", 88 | fill="#C2C2C2") 89 | 90 | def get_hue(self): 91 | """Return hue.""" 92 | return self._hue 93 | 94 | def set_hue(self, value): 95 | """Set hue.""" 96 | old = self._hue 97 | self._hue = value 98 | if value != old: 99 | self._fill() 100 | self.event_generate("<>") 101 | 102 | def _on_click(self, event): 103 | """Move cross on click.""" 104 | x = event.x 105 | y = event.y 106 | self.coords('cross_h', 0, y, self.winfo_width(), y) 107 | self.coords('cross_v', x, 0, x, self.winfo_height()) 108 | self.event_generate("<>") 109 | 110 | def _on_move(self, event): 111 | """Make the cross follow the cursor.""" 112 | w = self.winfo_width() 113 | h = self.winfo_height() 114 | x = min(max(event.x, 0), w) 115 | y = min(max(event.y, 0), h) 116 | self.coords('cross_h', 0, y, w, y) 117 | self.coords('cross_v', x, 0, x, h) 118 | self.event_generate("<>") 119 | 120 | def get(self): 121 | """Return selected color with format (RGB, HSV, HEX).""" 122 | x = self.coords('cross_v')[0] 123 | y = self.coords('cross_h')[1] 124 | xp = min(x, self.bg.width() - 1) 125 | yp = min(y, self.bg.height() - 1) 126 | try: 127 | r, g, b = self.bg.get(round2(xp), round2(yp)) 128 | except ValueError: 129 | r, g, b = self.bg.get(round2(xp), round2(yp)).split() 130 | r, g, b = int(r), int(g), int(b) 131 | hexa = rgb_to_hexa(r, g, b) 132 | h = self.get_hue() 133 | s = round2((1 - float(y) / self.winfo_height()) * 100) 134 | v = round2(100 * float(x) / self.winfo_width()) 135 | return (r, g, b), (h, s, v), hexa 136 | 137 | def set_rgb(self, sel_color): 138 | """Put cursor on sel_color given in RGB.""" 139 | width = self.winfo_width() 140 | height = self.winfo_height() 141 | h, s, v = rgb_to_hsv(*sel_color) 142 | self.set_hue(h) 143 | x = v / 100. 144 | y = (1 - s / 100.) 145 | self.coords('cross_h', 0, y * height, width, y * height) 146 | self.coords('cross_v', x * width, 0, x * width, height) 147 | 148 | def set_hsv(self, sel_color): 149 | """Put cursor on sel_color given in HSV.""" 150 | width = self.winfo_width() 151 | height = self.winfo_height() 152 | h, s, v = sel_color 153 | self.set_hue(h) 154 | x = v / 100. 155 | y = (1 - s / 100.) 156 | self.coords('cross_h', 0, y * height, width, y * height) 157 | self.coords('cross_v', x * width, 0, x * width, height) 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thonny-py5mode 2 | 3 | *A py5 plug-in for Thonny* 4 | 5 | ![](screenshots/00-header.png) 6 | 7 | This plug-in for the [Thonny IDE](https://thonny.org) can install [py5](https://py5coding.org/), a modern Python framework that leverages Processing's core libraries, adding an *imported mode for py5* feature to Thonny, making it a great alternative for creative coding with Python in a simplified manner similar to the Processing IDE (PDE). 8 | 9 | ## Instructions 10 | 11 | ### 1. Download and install the Thonny IDE 12 | 13 | *If you have the latest version of Thonny on your computer, you can skip straight to step 2.* 14 | 15 | You can download it for **Mac**, **Windows** and **Linux** from https://thonny.org or from [Thonny's repository releases](https://github.com/thonny/thonny/releases/). 16 | 17 | Then you launch Thonny, and If you're running it for the first time, just accept the *Standard* settings. 18 | 19 | ![](screenshots/02-start-splash.png) 20 | 21 | ### 2. Install the *thonny-py5mode* plug-in 22 | 23 | From Thonny's menu, choose the **Tools > Manage plug-ins...** menu item. 24 | 25 | ![](screenshots/03.01-manage-plug-ins.png) 26 | 27 | Search for **thonny-py5mode** it needs to be typed exactly like this. 28 | 29 | ![](screenshots/03.02-install-plug-in.png) 30 | 31 | Click on the blue thonny-py5mode link. 32 | 33 | ![](screenshots/03.02-install-plug-in2.png) 34 | 35 | Click on the **Install** button. When the installation is complete, you'll need to __restart Thonny__. 36 | 37 | Once restarted, a new ***py5* menu** should appear in Thonny's interface, click on the **py5 > Imported mode for py5** menu option. 38 | 39 | When you first select this, the plug-in will download, extract and configure the JDK for you (in Thonny's user-config directory). 40 | 41 | ![](screenshots/04.01-activate-imported-mode.png) 42 | 43 | Allow it to proceed, and be patient as this process can be lengthy depending on your connection, but it only happens the first time you select the menu option. 44 | 45 | ![](screenshots/04.02-download-jdk.png) 46 | 47 | A message will appear when it is finished. 48 | 49 | ![](screenshots/04.03-download-jdk-done.png) 50 | 51 | You can *apply recommended py5 settings* to make a few configuration tweaks to your IDE, including enabling the blue Kianite theme! 52 | 53 | ![](screenshots/05-apply-recommended-settings.png) 54 | 55 | ![](screenshots/06.01-imported-activated.png) 56 | 57 | ### 3. Run a small example, to check everything is working! 58 | 59 | With the **py5 > imported mode for py5** option on, you can run the following code using the green arrow button or CTRL+R (COMMAND+R on a Mac). Creative coders usually call their programs sketches. 60 | 61 | ```python 62 | def setup(): 63 | size(300, 200) 64 | rect_mode(CENTER) 65 | 66 | def draw(): 67 | square(mouse_x, mouse_y, 10) 68 | ``` 69 | 70 | If you have trouble getting your program to execute, try stopping any other execution that is still running. 71 | 72 | ![](screenshots/06.02-running-sketch.png) 73 | 74 | ## Learn about the difference between *imported mode* and *module mode* 75 | 76 | #### What is the *imported mode* feature provided by the *thonny-py5mode* plug-in? 77 | 78 | The *thonny-py5mode* plug-in creates a *py5* menu in the Thonny interface, inside the *py5* menu, there is an *Imported mode for py5* option that can be turned on or off. When *Imported mode for py5* is on you can write your sketches in a simplified manner, called [imported mode](https://py5coding.org/content/py5_modes.html#imported-mode). It works by making Thonny run your code using the *py5 sketch runner*, a special tool that can also be called from the command line if you are not using Thonny. 79 | 80 | **Important note:** The *imported mode* option is not appropriate for executing Python code that doesn't make use of the py5 library! 81 | 82 | In *imported mode* the vocabulary of *py5*, that is, the names of functions, constants and system variables (such as the mouse position), are available without the `py5.` prefix (needed on *module mode*, more about it later), and your program will be automatically executed by the `run_sketch` function from *py5*. 83 | 84 | With *imported mode* on, you can also run [static mode](https://py5coding.org/content/py5_modes.html#static-mode) sketches, that is, programs without animation or interactivity because they do not have a `draw()` function defined. 85 | 86 | #### What is *module mode* and how can I use it? 87 | 88 | When you disable the *imported mode for py5* menu option, you return Thonny to its normal behavior for executing any Python code. 89 | 90 | In this case, you can use *py5* in [module mode](https://py5coding.org/content/py5_modes.html#module-mode), which is how most Python libraries are handled, i.e. importing the library at the beginning of the program with `import`, and calling its functions with the library name as a prefix. 91 | 92 | ```python 93 | import py5 94 | 95 | def setup(): 96 | py5.size(300, 200) 97 | py5.rect_mode(py5.CENTER) 98 | 99 | def draw(): 100 | py5.square(py5.mouse_x, py5.mouse_y, 10) 101 | 102 | py5.run_sketch() 103 | ``` 104 | 105 | Note that you will need to use `import py5` at the beginning of your code, and `py5.run_sketch()` at the end, as well as the `py5.` prefix for all functions, constants and variables offered by the *py5* library. 106 | 107 | ## Useful py5 resources 108 | 109 | - The official py5 documentation at [py5coding.org](http://py5coding.org/) 110 | - The py5 [discussions](https://github.com/py5coding/py5generator/discussions) forum on GitHub 111 | - The py5 category at the [Processing Foundation Forum](https://discourse.processing.org/c/a-version-of-processing-for-python-38-to-work-with-other-popular-python-libraries-and-tools-such-as-jupyter-numpy-shapely-trimesh-matplotlib-and-pillow-built-to-work-with-popular-python-libraries-and-tools-such-as-jupyter-numpy-shapely-etc/28) 112 | - tabreturn's [py5 quick reference](https://github.com/tabreturn/processing.py-cheat-sheet/blob/pt-br/py5/py5_cc.pdf) 113 | - Villares' daily sketches at [sketch-a-day](https://abav.lugaralgum.com/sketch-a-day), mostly done with py5. 114 | 115 | ## Credits 116 | 117 | The **thonny-py5mode** plug-in was initially developed by [@tabreturn](https://github.com/tabreturn), who was inspired by a [proof of concept by @villares](https://github.com/villares/thonny-py5-runner), being thankful to [@hx2A](https://github.com/hx2A/) for the [py5 project](https://py5coding.org/), and the [Thonny folks](https://github.com/thonny) for their fantastic IDE. The *Color selector* incorporates Juliette Monsel's excellent [tkColorPicker](https://github.com/j4321/tkColorPicker) module. From 2025 onward the plug-in became a part of the py5 project and is maintained by its community. 118 | 119 | ## Contributing 120 | 121 | Follow discussions on the [*thonny-py5mode* plug-in GitHub repository](https://github.com/py5coding/thonny-py5mode/discussions/) and [report issues](https://github.com/py5coding/thonny-py5mode/issues). 122 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/__init__.py: -------------------------------------------------------------------------------- 1 | """thonny-py5mode frontend 2 | interacts with py5mode backend (backend > py5_imported_mode_backend.py) 3 | """ 4 | 5 | import builtins 6 | import keyword 7 | import os 8 | import pathlib 9 | import platform 10 | import shutil 11 | import site 12 | import subprocess 13 | import sys 14 | import tkinter as tk 15 | import types 16 | import webbrowser 17 | from distutils.sysconfig import get_python_lib 18 | from importlib import machinery, util 19 | from tkinter.messagebox import showerror, showinfo 20 | 21 | from thonny import editors, get_runner, get_workbench, running, token_utils 22 | from thonny.common import BackendEvent 23 | from thonny.languages import tr 24 | from thonny.running import Runner 25 | from thonny.shell import BaseShellText 26 | 27 | from .about_plugin import add_about_py5mode_command, open_about_plugin 28 | from .install_jdk import install_jdk 29 | 30 | try: # thonny 4 package layout 31 | from thonny import get_sys_path_directory_containg_plugins 32 | except ImportError: # thonny 3 package layout 33 | pass 34 | # modified tkcolorpicker (by j4321) to work with thonny for macos 35 | # now vendored on this same repo 36 | from .py5colorpicker.tkcolorpicker import modeless_colorpicker 37 | 38 | _PY5_IMPORTED_MODE = "run.py5_imported_mode" 39 | color_selector_open = False 40 | 41 | 42 | def apply_recommended_py5_config() -> None: 43 | """apply some recommended settings for thonny py5 work""" 44 | get_workbench().set_option("view.ui_theme", "Kyanite UI") 45 | get_workbench().set_option("view.syntax_theme", "Kyanite Syntax") 46 | get_workbench().set_option("view.highlight_current_line", "True") 47 | get_workbench().set_option("view.locals_highlighting", "True") 48 | get_workbench().set_option("assistance.open_assistant_on_errors", "False") 49 | get_workbench().set_option("view.assistantview", False) 50 | get_workbench().hide_view("AssistantView") 51 | get_workbench().reload_themes() 52 | 53 | 54 | def execute_imported_mode() -> None: 55 | """run imported mode script using py5_tools run_sketch""" 56 | current_editor = get_workbench().get_editor_notebook().get_current_editor() 57 | current_file = current_editor.get_filename() 58 | 59 | if current_file is None: 60 | # thonny must 'save as' any new files, before it can run them 61 | editors.Editor.save_file(current_editor) 62 | current_file = current_editor.get_filename() 63 | 64 | if current_file and current_file.split(".")[-1] in ("py", "py5", "pyde"): 65 | # save and run py5 imported mode 66 | current_editor.save_file() 67 | user_packages = str(site.getusersitepackages()) 68 | site_packages = str(site.getsitepackages()[0]) 69 | plug_packages = util.find_spec("py5_tools").submodule_search_locations 70 | run_sketch_locations = [ 71 | pathlib.Path(user_packages + "/py5_tools/tools/run_sketch.py"), 72 | pathlib.Path(site_packages + "/py5_tools/tools/run_sketch.py"), 73 | pathlib.Path(plug_packages[0] + "/tools/run_sketch.py"), 74 | pathlib.Path(get_python_lib() + "/py5_tools/tools/run_sketch.py"), 75 | ] 76 | 77 | for location in run_sketch_locations: 78 | # if location matches py5_tools path, use it 79 | if location.is_file(): 80 | run_sketch = location 81 | break 82 | 83 | # set switch so Sketch will report window location 84 | py5_switches = "--py5_options external" 85 | # retrieve last display window location 86 | py5_loc = get_workbench().get_option("run.py5_location") 87 | if py5_loc: 88 | # add location switch to command line 89 | py5_switches += " location=" + ",".join(map(str, py5_loc)) 90 | 91 | # run command to execute sketch 92 | working_directory = os.path.dirname(current_file) 93 | cd_cmd_line = running.construct_cd_command(working_directory) + "\n" 94 | cmd_parts = ["%Run", str(run_sketch), current_file] 95 | exe_cmd_line = running.construct_cmd_line(cmd_parts) + " " 96 | exe_cmd_line += py5_switches + "\n" 97 | running.get_shell().submit_magic_command(cd_cmd_line + exe_cmd_line) 98 | 99 | 100 | def patched_execute_current(self: Runner, command_name: str) -> None: 101 | """override run button behavior for py5 imported mode""" 102 | execute_imported_mode() 103 | 104 | 105 | def patch_token_coloring() -> None: 106 | """add py5 keywords to syntax highlighting""" 107 | spec = util.find_spec("py5_tools") 108 | # cannot use `dir(py5)` because of jvm check, hence direct loading 109 | path = pathlib.Path(spec.submodule_search_locations[0]) / "reference.py" 110 | loader = machinery.SourceFileLoader("py5_tools_reference", str(path)) 111 | module = types.ModuleType(loader.name) 112 | loader.exec_module(module) 113 | # add keywords to thonny builtin list 114 | patched_builtinlist = token_utils._builtinlist + module.PY5_ALL_STR 115 | matches = token_utils.matches_any("builtin", patched_builtinlist) 116 | patched_BUILTIN = r'([^.\'"\\#]\b|^)' + (matches + r"\b") 117 | token_utils.BUILTIN = patched_BUILTIN 118 | 119 | 120 | def set_py5_imported_mode() -> None: 121 | """set imported mode variable in thonny configuration.ini file""" 122 | if get_workbench().in_simple_mode(): 123 | os.environ["PY5_IMPORTED_MODE"] = "auto" 124 | else: 125 | p_i_m = str(get_workbench().get_option(_PY5_IMPORTED_MODE)) 126 | os.environ["PY5_IMPORTED_MODE"] = p_i_m 127 | 128 | # switch on/off py5 run button behavior 129 | if get_workbench().get_option(_PY5_IMPORTED_MODE): 130 | Runner._original_execute_current = Runner.execute_current 131 | Runner.execute_current = patched_execute_current 132 | # must restart backend for py5 autocompletion upon installing jdk 133 | try: 134 | get_runner().restart_backend(False) 135 | except AttributeError: 136 | pass 137 | else: 138 | # patched method non-existant when imported mode active at launch 139 | try: 140 | Runner.execute_current = Runner._original_execute_current 141 | # this line disable py5 autocompletion in this instance 142 | get_runner().restart_backend(False) 143 | except AttributeError: 144 | pass 145 | 146 | 147 | def toggle_py5_imported_mode() -> None: 148 | """toggle py5 imported mode settings""" 149 | var = get_workbench().get_variable(_PY5_IMPORTED_MODE) 150 | var.set(not var.get()) 151 | install_jdk() 152 | set_py5_imported_mode() 153 | 154 | 155 | def color_selector() -> None: 156 | """open tkinter color selector""" 157 | global color_selector_open 158 | # ... if one is not already open 159 | if not color_selector_open: 160 | color_selector_open = True 161 | modeless_colorpicker(title=tr("Color selector")) 162 | color_selector_open = False 163 | 164 | 165 | def convert_code(translator) -> None: 166 | """function to handle different py5_tools conversions""" 167 | workbench = get_workbench() 168 | current_editor = workbench.get_editor_notebook().get_current_editor() 169 | current_file = current_editor.get_filename() 170 | 171 | if current_file is None: 172 | # save unsaved file before attempting to convert it 173 | editors.Editor.save_file(current_editor) 174 | current_file = current_editor.get_filename() 175 | 176 | if current_file and current_file.split(".")[-1] in ("py", "py5", "pyde"): 177 | # save and run perform conversion 178 | current_editor.save_file() 179 | translator.translate_file(current_file, current_file) 180 | current_editor._load_file(current_file, keep_undo=True) 181 | showinfo("py5 Conversion", "Conversion complete", master=workbench) 182 | 183 | 184 | def patched_handle_program_output(self, msg: BackendEvent) -> None: 185 | """catch display window movements and write coords to the config file""" 186 | if msg.__getitem__("data")[:8] == "__MOVE__": 187 | py5_loc = msg.__getitem__("data")[9:-1].split(" ") 188 | # write display window location to config file 189 | if len(py5_loc) == 2: 190 | py5_loc = py5_loc[0] + "," + py5_loc[1] 191 | get_workbench().set_option("run.py5_location", py5_loc) 192 | # skip the rest of the function so the shell won't display coords 193 | return 194 | 195 | # print the rest of the shell output as usual 196 | BaseShellText._original_handle_program_output(self, msg) 197 | 198 | 199 | def show_sketch_folder() -> None: 200 | """open the enclosing folder of the current file""" 201 | current_editor = get_workbench().get_editor_notebook().get_current_editor() 202 | # check if the editor is empty/blank 203 | try: 204 | filename = current_editor.get_filename() 205 | except AttributeError: 206 | showerror("Editor is empty", "Do you have a file open in the editor?") 207 | return 208 | # check if the file isn't an (yet to be saved) file 209 | try: 210 | path = os.path.dirname(filename) 211 | except TypeError: 212 | showerror("File not found", "Have you saved your file somewhere yet?") 213 | return 214 | # open file manager for mac/linux/windows 215 | if sys.platform == "darwin": 216 | subprocess.Popen(["open", path]) 217 | elif sys.platform == "linux": 218 | subprocess.Popen(["xdg-open", path]) 219 | else: 220 | subprocess.Popen(["explorer", path]) 221 | 222 | 223 | def load_plugin() -> None: 224 | get_workbench().set_default(_PY5_IMPORTED_MODE, False) 225 | get_workbench().add_command( 226 | "toggle_py5_imported_mode", 227 | "py5", 228 | tr("Imported mode for py5"), 229 | toggle_py5_imported_mode, 230 | flag_name=_PY5_IMPORTED_MODE, 231 | group=10, 232 | ) 233 | get_workbench().add_command( 234 | "apply_recommended_py5_config", 235 | "py5", 236 | tr("Apply recommended py5 settings"), 237 | apply_recommended_py5_config, 238 | group=20, 239 | ) 240 | get_workbench().add_command( 241 | "py5_color_selector", 242 | "py5", 243 | tr("Color selector"), 244 | color_selector, 245 | group=30, 246 | default_sequence="", 247 | ) 248 | get_workbench().add_command( 249 | "py5_reference", 250 | "py5", 251 | tr("py5 reference"), 252 | lambda: webbrowser.open("https://py5coding.org/reference/"), 253 | group=30, 254 | ) 255 | git_raw_url = "https://raw.githubusercontent.com/" 256 | quick_reference_pdf = "py5coding/thonny-py5mode/main/assets/py5_quick_reference.pdf" 257 | get_workbench().add_command( 258 | "py5_quickreference", 259 | "py5", 260 | tr("py5 quick reference"), 261 | lambda: webbrowser.open(git_raw_url + quick_reference_pdf), 262 | group=30, 263 | ) 264 | get_workbench().add_command( 265 | "open_folder", "py5", tr("Show sketch folder"), show_sketch_folder, group=40 266 | ) 267 | add_about_py5mode_command(50) 268 | patch_token_coloring() 269 | set_py5_imported_mode() 270 | 271 | # note that _handle_program_output is not a public api 272 | # may need to treat different thonny versions differently 273 | h_p_o = BaseShellText._handle_program_output 274 | BaseShellText._original_handle_program_output = h_p_o 275 | BaseShellText._handle_program_output = patched_handle_program_output 276 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/install_jdk.py: -------------------------------------------------------------------------------- 1 | '''thonny-py5mode JDK installer. 2 | Checks for JDK and, if not found, installs it to Thonny's user directory.''' 3 | 4 | import re, shutil, jdk 5 | 6 | from pathlib import Path, PurePath 7 | from threading import Thread 8 | 9 | from os import environ as env, scandir, rename, PathLike 10 | from os.path import islink, realpath 11 | 12 | from typing import Any, Callable, Literal, TypeAlias 13 | from collections.abc import Iterable, Iterator 14 | 15 | import tkinter as tk 16 | from tkinter import ttk 17 | from tkinter.messagebox import showinfo 18 | 19 | from thonny import get_workbench, ui_utils, THONNY_USER_DIR 20 | from thonny.languages import tr 21 | 22 | StrPath: TypeAlias = str | PathLike[str] 23 | '''A type representing string-based filesystem paths.''' 24 | 25 | PathAction: TypeAlias = Callable[[StrPath], Any] 26 | '''Represents an action applied to a single path-like object.''' 27 | 28 | JDK_PATTERN = re.compile(r""" 29 | (?:java|jdk) # Match 'java' or 'jdk' (non-capturing group) 30 | -? # Match optional hyphen '-' 31 | (\d+) # Capture JDK major version number as group(1) 32 | """, re.IGNORECASE | re.VERBOSE) 33 | '''Captures the major version number from strings like "java-17" or "jdk21".''' 34 | 35 | REQUIRE_JDK, VERSION_JDK = 17, '17' 36 | '''JDK minimum required version to run Processing.''' 37 | 38 | DOWNLOAD_JDK = '21' 39 | '''JDK version to download.''' 40 | 41 | JDK_DIR = 'jdk-' + DOWNLOAD_JDK 42 | '''JDK install subfolder name.''' 43 | 44 | THONNY_USER_PATH = Path(THONNY_USER_DIR) 45 | '''Thonny user folder's full path string.''' 46 | 47 | JDK_PATH = THONNY_USER_PATH / JDK_DIR 48 | '''Path for JDK installation subfolder.''' 49 | 50 | WORKBENCH = get_workbench() 51 | '''Thonny's workbench singleton instance.''' 52 | 53 | def install_jdk() -> None: # Module's main entry-point function 54 | '''Call this function from where this module is imported.''' 55 | 56 | if is_java_home_set(): return # JAVA_HOME already points to required version 57 | 58 | # Set a local JAVA_HOME to the detected JDK found in THONNY_USER_DIR: 59 | if path := get_thonny_jdk_install(): set_java_home(path) 60 | 61 | # Otherwise, if Thonny doesn't have a proper JDK version... 62 | else: ui_utils.show_dialog(JdkDialog()) # ... ask permission to download 1. 63 | 64 | 65 | def is_java_home_set() -> bool: 66 | '''Check system for existing JDK that meets the py5 version requirements.''' 67 | 68 | if java_home := env.get('JAVA_HOME'): # Check if JAVA_HOME is already set 69 | system_jdk = 'TBD' # JDK version To-Be-Determined 70 | 71 | if islink(java_home): 72 | java_home = realpath(java_home) # If symlink, resolve actual path 73 | 74 | if match := JDK_PATTERN.search(java_home): 75 | system_jdk = match.group(1) # Get JDK version from 1st match group 76 | 77 | if is_valid_jdk_version(system_jdk) and is_valid_jdk_path(java_home): 78 | return True # Version is numeric and meets the minimum requirement 79 | 80 | return False # No JAVA_HOME pointing to a required JDK was found 81 | 82 | 83 | def get_thonny_jdk_install() -> PurePath | Literal['']: 84 | '''Check Thonny's user folder for a JDK installation subfolder 85 | and return its path. Otherwise, return an empty string.''' 86 | 87 | for subfolder in get_all_thonny_folders(): # Loop over each subfolder name 88 | # Use regexp to check if subfolder contains a valid JDK name: 89 | if match := JDK_PATTERN.search(subfolder): 90 | # Check JDK major version from 1st match group: 91 | if is_valid_jdk_version( match.group(1) ): 92 | # Create a full path by joining THONNY_USER_DIR + folder name: 93 | jdk_path = adjust_jdk_path(THONNY_USER_PATH / subfolder) 94 | 95 | # Check and return a valid JDK subfolder from THONNY_USER_DIR: 96 | if is_valid_jdk_path(jdk_path): return jdk_path 97 | 98 | return '' # No JDK with required version found in THONNY_USER_DIR 99 | 100 | 101 | def set_java_home(jdk_path: StrPath) -> None: 102 | '''Add JDK path to config file (tools > options > general > env vars).''' 103 | 104 | jdk_path = str(adjust_jdk_path(jdk_path)) # Platform-adjusted path 105 | env['JAVA_HOME'] = jdk_path # Python's process points to Thonny's JDK too 106 | 107 | java_home_entry = create_java_home_entry_from_path(jdk_path) 108 | env_vars = dict.fromkeys(WORKBENCH.get_option('general.environment')) 109 | 110 | if java_home_entry not in env_vars: 111 | entries = [ *drop_all_java_home_entries(env_vars), java_home_entry ] 112 | WORKBENCH.set_option('general.environment', entries) 113 | showinfo('JAVA_HOME', jdk_path, parent=WORKBENCH) 114 | 115 | 116 | def adjust_jdk_path(jdk_path: StrPath) -> PurePath: 117 | '''Adjust JDK path for the specificity of current platform.''' 118 | 119 | jdk_path = PurePath(jdk_path) 120 | 121 | # if MacOS, append "/Contents/Home/" to form the actual JDK path for it: 122 | if jdk.OS is jdk.OperatingSystem.MAC and jdk_path.name != 'Home': 123 | jdk_path = jdk_path / 'Contents' / 'Home' 124 | 125 | return jdk_path 126 | 127 | 128 | def create_java_home_entry_from_path(jdk_path: StrPath) -> str: 129 | '''Prefix JDK path with "JAVA_HOME=" to form a Thonny environment entry.''' 130 | return f'JAVA_HOME={jdk_path}' 131 | 132 | 133 | def drop_all_java_home_entries(entries: Iterable[str]) -> Iterator[str]: 134 | '''Filter out existing entries which start with "JAVA_HOME=".''' 135 | return filter(_non_java_home_predicate, entries) 136 | 137 | 138 | def _non_java_home_predicate(entry: str) -> bool: 139 | '''Check if the entry doesn't start with "JAVA_HOME=".''' 140 | return not entry.startswith('JAVA_HOME=') 141 | 142 | 143 | def is_valid_jdk_version(jdk_version: str) -> bool: 144 | '''Check if JDK version meets minimum version requirement.''' 145 | return jdk_version.isdigit() and int(jdk_version) >= REQUIRE_JDK 146 | 147 | 148 | def is_valid_jdk_path(jdk_path: StrPath) -> bool: 149 | '''Check if the given path points to a JDK install with a usable Java.''' 150 | java_compiler = jdk._IS_WINDOWS and 'javac.exe' or 'javac' 151 | return Path(jdk_path, 'bin', java_compiler).is_file() 152 | 153 | 154 | def get_all_thonny_folders() -> list[str]: 155 | """Return reverse-sorted names of subfolders within Thonny's user folder.""" 156 | with scandir(THONNY_USER_DIR) as entries: 157 | return sorted((e.name for e in entries if e.is_dir()), reverse=True) 158 | 159 | 160 | class JdkDialog(ui_utils.CommonDialog): 161 | '''User-facing dialog prompting install of required JDK for py5 sketches. 162 | - Presents user with option to proceed or cancel the JDK installation. 163 | - Displays a horizontal indeterminate-sized progress bar during download. 164 | - Launches a background thread to handle installation tasks. 165 | - Shows a success message when installation is complete.''' 166 | 167 | _TITLE = tr('Install JDK ' + DOWNLOAD_JDK + ' for py5') 168 | 169 | _PROGRESS = tr('Downloading and extracting JDK ' + DOWNLOAD_JDK + ' ...') 170 | 171 | _OK, _CANCEL, _DONE = map(tr, ('Proceed', 'Cancel', 'JDK done')) 172 | 173 | _MSG = 'JDK ' + DOWNLOAD_JDK + tr(' extracted to ') + THONNY_USER_DIR + tr( 174 | '\n\nYou can now run py5 sketches.') 175 | 176 | _INSTALL_JDK = tr( 177 | "Thonny requires at least JDK " + VERSION_JDK + " to run py5 sketches. " 178 | "It'll need to download about 180 MB.") 179 | 180 | _PROGRESS_BAR_Y_PADDING = 0, 15 181 | 182 | def __init__(self, master=WORKBENCH, skip_diag_attribs=False, **kw): 183 | super().__init__(master, skip_diag_attribs, **kw) 184 | 185 | # Set dialog properties: title, fixed size, close button disabled: 186 | self.title(self._TITLE) # Dialog title for JDK installation 187 | self.resizable(height=tk.FALSE, width=tk.FALSE) # Prevent its resizing 188 | self.protocol('WM_DELETE_WINDOW', '{#}') # Disable its close button 189 | 190 | # Window/Frame: 191 | main_frame = self.main_frame = ttk.Frame(self) 192 | main_frame.grid(ipadx=15, ipady=15, sticky=tk.NSEW) 193 | main_frame.rowconfigure(0, weight=1) 194 | main_frame.columnconfigure(0, weight=1) 195 | 196 | # Display install message: 197 | message_label = ttk.Label(main_frame, text=self._INSTALL_JDK) 198 | message_label.grid(pady=0, columnspan=2) 199 | 200 | # OK proceed button: 201 | ok_button = self.ok_button = ttk.Button( 202 | main_frame, 203 | text=self._OK, 204 | command=self._proceed, 205 | default=tk.ACTIVE) 206 | 207 | ok_button.grid(row=2, column=0, padx=15, pady=15, sticky=tk.W) 208 | ok_button.focus_set() 209 | 210 | # Cancel button: 211 | cancel_button = self.cancel_button = ttk.Button( 212 | main_frame, 213 | text=self._CANCEL, 214 | command=self._close) 215 | 216 | cancel_button.grid(row=2, column=1, padx=15, pady=15, sticky=tk.E) 217 | 218 | 219 | def _proceed(self) -> None: 220 | '''Starts JDK downloader thread.''' 221 | 222 | # Get rid of both OK & Cancel buttons: 223 | if self.ok_button: self.ok_button.destroy() 224 | if self.cancel_button: self.cancel_button.destroy() 225 | 226 | # Progress bar label: 227 | dl_label = ttk.Label(self.main_frame, text=self._PROGRESS) 228 | dl_label.grid(row=1, columnspan=2, pady=self._PROGRESS_BAR_Y_PADDING) 229 | 230 | # Progress bar: 231 | progress_bar = ttk.Progressbar(self.main_frame, mode='indeterminate') 232 | 233 | progress_bar.grid( 234 | row=2, column=0, columnspan=2, 235 | padx=15, pady=self._PROGRESS_BAR_Y_PADDING, 236 | sticky=tk.EW) 237 | 238 | # Start progress bar animation + download thread: 239 | if self.main_frame: self.main_frame.tkraise() 240 | 241 | download_thread = DownloadJDK() 242 | download_thread.start() 243 | progress_bar.start(20) 244 | 245 | self._monitor(download_thread, progress_bar) 246 | 247 | 248 | def _monitor(self, download: Thread, progress: ttk.Progressbar) -> None: 249 | '''Animate progress bar while JDK installs and extracts.''' 250 | 251 | if download.is_alive(): 252 | self.after(100, lambda: self._monitor(download, progress)) 253 | return 254 | 255 | # Destroy this JDK dialog instance once download has finished: 256 | progress.stop() 257 | self._close() 258 | 259 | showinfo(self._DONE, self._MSG, parent=WORKBENCH) 260 | 261 | 262 | def _close(self) -> None: 263 | '''Fully shutdown the JdkDialog instance.''' 264 | self.destroy() 265 | self.main_frame = self.ok_button = self.cancel_button = None 266 | 267 | 268 | 269 | class DownloadJDK(Thread): 270 | '''Background thread for downloading & installing JDK into Thonny's folder. 271 | - Removes any preexisting JDK folders matching the expected version. 272 | - Downloads and extracts the required JDK version. 273 | - Renames the downloaded folder to the expected format. 274 | - Sets JAVA_HOME on Thonny configuration.''' 275 | 276 | def run(self) -> None: 277 | '''Download and setup JDK (installs to Thonny's user directory)''' 278 | 279 | # Delete existing Thonny's JDK subfolders matching jdk-: 280 | self.process_match_jdk_dirs(shutil.rmtree) 281 | 282 | # Download and extract JDK subfolder into Thonny's user folder: 283 | jdk.install(DOWNLOAD_JDK, path=THONNY_USER_DIR) 284 | 285 | # Rename extracted Thonny's JDK subfolder to jdk-: 286 | self.process_match_jdk_dirs(self.rename_folder, True) 287 | 288 | set_java_home(JDK_PATH) # Add a Thonny's JAVA_HOME entry for it 289 | 290 | 291 | @staticmethod 292 | def process_match_jdk_dirs(action: PathAction, only_1st=False) -> None: 293 | '''Apply an action to JDK-matching subfolders in Thonny's folder.''' 294 | 295 | for path in DownloadJDK.get_all_thonny_folder_paths(): 296 | if path.name.startswith(JDK_DIR): # Folder name matches 297 | action(path) # Callback to run on each matching folder path 298 | if only_1st: break # Stop at 1st match occurrence 299 | 300 | 301 | @staticmethod 302 | def get_all_thonny_folder_paths() -> Iterator[Path]: 303 | '''Find all subfolder paths within Thonny's user folder''' 304 | return filter(Path.is_dir, THONNY_USER_PATH.iterdir()) 305 | 306 | 307 | @staticmethod 308 | def rename_folder(path: StrPath) -> None: 309 | '''Rename a JDK subfolder to the expected jdk- format.''' 310 | rename(path, JDK_PATH) 311 | -------------------------------------------------------------------------------- /thonnycontrib/thonny-py5mode/py5colorpicker/tkcolorpicker/colorpicker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tkcolorpicker - Alternative to colorchooser for Tkinter. 4 | Copyright 2017 Juliette Monsel 5 | 6 | tkcolorpicker is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | tkcolorpicker is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | Colorpicker dialog 20 | """ 21 | # 2022_07_17 added a 'pt' translation and made some minor ajustments 22 | # to the labels for use with thonny-py5mode plugin (@villares) 23 | 24 | from PIL import ImageTk 25 | from .functions import tk, ttk, round2, create_checkered_image, \ 26 | overlay, PALETTE, hsv_to_rgb, hexa_to_rgb, rgb_to_hexa, col2hue, rgb_to_hsv 27 | from .alphabar import AlphaBar 28 | from .gradientbar import GradientBar 29 | from .colorsquare import ColorSquare 30 | from .spinbox import Spinbox 31 | from .limitvar import LimitVar 32 | from locale import getdefaultlocale 33 | import re 34 | import pyperclip 35 | 36 | 37 | # --- Translation 38 | interface_texts = { 39 | 'en': { 40 | # 'en' can be empty, in the absense of a key, the key itself is used 41 | # Being used here to customize Juliette's original interface 42 | "HTML": "Hex Notation", 43 | "OK": "Select", 44 | "Copy Hex": "Copy Hex Notation", 45 | "Copy RGB": "Copy red, green, blue", 46 | }, 47 | 'fr': { 48 | "Red": "Rouge", "Green": "Vert", "Blue": "Bleu", 49 | "Hue": "Teinte", "Saturation": "Saturation", "Value": "Valeur", 50 | "Cancel": "Annuler", "Color Chooser": "Sélecteur de couleur", 51 | "Close": "Fermer", 52 | "Alpha": "Alpha", "HTML": "Notation hexadécimale", 53 | "OK": "Selectioner", 54 | "Copy Hex": "Copier notation hexadécimale", 55 | "Copy RGB": "Copier rouge, vert, bleu", 56 | }, 57 | 'pt': { 58 | "Red": "Vermelho", "Green": "Verde", "Blue": "Azul", 59 | "Hue": "Matiz", "Saturation": "Saturação", "Value": "Valor", 60 | "Cancel": "Cancelar", "Color Chooser": "Sélecteur de couleur", 61 | "Close": "Fechar", 62 | "Alpha": "Alpha", "HTML": "Notação hexadecimal", 63 | "OK": "Selecionar", 64 | "Copy Hex": "Copiar notação hexa", 65 | "Copy RGB": "Copiar vermelho, verde, azul", 66 | }, 67 | } 68 | try: 69 | loc = getdefaultlocale()[0][:2] 70 | except (TypeError, ValueError): 71 | loc = 'en' 72 | TR = interface_texts.get(loc, interface_texts['en']) 73 | 74 | 75 | def _(text): 76 | """Translate text.""" 77 | return TR.get(text, text) 78 | 79 | 80 | class ColorPicker(tk.Toplevel): 81 | """Color picker dialog.""" 82 | 83 | def __init__(self, 84 | parent=None, 85 | color=(255, 0, 0), 86 | alpha=False, 87 | title=_("Color Chooser"), 88 | modeless=False): 89 | """ 90 | Create a ColorPicker dialog. 91 | 92 | Arguments: 93 | * parent: parent window 94 | * color: initially selected color in rgb or hexa format 95 | * alpha: alpha channel support (boolean) 96 | * title: dialog title 97 | * modeless: Won't grab_set(), no OK button, Cancel is named Close 98 | buttons to to copy color as hex notation or 'r, g, b' 99 | that won't close window. Always returns None. 100 | """ 101 | tk.Toplevel.__init__(self, parent) 102 | 103 | self.title(title) 104 | self.transient(self.master) 105 | self.resizable(False, False) 106 | self.rowconfigure(1, weight=1) 107 | self.modeless = modeless 108 | 109 | self.color = "" 110 | self.alpha_channel = bool(alpha) 111 | style = ttk.Style(self) 112 | style.map("palette.TFrame", relief=[('focus', 'sunken')], 113 | bordercolor=[('focus', "#4D4D4D")]) 114 | self.configure(background=style.lookup("TFrame", "background")) 115 | 116 | if isinstance(color, str): 117 | if re.match(r"^#[0-9A-F]{8}$", color.upper()): 118 | col = hexa_to_rgb(color) 119 | self._old_color = col[:3] 120 | if alpha: 121 | self._old_alpha = col[3] 122 | old_color = color 123 | else: 124 | old_color = color[:7] 125 | elif re.match(r"^#[0-9A-F]{6}$", color.upper()): 126 | self._old_color = hexa_to_rgb(color) 127 | old_color = color 128 | if alpha: 129 | self._old_alpha = 255 130 | old_color += 'FF' 131 | else: 132 | col = self.winfo_rgb(color) 133 | self._old_color = tuple(round2(c * 255 / 65535) for c in col) 134 | args = self._old_color 135 | if alpha: 136 | self._old_alpha = 255 137 | args = self._old_color + (255,) 138 | old_color = rgb_to_hexa(*args) 139 | else: 140 | self._old_color = color[:3] 141 | if alpha: 142 | if len(color) < 4: 143 | color += (255,) 144 | self._old_alpha = 255 145 | else: 146 | self._old_alpha = color[3] 147 | old_color = rgb_to_hexa(*color) 148 | 149 | # --- GradientBar 150 | hue = col2hue(*self._old_color) 151 | bar = ttk.Frame(self, borderwidth=2, relief='groove') 152 | self.bar = GradientBar(bar, hue=hue, width=200, highlightthickness=0) 153 | self.bar.pack() 154 | 155 | # --- ColorSquare 156 | square = ttk.Frame(self, borderwidth=2, relief='groove') 157 | self.square = ColorSquare(square, hue=hue, width=200, height=200, 158 | color=rgb_to_hsv(*self._old_color), 159 | highlightthickness=0) 160 | self.square.pack() 161 | 162 | frame = ttk.Frame(self) 163 | frame.columnconfigure(1, weight=1) 164 | frame.rowconfigure(1, weight=1) 165 | 166 | # --- color preview: initial color and currently selected color side by side 167 | preview_frame = ttk.Frame(frame, relief="groove", borderwidth=2) 168 | preview_frame.grid(row=0, column=0, sticky="nw", pady=2) 169 | if alpha: 170 | self._transparent_bg = create_checkered_image(42, 32) 171 | transparent_bg_old = create_checkered_image(42, 32, 172 | (100, 100, 100, 255), 173 | (154, 154, 154, 255)) 174 | prev_old = overlay(transparent_bg_old, hexa_to_rgb(old_color)) 175 | prev = overlay(self._transparent_bg, hexa_to_rgb(old_color)) 176 | self._im_old_color = ImageTk.PhotoImage(prev_old, master=self) 177 | self._im_color = ImageTk.PhotoImage(prev, master=self) 178 | old_color_prev = tk.Label(preview_frame, padx=0, pady=0, 179 | image=self._im_old_color, 180 | borderwidth=0, highlightthickness=0) 181 | self.color_preview = tk.Label(preview_frame, pady=0, padx=0, 182 | image=self._im_color, 183 | borderwidth=0, highlightthickness=0) 184 | else: 185 | old_color_prev = tk.Label(preview_frame, background=old_color[:7], 186 | width=5, highlightthickness=0, height=2, 187 | padx=0, pady=0) 188 | self.color_preview = tk.Label(preview_frame, width=5, height=2, 189 | pady=0, background=old_color[:7], 190 | padx=0, highlightthickness=0) 191 | old_color_prev.bind("<1>", self._reset_preview) 192 | old_color_prev.grid(row=0, column=0) 193 | self.color_preview.grid(row=0, column=1) 194 | 195 | # --- palette 196 | palette = ttk.Frame(frame) 197 | palette.grid(row=0, column=1, rowspan=2, sticky="ne") 198 | for i, col in enumerate(PALETTE): 199 | f = ttk.Frame(palette, borderwidth=1, relief="raised", 200 | style="palette.TFrame") 201 | l = tk.Label(f, background=col, width=2, height=1) 202 | l.bind("<1>", self._palette_cmd) 203 | f.bind("", lambda e: e.widget.configure(relief="raised")) 204 | l.pack() 205 | f.grid(row=i % 2, column=i // 2, padx=2, pady=2) 206 | 207 | col_frame = ttk.Frame(self) 208 | # --- hsv 209 | hsv_frame = ttk.Frame(col_frame, relief="ridge", borderwidth=2) 210 | hsv_frame.pack(pady=(0, 4), fill="x") 211 | hsv_frame.columnconfigure(0, weight=1) 212 | self.hue = LimitVar(0, 360, self) 213 | self.saturation = LimitVar(0, 100, self) 214 | self.value = LimitVar(0, 100, self) 215 | 216 | s_h = Spinbox(hsv_frame, from_=0, to=360, width=4, name='spinbox', 217 | textvariable=self.hue, command=self._update_color_hsv) 218 | s_s = Spinbox(hsv_frame, from_=0, to=100, width=4, 219 | textvariable=self.saturation, name='spinbox', 220 | command=self._update_color_hsv) 221 | s_v = Spinbox(hsv_frame, from_=0, to=100, width=4, name='spinbox', 222 | textvariable=self.value, command=self._update_color_hsv) 223 | h, s, v = rgb_to_hsv(*self._old_color) 224 | s_h.delete(0, 'end') 225 | s_h.insert(0, h) 226 | s_s.delete(0, 'end') 227 | s_s.insert(0, s) 228 | s_v.delete(0, 'end') 229 | s_v.insert(0, v) 230 | s_h.grid(row=0, column=1, sticky='w', padx=4, pady=4) 231 | s_s.grid(row=1, column=1, sticky='w', padx=4, pady=4) 232 | s_v.grid(row=2, column=1, sticky='w', padx=4, pady=4) 233 | ttk.Label(hsv_frame, text=_('Hue')).grid(row=0, column=0, sticky='e', 234 | padx=4, pady=4) 235 | ttk.Label(hsv_frame, text=_('Saturation')).grid(row=1, column=0, sticky='e', 236 | padx=4, pady=4) 237 | ttk.Label(hsv_frame, text=_('Value')).grid(row=2, column=0, sticky='e', 238 | padx=4, pady=4) 239 | 240 | # --- rgb 241 | rgb_frame = ttk.Frame(col_frame, relief="ridge", borderwidth=2) 242 | rgb_frame.pack(pady=4, fill="x") 243 | rgb_frame.columnconfigure(0, weight=1) 244 | self.red = LimitVar(0, 255, self) 245 | self.green = LimitVar(0, 255, self) 246 | self.blue = LimitVar(0, 255, self) 247 | 248 | s_red = Spinbox(rgb_frame, from_=0, to=255, width=4, name='spinbox', 249 | textvariable=self.red, command=self._update_color_rgb) 250 | s_green = Spinbox(rgb_frame, from_=0, to=255, width=4, name='spinbox', 251 | textvariable=self.green, command=self._update_color_rgb) 252 | s_blue = Spinbox(rgb_frame, from_=0, to=255, width=4, name='spinbox', 253 | textvariable=self.blue, command=self._update_color_rgb) 254 | s_red.delete(0, 'end') 255 | s_red.insert(0, self._old_color[0]) 256 | s_green.delete(0, 'end') 257 | s_green.insert(0, self._old_color[1]) 258 | s_blue.delete(0, 'end') 259 | s_blue.insert(0, self._old_color[2]) 260 | s_red.grid(row=0, column=1, sticky='e', padx=4, pady=4) 261 | s_green.grid(row=1, column=1, sticky='e', padx=4, pady=4) 262 | s_blue.grid(row=2, column=1, sticky='e', padx=4, pady=4) 263 | ttk.Label(rgb_frame, text=_('Red')).grid(row=0, column=0, sticky='e', 264 | padx=4, pady=4) 265 | ttk.Label(rgb_frame, text=_('Green')).grid(row=1, column=0, sticky='e', 266 | padx=4, pady=4) 267 | ttk.Label(rgb_frame, text=_('Blue')).grid(row=2, column=0, sticky='e', 268 | padx=4, pady=4) 269 | # --- hexa 270 | hexa_frame = ttk.Frame(col_frame) 271 | hexa_frame.pack(fill="x") 272 | self.hexa = ttk.Entry(hexa_frame, justify="center", width=10, name='entry') 273 | self.hexa.insert(0, old_color.upper()) 274 | ttk.Label(hexa_frame, text=_("HTML")).pack(side="left", padx=4, pady=(4, 1)) 275 | self.hexa.pack(side="left", padx=6, pady=(4, 1), fill='x', expand=True) 276 | 277 | # --- alpha 278 | if alpha: 279 | alpha_frame = ttk.Frame(self) 280 | alpha_frame.columnconfigure(1, weight=1) 281 | self.alpha = LimitVar(0, 255, self) 282 | alphabar = ttk.Frame(alpha_frame, borderwidth=2, relief='groove') 283 | self.alphabar = AlphaBar(alphabar, alpha=self._old_alpha, width=200, 284 | color=self._old_color, highlightthickness=0) 285 | self.alphabar.pack() 286 | s_alpha = Spinbox(alpha_frame, from_=0, to=255, width=4, 287 | textvariable=self.alpha, command=self._update_alpha) 288 | s_alpha.delete(0, 'end') 289 | s_alpha.insert(0, self._old_alpha) 290 | alphabar.grid(row=0, column=0, padx=(0, 4), pady=4, sticky='w') 291 | ttk.Label(alpha_frame, text=_('Alpha')).grid(row=0, column=1, sticky='e', 292 | padx=4, pady=4) 293 | s_alpha.grid(row=0, column=2, sticky='w', padx=(4, 6), pady=4) 294 | 295 | # --- validation 296 | button_frame = ttk.Frame(self) 297 | if not self.modeless: 298 | ttk.Button(button_frame, text=_("OK"), width=25, 299 | command=self.ok).pack(side="right", padx=10) 300 | ttk.Button(button_frame, text=_("Cancel"), 301 | command=self.destroy).pack(side="right", padx=10) 302 | else: 303 | ttk.Button(button_frame, text=_("Copy Hex"), width=25, 304 | command=self.copy_hex).pack(side="right", padx=10) 305 | ttk.Button(button_frame, text=_("Copy RGB"), width=25, 306 | command=self.copy_rgb).pack(side="right", padx=10) 307 | ttk.Button(button_frame, text=_("Close"), 308 | command=self.destroy).pack(side="right", padx=10) 309 | # --- placement 310 | bar.grid(row=0, column=0, padx=10, pady=(10, 4), sticky='n') 311 | square.grid(row=1, column=0, padx=10, pady=(9, 0), sticky='n') 312 | if alpha: 313 | alpha_frame.grid(row=2, column=0, columnspan=2, padx=10, 314 | pady=(1, 4), sticky='ewn') 315 | col_frame.grid(row=0, rowspan=2, column=1, padx=(4, 10), pady=(10, 4)) 316 | frame.grid(row=3, column=0, columnspan=2, pady=(4, 10), padx=10, sticky="new") 317 | button_frame.grid(row=4, columnspan=2, pady=(0, 10), padx=10) 318 | 319 | # --- bindings 320 | self.bar.bind("", self._change_color, True) 321 | self.bar.bind("", self._unfocus, True) 322 | if alpha: 323 | self.alphabar.bind("", self._change_alpha, True) 324 | self.alphabar.bind("", self._unfocus, True) 325 | self.square.bind("", self._unfocus, True) 326 | self.square.bind("", self._change_sel_color, True) 327 | self.square.bind("", self._change_sel_color, True) 328 | s_red.bind('', self._update_color_rgb) 329 | s_green.bind('', self._update_color_rgb) 330 | s_blue.bind('', self._update_color_rgb) 331 | s_red.bind('', self._update_color_rgb) 332 | s_green.bind('', self._update_color_rgb) 333 | s_blue.bind('', self._update_color_rgb) 334 | s_red.bind('', self._select_all_spinbox) 335 | s_green.bind('', self._select_all_spinbox) 336 | s_blue.bind('', self._select_all_spinbox) 337 | s_h.bind('', self._update_color_hsv) 338 | s_s.bind('', self._update_color_hsv) 339 | s_v.bind('', self._update_color_hsv) 340 | s_h.bind('', self._update_color_hsv) 341 | s_s.bind('', self._update_color_hsv) 342 | s_v.bind('', self._update_color_hsv) 343 | s_h.bind('', self._select_all_spinbox) 344 | s_s.bind('', self._select_all_spinbox) 345 | s_v.bind('', self._select_all_spinbox) 346 | if alpha: 347 | s_alpha.bind('', self._update_alpha) 348 | s_alpha.bind('', self._update_alpha) 349 | s_alpha.bind('', self._select_all_spinbox) 350 | self.hexa.bind("", self._update_color_hexa) 351 | self.hexa.bind("", self._update_color_hexa) 352 | self.hexa.bind("", self._select_all_entry) 353 | 354 | self.hexa.focus_set() 355 | self.wait_visibility() 356 | self.lift() 357 | if not self.modeless: 358 | self.grab_set() 359 | 360 | def get_color(self): 361 | """Return selected color, return an empty string if no color is selected.""" 362 | return self.color 363 | 364 | @staticmethod 365 | def _select_all_spinbox(event): 366 | """Select all entry content.""" 367 | event.widget.selection('range', 0, 'end') 368 | return "break" 369 | 370 | @staticmethod 371 | def _select_all_entry(event): 372 | """Select all entry content.""" 373 | event.widget.selection_range(0, 'end') 374 | return "break" 375 | 376 | def _unfocus(self, event): 377 | """Unfocus palette items when click on bar or square.""" 378 | w = self.focus_get() 379 | if w != self and 'spinbox' not in str(w) and 'entry' not in str(w): 380 | self.focus_set() 381 | 382 | def _update_preview(self): 383 | """Update color preview.""" 384 | color = self.hexa.get() 385 | if self.alpha_channel: 386 | prev = overlay(self._transparent_bg, hexa_to_rgb(color)) 387 | self._im_color = ImageTk.PhotoImage(prev, master=self) 388 | self.color_preview.configure(image=self._im_color) 389 | else: 390 | self.color_preview.configure(background=color) 391 | 392 | def _reset_preview(self, event): 393 | """Respond to user click on a palette item.""" 394 | label = event.widget 395 | label.master.focus_set() 396 | label.master.configure(relief="sunken") 397 | args = self._old_color 398 | if self.alpha_channel: 399 | args += (self._old_alpha,) 400 | self.alpha.set(self._old_alpha) 401 | self.alphabar.set_color(args) 402 | color = rgb_to_hexa(*args) 403 | h, s, v = rgb_to_hsv(*self._old_color) 404 | self.red.set(self._old_color[0]) 405 | self.green.set(self._old_color[1]) 406 | self.blue.set(self._old_color[2]) 407 | self.hue.set(h) 408 | self.saturation.set(s) 409 | self.value.set(v) 410 | self.hexa.delete(0, "end") 411 | self.hexa.insert(0, color.upper()) 412 | self.bar.set(h) 413 | self.square.set_hsv((h, s, v)) 414 | self._update_preview() 415 | 416 | def _palette_cmd(self, event): 417 | """Respond to user click on a palette item.""" 418 | label = event.widget 419 | label.master.focus_set() 420 | label.master.configure(relief="sunken") 421 | r, g, b = self.winfo_rgb(label.cget("background")) 422 | r = round2(r * 255 / 65535) 423 | g = round2(g * 255 / 65535) 424 | b = round2(b * 255 / 65535) 425 | args = (r, g, b) 426 | if self.alpha_channel: 427 | a = self.alpha.get() 428 | args += (a,) 429 | self.alphabar.set_color(args) 430 | color = rgb_to_hexa(*args) 431 | h, s, v = rgb_to_hsv(r, g, b) 432 | self.red.set(r) 433 | self.green.set(g) 434 | self.blue.set(b) 435 | self.hue.set(h) 436 | self.saturation.set(s) 437 | self.value.set(v) 438 | self.hexa.delete(0, "end") 439 | self.hexa.insert(0, color.upper()) 440 | self.bar.set(h) 441 | self.square.set_hsv((h, s, v)) 442 | self._update_preview() 443 | 444 | def _change_sel_color(self, event): 445 | """Respond to motion of the color selection cross.""" 446 | (r, g, b), (h, s, v), color = self.square.get() 447 | self.red.set(r) 448 | self.green.set(g) 449 | self.blue.set(b) 450 | self.saturation.set(s) 451 | self.value.set(v) 452 | self.hexa.delete(0, "end") 453 | self.hexa.insert(0, color.upper()) 454 | if self.alpha_channel: 455 | self.alphabar.set_color((r, g, b)) 456 | self.hexa.insert('end', 457 | ("%2.2x" % self.alpha.get()).upper()) 458 | self._update_preview() 459 | 460 | def _change_color(self, event): 461 | """Respond to motion of the hsv cursor.""" 462 | h = self.bar.get() 463 | self.square.set_hue(h) 464 | (r, g, b), (h, s, v), sel_color = self.square.get() 465 | self.red.set(r) 466 | self.green.set(g) 467 | self.blue.set(b) 468 | self.hue.set(h) 469 | self.saturation.set(s) 470 | self.value.set(v) 471 | self.hexa.delete(0, "end") 472 | self.hexa.insert(0, sel_color.upper()) 473 | if self.alpha_channel: 474 | self.alphabar.set_color((r, g, b)) 475 | self.hexa.insert('end', 476 | ("%2.2x" % self.alpha.get()).upper()) 477 | self._update_preview() 478 | 479 | def _change_alpha(self, event): 480 | """Respond to motion of the alpha cursor.""" 481 | a = self.alphabar.get() 482 | self.alpha.set(a) 483 | hexa = self.hexa.get() 484 | hexa = hexa[:7] + ("%2.2x" % a).upper() 485 | self.hexa.delete(0, 'end') 486 | self.hexa.insert(0, hexa) 487 | self._update_preview() 488 | 489 | def _update_color_hexa(self, event=None): 490 | """Update display after a change in the HEX entry.""" 491 | color = self.hexa.get().upper() 492 | self.hexa.delete(0, 'end') 493 | self.hexa.insert(0, color) 494 | if re.match(r"^#[0-9A-F]{6}$", color): 495 | r, g, b = hexa_to_rgb(color) 496 | self.red.set(r) 497 | self.green.set(g) 498 | self.blue.set(b) 499 | h, s, v = rgb_to_hsv(r, g, b) 500 | self.hue.set(h) 501 | self.saturation.set(s) 502 | self.value.set(v) 503 | self.bar.set(h) 504 | self.square.set_hsv((h, s, v)) 505 | if self.alpha_channel: 506 | a = self.alpha.get() 507 | self.hexa.insert('end', ("%2.2x" % a).upper()) 508 | self.alphabar.set_color((r, g, b, a)) 509 | elif self.alpha_channel and re.match(r"^#[0-9A-F]{8}$", color): 510 | r, g, b, a = hexa_to_rgb(color) 511 | self.red.set(r) 512 | self.green.set(g) 513 | self.blue.set(b) 514 | self.alpha.set(a) 515 | self.alphabar.set_color((r, g, b, a)) 516 | h, s, v = rgb_to_hsv(r, g, b) 517 | self.hue.set(h) 518 | self.saturation.set(s) 519 | self.value.set(v) 520 | self.bar.set(h) 521 | self.square.set_hsv((h, s, v)) 522 | else: 523 | self._update_color_rgb() 524 | self._update_preview() 525 | 526 | def _update_alpha(self, event=None): 527 | """Update display after a change in the alpha spinbox.""" 528 | a = self.alpha.get() 529 | hexa = self.hexa.get() 530 | hexa = hexa[:7] + ("%2.2x" % a).upper() 531 | self.hexa.delete(0, 'end') 532 | self.hexa.insert(0, hexa) 533 | self.alphabar.set(a) 534 | self._update_preview() 535 | 536 | def _update_color_hsv(self, event=None): 537 | """Update display after a change in the HSV spinboxes.""" 538 | if event is None or event.widget.old_value != event.widget.get(): 539 | h = self.hue.get() 540 | s = self.saturation.get() 541 | v = self.value.get() 542 | sel_color = hsv_to_rgb(h, s, v) 543 | self.red.set(sel_color[0]) 544 | self.green.set(sel_color[1]) 545 | self.blue.set(sel_color[2]) 546 | if self.alpha_channel: 547 | sel_color += (self.alpha.get(),) 548 | self.alphabar.set_color(sel_color) 549 | hexa = rgb_to_hexa(*sel_color) 550 | self.hexa.delete(0, "end") 551 | self.hexa.insert(0, hexa) 552 | self.square.set_hsv((h, s, v)) 553 | self.bar.set(h) 554 | self._update_preview() 555 | 556 | def _update_color_rgb(self, event=None): 557 | """Update display after a change in the RGB spinboxes.""" 558 | if event is None or event.widget.old_value != event.widget.get(): 559 | r = self.red.get() 560 | g = self.green.get() 561 | b = self.blue.get() 562 | h, s, v = rgb_to_hsv(r, g, b) 563 | self.hue.set(h) 564 | self.saturation.set(s) 565 | self.value.set(v) 566 | args = (r, g, b) 567 | if self.alpha_channel: 568 | args += (self.alpha.get(),) 569 | self.alphabar.set_color(args) 570 | hexa = rgb_to_hexa(*args) 571 | self.hexa.delete(0, "end") 572 | self.hexa.insert(0, hexa) 573 | self.square.set_hsv((h, s, v)) 574 | self.bar.set(h) 575 | self._update_preview() 576 | 577 | def ok(self): 578 | rgb, hsv, hexa = self.square.get() 579 | if self.alpha_channel: 580 | hexa = self.hexa.get() 581 | rgb += (self.alpha.get(),) 582 | self.color = rgb, hsv, hexa 583 | self.destroy() 584 | 585 | def copy_hex(self): 586 | rgb, hsv, hexa = self.square.get() 587 | pyperclip.copy(hexa) 588 | 589 | def copy_rgb(self): 590 | rgb, hsv, hexa = self.square.get() 591 | pyperclip.copy("{}, {}, {}".format(*rgb)) 592 | 593 | def modeless_colorpicker(color="red", parent=None, title=_("Color Chooser"), alpha=False): 594 | """ 595 | Clipboard based ColorPicker, lets user "copy" selected color 596 | in hex notation or as 'r, g, b', and always returns None 597 | """ 598 | col = ColorPicker(parent, color, alpha, title, modeless=True) 599 | col.wait_window(col) 600 | return None 601 | 602 | def askcolor(color="red", parent=None, title=_("Color Chooser"), alpha=False): 603 | """ 604 | Open a ColorPicker dialog and return the chosen color. 605 | 606 | The selected color is retunred in RGB(A) and hexadecimal #RRGGBB(AA) formats. 607 | (None, None) is returned if the color selection is cancelled. 608 | 609 | Arguments: 610 | * color: initially selected color (RGB(A), hexa or tkinter color name) 611 | * parent: parent window 612 | * title: dialog title 613 | * alpha: alpha channel suppport 614 | """ 615 | col = ColorPicker(parent, color, alpha, title) 616 | col.wait_window(col) 617 | res = col.get_color() 618 | if res: 619 | return res[0], res[2] 620 | else: 621 | return None, None 622 | --------------------------------------------------------------------------------