├── standalone ├── icons.rc ├── 7z.sfx ├── icons.ico ├── create.py ├── version.rc ├── pyunity-editor.c └── pyunity_updater.py ├── pyunity_editor ├── theme │ ├── dark │ │ ├── transparent.svg │ │ ├── branch_closed.svg │ │ ├── branch_open-on.svg │ │ ├── branch_open.svg │ │ ├── stylesheet-vline.svg │ │ ├── branch_closed-on.svg │ │ ├── undock.svg │ │ ├── hsepartoolbar.svg │ │ ├── stylesheet-branch-end.svg │ │ ├── stylesheet-branch-more.svg │ │ ├── stylesheet-branch-end-closed.svg │ │ ├── stylesheet-branch-end-open.svg │ │ ├── sizegrip.svg │ │ ├── hmovetoolbar.svg │ │ ├── checkbox_unchecked.svg │ │ ├── checkbox_unchecked_disabled.svg │ │ ├── checkbox_checked.svg │ │ ├── radio_unchecked.svg │ │ ├── checkbox_checked_disabled.svg │ │ ├── undock-hover.svg │ │ ├── radio_unchecked_disabled.svg │ │ ├── radio_checked.svg │ │ ├── radio_checked_disabled.svg │ │ ├── right_arrow.svg │ │ ├── spinup_disabled.svg │ │ ├── right_arrow_disabled.svg │ │ ├── checkbox_indeterminate.svg │ │ ├── close.svg │ │ ├── checkbox_indeterminate_disabled.svg │ │ ├── close-hover.svg │ │ ├── close-pressed.svg │ │ ├── up_arrow.svg │ │ ├── down_arrow.svg │ │ ├── up_arrow-hover.svg │ │ ├── left_arrow.svg │ │ ├── up_arrow_disabled.svg │ │ ├── down_arrow-hover.svg │ │ ├── down_arrow_disabled.svg │ │ ├── left_arrow_disabled.svg │ │ ├── vsepartoolbars.svg │ │ └── vmovetoolbar.svg │ └── light │ │ ├── transparent.svg │ │ ├── branch_closed.svg │ │ ├── branch_open-on.svg │ │ ├── branch_open.svg │ │ ├── branch_closed-on.svg │ │ ├── stylesheet-vline.svg │ │ ├── undock.svg │ │ ├── stylesheet-branch-end.svg │ │ ├── stylesheet-branch-more.svg │ │ ├── hsepartoolbar.svg │ │ ├── stylesheet-branch-end-closed.svg │ │ ├── stylesheet-branch-end-open.svg │ │ ├── sizegrip.svg │ │ ├── hmovetoolbar.svg │ │ ├── checkbox_unchecked-hover.svg │ │ ├── checkbox_unchecked_disabled.svg │ │ ├── checkbox_checked.svg │ │ ├── checkbox_checked-hover.svg │ │ ├── checkbox_checked_disabled.svg │ │ ├── undock-hover.svg │ │ ├── radio_unchecked-hover.svg │ │ ├── radio_unchecked_disabled.svg │ │ ├── radio_checked.svg │ │ ├── radio_checked-hover.svg │ │ ├── radio_checked_disabled.svg │ │ ├── spinup_disabled.svg │ │ ├── right_arrow_disabled.svg │ │ ├── checkbox_indeterminate.svg │ │ ├── close.svg │ │ ├── checkbox_indeterminate-hover.svg │ │ ├── checkbox_indeterminate_disabled.svg │ │ ├── close-hover.svg │ │ ├── close-pressed.svg │ │ ├── up_arrow.svg │ │ ├── up_arrow-hover.svg │ │ ├── down_arrow.svg │ │ ├── right_arrow.svg │ │ ├── up_arrow_disabled.svg │ │ ├── down_arrow-hover.svg │ │ ├── left_arrow.svg │ │ ├── down_arrow_disabled.svg │ │ ├── left_arrow_disabled.svg │ │ ├── vsepartoolbars.svg │ │ └── vmovetoolbar.svg ├── __main__.py ├── icons │ ├── splash.png │ ├── buttons │ │ ├── pause.png │ │ ├── play.png │ │ └── stop.png │ ├── console │ │ ├── error.png │ │ ├── info.png │ │ ├── output.png │ │ ├── template.png │ │ └── warning.png │ ├── inspector │ │ └── add.png │ └── window │ │ ├── icon16x16.png │ │ ├── icon24x24.png │ │ ├── icon32x32.png │ │ ├── icon48x48.png │ │ ├── icon64x64.png │ │ ├── icon128x128.png │ │ └── icon256x256.png ├── __init__.py ├── splash.py ├── local.py ├── files.py ├── cli.py ├── resources.qrc ├── smoothScroll.py ├── views.py ├── app.py ├── window.py ├── render.py └── inspector.py ├── Test ├── Materials │ └── Side.mat ├── __main__.py ├── Scripts │ ├── Rotator.py │ ├── Oscillator2.py │ └── Oscillator.py ├── __init__.py ├── Meshes │ └── Side.mesh ├── Test.pyunity └── Scenes │ └── Scene.scene ├── cli.py ├── splash.py ├── requirements.txt ├── .github ├── dependabot.yml └── workflows │ ├── wheel.yml │ └── build.yml ├── .gitignore ├── setup.py ├── LICENSE ├── pyproject.toml ├── install.py ├── cube.py ├── README.md ├── contributing.md └── CODE_OF_CONDUCT.md /standalone/icons.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "icons.ico" -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Test/Materials/Side.mat: -------------------------------------------------------------------------------- 1 | Material 2 | texture: None 3 | color: RGB(200, 200, 200) -------------------------------------------------------------------------------- /standalone/7z.sfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/standalone/7z.sfx -------------------------------------------------------------------------------- /standalone/icons.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/standalone/icons.ico -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.argv.append("Test") 3 | 4 | from pyunity_editor.cli import run 5 | run() 6 | -------------------------------------------------------------------------------- /pyunity_editor/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /splash.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.argv.append("Test") 3 | 4 | from pyunity_editor.cli import main 5 | main() 6 | -------------------------------------------------------------------------------- /Test/__main__.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | from . import firstScene 3 | 4 | SceneManager.LoadScene(firstScene) 5 | -------------------------------------------------------------------------------- /pyunity_editor/icons/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/splash.png -------------------------------------------------------------------------------- /pyunity_editor/icons/buttons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/buttons/pause.png -------------------------------------------------------------------------------- /pyunity_editor/icons/buttons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/buttons/play.png -------------------------------------------------------------------------------- /pyunity_editor/icons/buttons/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/buttons/stop.png -------------------------------------------------------------------------------- /pyunity_editor/icons/console/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/console/error.png -------------------------------------------------------------------------------- /pyunity_editor/icons/console/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/console/info.png -------------------------------------------------------------------------------- /pyunity_editor/icons/console/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/console/output.png -------------------------------------------------------------------------------- /pyunity_editor/icons/inspector/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/inspector/add.png -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/branch_closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/branch_open-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/branch_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/branch_closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/branch_open-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/branch_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/icons/console/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/console/template.png -------------------------------------------------------------------------------- /pyunity_editor/icons/console/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/console/warning.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon16x16.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon24x24.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon32x32.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon48x48.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon64x64.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon128x128.png -------------------------------------------------------------------------------- /pyunity_editor/icons/window/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyunity/pyunity-gui/HEAD/pyunity_editor/icons/window/icon256x256.png -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/stylesheet-vline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/branch_closed-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/branch_closed-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/stylesheet-vline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyunity==0.9.0 2 | pyside6-essentials>=6.5.2 3 | pillow 4 | importlib_metadata; python_version < '3.8' 5 | PySideSix-Frameless-Window 6 | -------------------------------------------------------------------------------- /Test/Scripts/Rotator.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | 3 | class Rotator(Behaviour): 4 | def Update(self, dt): 5 | self.transform.eulerAngles += Vector3(0, 90, 135) * dt 6 | -------------------------------------------------------------------------------- /Test/__init__.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | import os 3 | 4 | project = Loader.LoadProject(os.path.abspath(os.path.dirname(__file__))) 5 | firstScene = SceneManager.GetSceneByIndex(0) 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/undock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/undock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/hsepartoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/stylesheet-branch-end.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/stylesheet-branch-more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/stylesheet-branch-end.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/stylesheet-branch-more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/stylesheet-branch-end-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/stylesheet-branch-end-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/hsepartoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/stylesheet-branch-end-closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/stylesheet-branch-end-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/sizegrip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/sizegrip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/hmovetoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/hmovetoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_unchecked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Test/Meshes/Side.mesh: -------------------------------------------------------------------------------- 1 | 1.0/1.0/0.0/1.0/-1.0/0.0/-1.0/-1.0/0.0/-1.0/1.0/0.0/1.0/1.0/0.0/1.0/-1.0/0.0/-1.0/-1.0/0.0/-1.0/1.0/0.0 2 | 0/1/2/0/2/3/4/6/5/4/7/6 3 | 0.0/0.0/-1.0/0.0/0.0/-1.0/0.0/0.0/-1.0/0.0/0.0/-1.0/0.0/0.0/1.0/0.0/0.0/1.0/0.0/0.0/1.0/0.0/0.0/1.0 4 | 0.0/0.0/0.0/1.0/1.0/1.0/1.0/0.0/0.0/0.0/0.0/1.0/1.0/1.0/1.0/0.0 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_unchecked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_unchecked-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_unchecked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" 2 | __copyright__ = "Copyright (c) 2020-2023 The PyUnity Team" 3 | __email__ = "tankimarshal2@gmail.com" 4 | __license__ = "MIT License" 5 | __summary__ = "An Editor for PyUnity in the style of the UnityEditor" 6 | __title__ = "pyunity-editor" 7 | __uri__ = "https://github.com/pyunity/pyunity-gui" 8 | -------------------------------------------------------------------------------- /Test/Scripts/Oscillator2.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | 3 | class Oscillator2(Behaviour): 4 | a = 0 5 | speed = ShowInInspector(int, 10) 6 | def Update(self, dt): 7 | self.a += dt * self.speed / 10 8 | size = Mathf.LerpUnclamped(Mathf.Cos(self.a), 0.75, 1) 9 | self.transform.localScale = Vector3.one() * size 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | assignees: 8 | - "rayzchen" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | assignees: 14 | - "rayzchen" 15 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/radio_unchecked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_checked-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_checked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/undock-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_checked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/undock-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/radio_unchecked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/radio_unchecked-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/radio_unchecked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Test/Scripts/Oscillator.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | 3 | class Oscillator(Behaviour): 4 | a = 0 5 | speed = ShowInInspector(int, 5) 6 | renderer = ShowInInspector(MeshRenderer) 7 | def Start(self): 8 | self.renderer.mat = Material(RGB(255, 0, 0)) 9 | 10 | def Update(self, dt): 11 | self.a += dt * self.speed / 10 12 | self.renderer.mat.color = HSV(self.a % 3 * 360, 100, 100) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python generated files 2 | **/*.pyc 3 | **/*.pyo 4 | **/__pycache__ 5 | 6 | # Builds 7 | *.egg-info 8 | build/ 9 | src/ 10 | **/.doctrees 11 | dist/ 12 | 13 | # Pytest and coverage 14 | .coverage 15 | coverage.xml 16 | htmlcov/ 17 | .pytest_cache/ 18 | 19 | # Replit 20 | .replit 21 | poetry.lock 22 | main.sh 23 | 24 | # Mypy 25 | .mypy_cache/ 26 | out/ 27 | 28 | # Misc 29 | venv/ 30 | .vscode/ 31 | docs/en/ 32 | -------------------------------------------------------------------------------- /Test/Test.pyunity: -------------------------------------------------------------------------------- 1 | Project 2 | name: Test 3 | firstScene: 0 4 | Files 5 | 06c89310-7041-4314-93e7-61d2087a5801: Scripts/Rotator.py 6 | bec0b07c-a62a-46ef-83c0-17fc576905d3: Scripts/Oscillator2.py 7 | fc463003-ce33-4408-998e-c7c21e404129: Materials/Side.mat 8 | e082e34b-5b4c-4927-8281-a7e0b18c7a23: Meshes/Side.mesh 9 | 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7: Scripts/Oscillator.py 10 | bb1e11ed-e89f-4d66-bc0f-0f4c59d285a6: Scenes/Scene.scene -------------------------------------------------------------------------------- /standalone/create.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import glob 3 | import os 4 | 5 | orig = os.getcwd() 6 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 7 | imgs = [] 8 | root = None 9 | for file in glob.glob("pyunity_editor\\icons\\window\\icon*.png"): 10 | img = Image.open(file) 11 | if "256" in file: 12 | root = img 13 | else: 14 | imgs.append(img) 15 | root.save("icons.ico", format="ICO", append_images=imgs) 16 | os.chdir(orig) 17 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/radio_checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/radio_checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/radio_checked-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/radio_checked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/radio_checked_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/spinup_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/spinup_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/right_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/right_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_indeterminate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_indeterminate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/checkbox_indeterminate_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/close-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_indeterminate-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/checkbox_indeterminate_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/close-pressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/close-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/close-pressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/up_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/up_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/down_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/up_arrow-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/up_arrow-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/left_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/up_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/down_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/up_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/down_arrow-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/down_arrow-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/left_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /standalone/version.rc: -------------------------------------------------------------------------------- 1 | 1 VERSIONINFO 2 | FILEVERSION 19,0,0,0 3 | PRODUCTVERSION 19,0,0,0 4 | FILEOS 0x40004 5 | FILETYPE 0x1 6 | { 7 | BLOCK "StringFileInfo" 8 | { 9 | BLOCK "040904B0" 10 | { 11 | VALUE "CompanyName", "The PyUnity Team" 12 | VALUE "FileDescription", "PyUnity Editor" 13 | VALUE "FileVersion", "19.00" 14 | VALUE "InternalName", "PyUnity Editor" 15 | VALUE "LegalCopyright", "Copyright (c) 2020-2023 The PyUnity Team" 16 | VALUE "OriginalFilename", "pyunity-editor.exe" 17 | VALUE "ProductName", "PyUnity" 18 | VALUE "ProductVersion", "19.00" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/down_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/left_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/down_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/left_arrow_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/vsepartoolbars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/vsepartoolbars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyunity_editor/theme/dark/vmovetoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /pyunity_editor/theme/light/vmovetoolbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import glob 3 | 4 | data_files = glob.glob("pyunity_editor/**/*.qss", recursive=True) + \ 5 | glob.glob("pyunity_editor/**/*.png", recursive=True) + \ 6 | ["pyunity_editor/resources.qrc"] 7 | 8 | setup( 9 | packages=["pyunity_editor"] + ["pyunity_editor." + package for package in find_packages(where="pyunity_editor")], 10 | package_data={"pyunity_editor": [file[15:] for file in data_files]}, 11 | entry_points={ 12 | "gui_scripts": [ 13 | "pyunity-editor=pyunity_editor.cli:gui" 14 | ], 15 | # "console_scripts": [ 16 | # "pyunity-editor=pyunity_editor.cli:run" 17 | # ], 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/wheel.yml: -------------------------------------------------------------------------------- 1 | name: Wheel build 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | build: 5 | runs-on: windows-latest 6 | name: Build python wheel 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Set up Python 10 | uses: actions/setup-python@v3 11 | with: 12 | python-version: 3.11 13 | architecture: x64 14 | - name: Install dependencies 15 | run: pip install -U wheel build[virtualenv] setuptools 16 | - name: Build pure wheel 17 | run: | 18 | python -m build 19 | - name: Upload python wheel 20 | uses: actions/upload-artifact@v3 21 | with: 22 | name: purepython 23 | path: dist/*.whl 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 The PyUnity Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "wheel>=0.29.0", 4 | "setuptools>=58.6.0" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "pyunity-editor" 10 | version = "0.1.1" 11 | authors = [{name = "The PyUnity Team", email = "tankimarshal2@gmail.com"}] 12 | description = "An Editor for PyUnity in the style of the UnityEditor" 13 | license = {text = "MIT"} 14 | classifiers = [ 15 | "Development Status :: 3 - Alpha", 16 | "Natural Language :: English", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: MacOS", 19 | "Operating System :: Microsoft :: Windows", 20 | "Operating System :: POSIX :: Linux", 21 | "Programming Language :: Python :: 3", 22 | ] 23 | requires-python = ">=3" 24 | dependencies = [ 25 | "pyunity>=0.9.0", 26 | "pyside6-essentials>=6.5.2", 27 | "pillow", 28 | "importlib_metadata; python_version < '3.8'", 29 | "PySideSix-Frameless-Window", 30 | ] 31 | 32 | [project.readme] 33 | file = "README.md" 34 | content-type = "text/markdown; charset=UTF-8" 35 | 36 | [project.urls] 37 | Homepage = "https://docs.pyunity.x10.bz/" 38 | Documentation = "https://docs.pyunity.x10.bz/" 39 | Source = "https://github.com/pyunity/pyunity-gui" 40 | Tracker = "https://github.com/pyunity/pyunity-gui/issues" 41 | 42 | [tool.setuptools] 43 | include-package-data = false 44 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import os 3 | import sys 4 | import tempfile 5 | import zipfile 6 | import shutil 7 | import sysconfig 8 | import subprocess 9 | 10 | plat = sysconfig.get_platform() 11 | if plat.startswith("win"): 12 | workflow = "windows" 13 | name = f"python{sys.version_info.major}.{sys.version_info.minor}" 14 | if plat == "win-amd64": 15 | name += "-x64" 16 | else: 17 | name += "-x86" 18 | elif plat.startswith("linux"): 19 | workflow = "linux" 20 | arch = plat.split("-", 1)[1] 21 | name = f"python{sys.version_info.major}.{sys.version_info.minor}-{arch}" 22 | elif plat.startswith("macos"): 23 | workflow = "macos" 24 | name = f"python{sys.version_info.major}.{sys.version_info.minor}" 25 | 26 | print(f"Target artifact: {name}.zip") 27 | 28 | tmp = tempfile.mkdtemp() 29 | orig = os.getcwd() 30 | os.chdir(tmp) 31 | try: 32 | urllib.request.urlretrieve(f"https://nightly.link/pyunity/pyunity/workflows/{workflow}/develop/{name}.zip", "artifact.zip") 33 | 34 | with zipfile.ZipFile(os.path.join(tmp, "artifact.zip")) as zf: 35 | files = zf.infolist() 36 | name = files[0].filename 37 | print(f"Target wheel: {name}") 38 | zf.extract(name) 39 | 40 | print("Installing wheel") 41 | subprocess.call([sys.executable, "-m", "pip", "uninstall", "-y", "pyunity"], 42 | stdout=sys.stdout, stderr=sys.stderr) 43 | subprocess.call([sys.executable, "-m", "pip", "install", "-U", name], 44 | stdout=sys.stdout, stderr=sys.stderr) 45 | finally: 46 | print("Cleaning up") 47 | os.chdir(orig) 48 | shutil.rmtree(tmp) 49 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | build: 5 | runs-on: windows-latest 6 | strategy: 7 | matrix: 8 | python-version: ["3.7.9", "3.8.10", "3.9.13", "3.10.11"] 9 | architecture: ["x64"] # disable x86 for now 10 | name: Python ${{ matrix.python-version }}-${{ matrix.architecture }} 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | architecture: ${{ matrix.architecture }} 18 | - name: Install dependencies 19 | run: pip install -U wheel build[virtualenv] setuptools 20 | - uses: ilammy/msvc-dev-cmd@v1 21 | name: Set up MSVC 22 | - name: Build application 23 | env: 24 | GITHUB_ACTIONS: 1 25 | PYTHON_VERSION: ${{ matrix.python-version }} 26 | PYTHON_ARCHITECTURE: ${{ matrix.architecture }} 27 | run: python builder.py 28 | - name: Upload zip artifact 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: Zip archive (${{ matrix.python-version }}-${{ matrix.architecture }}) 32 | path: pyunity-editor.zip 33 | - name: Upload 7z artifact 34 | uses: actions/upload-artifact@v3 35 | with: 36 | name: 7z archive (${{ matrix.python-version }}-${{ matrix.architecture }}) 37 | path: pyunity-editor.7z 38 | - name: Upload self-extracting artifact 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: Self-extracting archive (${{ matrix.python-version }}-${{ matrix.architecture }}) 42 | path: pyunity-editor-install.exe 43 | -------------------------------------------------------------------------------- /cube.py: -------------------------------------------------------------------------------- 1 | from pyunity import * 2 | 3 | class Oscillator(Behaviour): 4 | a = 0 5 | speed = ShowInInspector(int, 5) 6 | renderer = ShowInInspector(MeshRenderer) 7 | def Start(self): 8 | self.renderer.mat = Material(RGB(255, 0, 0)) 9 | 10 | def Update(self, dt): 11 | self.a += dt * self.speed / 10 12 | self.renderer.mat.color = HSV(self.a % 3 * 360, 100, 100) 13 | 14 | class Oscillator2(Behaviour): 15 | a = 0 16 | speed = ShowInInspector(int, 10) 17 | def Update(self, dt): 18 | self.a += dt * self.speed / 10 19 | size = Mathf.LerpUnclamped(Mathf.Cos(self.a), 0.75, 1) 20 | self.transform.localScale = Vector3.one() * size 21 | 22 | class Rotator(Behaviour): 23 | def Update(self, dt): 24 | self.transform.eulerAngles += Vector3(0, 90, 135) * dt 25 | 26 | scene = SceneManager.AddScene("Scene") 27 | scene.mainCamera.transform.position = Vector3(0, 5, -5) 28 | scene.mainCamera.transform.eulerAngles = Quaternion.Euler(Vector3(45, 0, 0)) 29 | scene.gameObjects[1].transform.eulerAngles = Quaternion.Euler(Vector3(75, -25, 0)) 30 | 31 | root = GameObject("Root") 32 | root.AddComponent(Rotator) 33 | root.AddComponent(Oscillator2) 34 | scene.Add(root) 35 | 36 | i = 0 37 | for direction in [Vector3.up(), Vector3.right(), Vector3.forward()]: 38 | for parity in [-1, 1]: 39 | i += 1 40 | side = direction * parity 41 | go = GameObject("Side", root) 42 | renderer = go.AddComponent(MeshRenderer) 43 | renderer.mesh = Loader.Primitives.double_quad 44 | oscillator = go.AddComponent(Oscillator) 45 | oscillator.renderer = renderer 46 | oscillator.speed = i 47 | go.transform.localPosition = side 48 | if direction == Vector3.forward(): 49 | angle = 0 50 | elif direction == Vector3.back(): 51 | angle = 180 52 | else: 53 | angle = 90 54 | go.transform.localRotation = Quaternion.FromAxis(angle, Vector3.forward().cross(side)) 55 | scene.Add(go) 56 | 57 | SceneManager.LoadScene(scene) 58 | Loader.GenerateProject("Test") 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyUnity Editor 2 | 3 | [![License](https://img.shields.io/pypi/l/pyunity-editor.svg?logo=python&logoColor=FBE072)](https://github.com/pyunity/pyunity-gui/blob/master/LICENSE) 4 | [![PyPI version](https://img.shields.io/pypi/v/pyunity-editor.svg?logo=python&logoColor=FBE072)](https://pypi.python.org/pypi/pyunity-gui) 5 | [![Python version](https://img.shields.io/pypi/pyversions/pyunity-editor.svg?logo=python&logoColor=FBE072)](https://pypi.python.org/pypi/pyunity-gui) 6 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/pyunity/pyunity-gui.svg?logo=lgtm)](https://lgtm.com/projects/g/pyunity/pyunity-gui/context:python) 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/pyunity/pyunity-gui.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pyunity/pyunity-gui/alerts/) 8 | [![Discord](https://img.shields.io/discord/835911328693616680?logo=discord&label=discord)](https://discord.gg/zTn48BEbF9) 9 | [![Gitter](https://badges.gitter.im/pyunity/community.svg)](https://gitter.im/pyunity/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 10 | [![GitHub Repo stars](https://img.shields.io/github/stars/pyunity/pyunity-gui?logo=github)](https://github.com/pyunity/pyunity-gui/stargazers) 11 | 12 | This is a pure Python editor to make [PyUnity](https://github.com/pyunity/pyunity) projects. 13 | PyUnity is a pure Python Game Engine that is only inspired by Unity and does not contain or is not a binding for Unity itself. 14 | Therefore, PyUnity Editor is also completely seperate from UnityEditor. 15 | 16 | ## Installing 17 | 18 | The PyPi package does not work with the latest releases of PyUnity, and as such the best way 19 | to use this editor is to clone this editor and regularly run `git pull` to update. From the repo, 20 | running `python -m pyunity_editor ProjectPath/` will work. To create a new project, run 21 | `python -m pyunity_editor --new ProjectPath/`. Note that this editor also relies on the `develop` branch 22 | of PyUnity, which can be fetched with `install.py`. Run this periodically in case of any errors. 23 | 24 | A full run would look something like this: 25 | 26 | ``` 27 | git clone https://github.com/pyunity/pyunity-gui/ 28 | cd pyunity-gui/ 29 | python install.py 30 | python -m pip install -r requirements.txt 31 | python -m pyunity_editor --new ProjectPath/ 32 | ``` 33 | 34 | ## Contributing 35 | 36 | If you would like to contribute, please 37 | first see the [contributing guidelines](https://github.com/pyunity/pyunity-gui/blob/master/contributing.md), 38 | check out the latest [issues](https://github.com/pyunity/pyunity-gui/issues) 39 | and then make a [pull request](https://github.com/pyunity/pyunity-gui/pulls). 40 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to PyUnity 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with Github 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `main`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. 21 | 6. Issue that pull request! 22 | 23 | ## Any contributions you make will be under the MIT Software License 24 | In short, when you submit code changes, your submissions will be understood under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](https://github.com/pyunity/pyunity-gui/issues) 27 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/pyunity/pyunity-gui/issues/new); it's that easy! 28 | 29 | **Great Bug Reports** tend to have: 30 | 31 | - A quick summary and/or background 32 | - Steps to reproduce 33 | - Be specific! 34 | - Give sample code if you can. Please try to provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). 35 | - What you expected would happen 36 | - What actually happens 37 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 38 | 39 | People *love* thorough bug reports. I'm not even kidding. 40 | 41 | ## Use a Consistent Coding Style 42 | 43 | * 4 spaces for indentation rather than tabs 44 | * Adhere to our naming convention (it's a little different to Python's standard one): 45 | 46 | - PascalCase for class names 47 | - camelCase for all functions and attributes, including Qt slots and event handlers 48 | 49 | * Add comments wherever needed, to explain what your changes do 50 | 51 | ## License 52 | By contributing, you agree that your contributions will be licensed under its MIT License. 53 | 54 | ## References 55 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md). -------------------------------------------------------------------------------- /pyunity_editor/splash.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import threading 4 | import pkgutil 5 | from .local import getPath 6 | 7 | splashPath = getPath("icons/splash.png") 8 | 9 | def tksplash(): 10 | print("Loading tkinter splash image") 11 | import tkinter 12 | from PIL import ImageTk, Image 13 | img = ImageTk.PhotoImage(Image.open(splashPath).resize((size, size))) 14 | 15 | root = tkinter.Tk() 16 | root.overrideredirect(1) 17 | 18 | screen_width = root.winfo_screenwidth() 19 | screen_height = root.winfo_screenheight() 20 | size = int(screen_height // 2) 21 | x = int(screen_width / 2 - size / 2) 22 | y = int(screen_height / 2 - size / 2) 23 | root.geometry(str(size) + "x" + str(size) + "+" + str(x) + "+" + str(y)) 24 | 25 | canvas = tkinter.Canvas(root, width=size, height=size, 26 | bd=0, highlightthickness=0, relief="ridge") 27 | canvas.pack() 28 | canvas.create_image(0, 0, anchor=tkinter.NW, image=img) 29 | 30 | while True: 31 | if os.getenv("PYUNITY_EDITOR_LOADED") == "1": 32 | break 33 | root.update() 34 | time.sleep(0.2) 35 | root.destroy() 36 | 37 | def sdlsplash(): 38 | print("Loading SDL2 splash image") 39 | import warnings 40 | import ctypes 41 | 42 | with warnings.catch_warnings(): 43 | warnings.filterwarnings("ignore") 44 | from sdl2.ext import Window 45 | from sdl2 import sdlimage 46 | import sdl2 47 | 48 | sdlimage.IMG_Init(sdlimage.IMG_INIT_PNG) 49 | img = sdlimage.IMG_Load(splashPath.encode()) 50 | 51 | sdl2.ext.init() 52 | dispMode = sdl2.SDL_DisplayMode() 53 | sdl2.SDL_GetCurrentDisplayMode(0, ctypes.byref(dispMode)) 54 | size = int(min(dispMode.w, dispMode.h) / 2) 55 | x = int(dispMode.w / 2 - size / 2) 56 | y = int(dispMode.h / 2 - size / 2) 57 | window = Window( 58 | "PyUnity Editor is loading...", 59 | (size, size), 60 | (x, y), 61 | sdl2.SDL_WINDOW_SHOWN 62 | | sdl2.SDL_WINDOW_BORDERLESS 63 | | sdl2.SDL_WINDOW_ALWAYS_ON_TOP 64 | ) 65 | window.create() 66 | window.show() 67 | 68 | renderer = sdl2.SDL_CreateRenderer(window.window, -1, sdl2.SDL_RENDERER_ACCELERATED) 69 | texture = sdl2.SDL_CreateTextureFromSurface(renderer, img) 70 | 71 | event = sdl2.SDL_Event() 72 | while True: 73 | if os.getenv("PYUNITY_EDITOR_LOADED") == "1": 74 | break 75 | sdl2.SDL_WaitEvent(ctypes.byref(event)) 76 | sdl2.SDL_RenderClear(renderer) 77 | sdl2.SDL_RenderCopy(renderer, 78 | texture, 79 | None, 80 | None) 81 | sdl2.SDL_RenderPresent(renderer) 82 | sdl2.SDL_DestroyTexture(texture) 83 | sdl2.SDL_DestroyRenderer(renderer) 84 | window.close() 85 | 86 | def splash(): 87 | if pkgutil.find_loader("sdl2") is not None: 88 | sdlsplash() 89 | elif pkgutil.find_loader("tkinter") is not None: 90 | tksplash() 91 | else: 92 | print("Could not find splash screen window provider") 93 | 94 | def start(func, args=[], kwargs={}): 95 | t = threading.Thread(target=splash) 96 | t.daemon = True 97 | t.start() 98 | func(*args, **kwargs) 99 | -------------------------------------------------------------------------------- /pyunity_editor/local.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from importlib.machinery import SourceFileLoader 3 | import importlib.util 4 | import zipimport 5 | import sys 6 | import io 7 | 8 | def redirect_out(stream): 9 | sys.stdout = stream 10 | sys.stderr = stream 11 | 12 | def restore_out(): 13 | sys.stdout = sys.__stdout__ 14 | sys.stderr = sys.__stderr__ 15 | 16 | # Problem: Need to import `pyunity.resources` for splash 17 | # image fetching, but that will load the entirety of 18 | # pyunity and take a long time. 19 | # Solution: Get the module of `pyunity` but don't execute 20 | # it. Import `pyunity.resources` and `pyunity.logger` 21 | # then reset and execute `pyunity` correctly later. 22 | 23 | packageSpec = importlib.util.find_spec("pyunity") 24 | if packageSpec is None: 25 | raise ModuleNotFoundError("No module named 'pyunity'") 26 | 27 | def importModule(submodule): 28 | folder = packageSpec.submodule_search_locations[0] 29 | if not Path(folder).exists(): 30 | loader = zipimport.zipimporter(folder) 31 | spec = loader.find_spec("pyunity." + submodule) 32 | else: 33 | loader = None 34 | for extension in [".py", ".pyc"]: 35 | path = Path(folder) / (submodule + extension) 36 | if path.exists(): 37 | loader = SourceFileLoader("pyunity." + submodule, str(path)) 38 | break 39 | if loader is None: 40 | return None 41 | spec = importlib.util.spec_from_loader("pyunity." + submodule, loader) 42 | if spec is None: 43 | raise ModuleNotFoundError("No module named " + repr("pyunity." + submodule)) 44 | module = importlib.util.module_from_spec(spec) 45 | try: 46 | spec.loader.exec_module(module) 47 | except (FileNotFoundError, zipimport.ZipImportError): 48 | return None 49 | return module 50 | 51 | # Import `pyunity.logger` into `pyunity.Logger` for 52 | # use by `pyunity.resources` 53 | logger = importModule("logger") 54 | if logger is None: 55 | raise Exception("Could not load asset resolver: pyunity.logger failed to load") 56 | sys.modules["pyunity.logger"] = logger 57 | sys.modules["pyunity.Logger"] = logger 58 | 59 | tempStream = io.StringIO() 60 | redirect_out(tempStream) 61 | 62 | # Get module but don't execute `pyunity` 63 | pyunity = importlib.util.module_from_spec(packageSpec) 64 | sys.modules["pyunity"] = pyunity 65 | resources = importModule("resources") 66 | if resources is None: 67 | raise Exception("Could not load asset resolver: pyunity.resources failed to load") 68 | sys.modules["pyunity.resources"] = resources 69 | loaded = False 70 | 71 | restore_out() 72 | 73 | # Code for asset resolver 74 | directory = Path.home() / ".pyunity" / ".editor" 75 | if not directory.is_dir(): 76 | directory.mkdir(parents=True) 77 | 78 | package = Path(__file__).resolve().parent 79 | if package.parent.name.endswith(".zip"): 80 | package = package.parent 81 | if not package.is_file(): 82 | raise Exception("Cannot find egg file") 83 | resolver = resources.ZipAssetResolver(directory, package, __package__) 84 | else: 85 | resolver = resources.PackageAssetResolver(directory, package) 86 | 87 | def getPath(local): 88 | # Most Qt functions cannot take Path arguments 89 | return str(resolver.getPath(local)) 90 | 91 | def fixPackage(): 92 | # Only load once 93 | global loaded 94 | if loaded: 95 | return 96 | sys.modules.pop("pyunity.Logger") 97 | packageSpec.loader.exec_module(sys.modules["pyunity"]) 98 | loaded = True 99 | -------------------------------------------------------------------------------- /pyunity_editor/files.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QTimer, Qt 2 | from PySide6.QtWidgets import QMessageBox 3 | from PySide6.QtGui import QFont 4 | from pyunity import Loader, Logger, Scripts, SceneManager 5 | import os 6 | import glob 7 | 8 | class FileTracker: 9 | font = QFont("Segoe UI", 12) 10 | states = { 11 | "modified": "Modifying", 12 | "deleted": "Deleting", 13 | "created": "Creating" 14 | } 15 | 16 | def __init__(self, path): 17 | self.app = None 18 | self.path = os.path.normpath(path) 19 | self.files = set(glob.glob(os.path.join(self.path, "**/*"), recursive=True)) 20 | self.times = {file: os.stat(file)[8] for file in self.files} 21 | self.changed = [] 22 | self.timer = QTimer() 23 | self.timer.timeout.connect(self.check) 24 | self.project = Loader.LoadProject(self.path) 25 | 26 | def check(self): 27 | files2 = set(glob.glob(os.path.join(self.path, "**/*"), recursive=True)) 28 | for file in self.files: 29 | if file not in files2: 30 | Logger.Log("Removed " + file) 31 | self.changed.append((file, "deleted")) 32 | elif self.times[file] < os.stat(file)[8]: 33 | Logger.Log("Modified " + file) 34 | self.changed.append((file, "modified")) 35 | self.times[file] = os.stat(file)[8] 36 | for file in files2 - self.files: 37 | Logger.Log("Created " + file) 38 | self.changed.append((file, "created")) 39 | self.times[file] = os.stat(file)[8] 40 | self.files = files2 41 | 42 | if self.app.activeWindow() is not None: 43 | scripts = [] 44 | for file in self.changed: 45 | if file[0].endswith(".py"): 46 | scripts.append(file[0]) 47 | message = QMessageBox() 48 | message.setText(self.states[file[1]] + " " + file[0]) 49 | message.setWindowTitle("Importing files...") 50 | message.setStandardButtons(QMessageBox.StandardButton.NoButton) 51 | message.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint) 52 | QTimer.singleShot(2000, lambda: message.done(0)) 53 | message.setFont(self.font) 54 | message.exec() 55 | 56 | if len(self.changed): 57 | if len(scripts): 58 | Scripts.Reset() 59 | Scripts.GenerateModule() 60 | for file in self.project.filePaths: 61 | if file.endswith(".py"): 62 | fullpath = self.project.path / os.path.normpath(file) 63 | Scripts.LoadScript(fullpath) 64 | selected = self.app.hierarchy_content.tree_widget.selectedItems() 65 | if len(selected): 66 | prevIDs = [] 67 | for item in selected: 68 | prevIDs.append(self.project._ids[item.gameObject]) 69 | else: 70 | prevIDs = None 71 | file = self.project.fileIDs[self.project._ids[self.app.loaded]].path 72 | SceneManager.RemoveScene(self.app.loaded) 73 | scene = Loader.LoadScene(self.project.path / file, self.project) 74 | self.app.loadScene(scene, prevIDs) 75 | 76 | self.changed = [] 77 | 78 | def start(self, delay): 79 | self.check() 80 | self.timer.start(int(delay * 1000)) 81 | 82 | def stop(self): 83 | self.timer.stop() 84 | -------------------------------------------------------------------------------- /pyunity_editor/cli.py: -------------------------------------------------------------------------------- 1 | # Disable window provider selection 2 | import os 3 | os.environ["PYUNITY_WINDOW_PROVIDER"] = "0" 4 | from .splash import start 5 | from .local import fixPackage, redirect_out, restore_out 6 | from time import strftime 7 | import argparse 8 | import sys 9 | import os 10 | import io 11 | 12 | class Parser(argparse.ArgumentParser): 13 | def __init__(self, **kwargs): 14 | kwargs["prog"] = os.path.basename(sys.argv[0]) 15 | if kwargs["prog"] == "__main__.py": 16 | kwargs["prog"] = "editor" 17 | super(Parser, self).__init__(**kwargs) 18 | self.add_argument("-n", "--new", 19 | action="store_true", help="Create a new PyUnity project") 20 | self.add_argument("-S", "--no-splash", action="store_false", dest="splash", 21 | help="Disable the splash image on launch") 22 | self.add_argument("project", help="Path to PyUnity project", nargs="?") 23 | self.gui = False 24 | 25 | def parse_args(self, args=None, namespace=None): 26 | args = super(Parser, self).parse_args(args, namespace) 27 | if args.project is None: 28 | restore_out() 29 | self.print_help() 30 | self.exit(0) 31 | if not args.new and not os.path.isdir(args.project): 32 | if self.gui: 33 | import ctypes 34 | ctypes.windll.user32.MessageBoxW(0, "Project not found", "Help", 0x10) 35 | self.exit(1) 36 | else: 37 | raise Exception("Project not found") 38 | return args 39 | 40 | def print_help(self): 41 | if self.gui: 42 | import ctypes 43 | ctypes.windll.user32.MessageBoxW(0, self.format_help(), "Help", 0x40) 44 | else: 45 | print(self.format_help()) 46 | 47 | def error(self, message): 48 | msg = f"{self.prog}: error: {message}" 49 | if self.gui: 50 | import ctypes 51 | ctypes.windll.user32.MessageBoxW(0, msg, "Error", 0x10) 52 | else: 53 | self.print_usage(sys.stderr) 54 | sys.stderr.write(msg + "\n") 55 | self.exit(2) 56 | 57 | parser = Parser(description="Launch the PyUnity editor") 58 | 59 | def run(args=None): 60 | if args is None: 61 | args = parser.parse_args() 62 | 63 | fixPackage() 64 | from pyunity import SceneManager, Loader 65 | if args.new: 66 | SceneManager.AddScene("Scene") 67 | Loader.GenerateProject(args.project) 68 | SceneManager.RemoveAllScenes() 69 | 70 | from .app import Application 71 | app = Application(args.project) 72 | app.start() 73 | 74 | def main(): 75 | args = parser.parse_args() 76 | if args.splash: 77 | start(run, args=[args]) 78 | else: 79 | run(args) 80 | 81 | def gui(): 82 | parser.gui = True 83 | args = parser.parse_args() 84 | 85 | def inner(): 86 | temp_stream = io.StringIO() 87 | redirect_out(temp_stream) 88 | from pyunity import Logger 89 | Logger.SetStream(temp_stream) 90 | fixPackage() 91 | 92 | directory = os.path.join(os.path.dirname(Logger.folder), "Editor", "Logs") 93 | os.makedirs(directory, exist_ok=True) 94 | path = os.path.join(directory, strftime("%Y-%m-%d %H-%M-%S") + ".log") 95 | f = open(path, "w+", buffering=1) 96 | 97 | temp_stream.seek(0) 98 | f.write(temp_stream.read()) 99 | temp_stream.close() 100 | redirect_out(f) 101 | Logger.SetStream(f) 102 | run(args) 103 | if args.splash: 104 | start(inner) 105 | else: 106 | inner() 107 | -------------------------------------------------------------------------------- /standalone/pyunity-editor.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #define Py_LIMITED_API 0x03060000 3 | #include 4 | 5 | #ifdef NOCONSOLE 6 | #include 7 | #define CHECK_ERROR() if (PyErr_Occurred() != NULL) { showError(); exit(1); } 8 | 9 | void showError() { 10 | printf("Error encountered\n"); 11 | SetEnvironmentVariable("PYUNITY_EDITOR_LOADED", "1"); 12 | PyObject *type, *value, *traceback; 13 | PyErr_Fetch(&type, &value, &traceback); 14 | PyErr_Print(); 15 | 16 | PyObject *tracebackModule = PyImport_ImportModule("traceback"); 17 | PyObject *formatFunc = PyObject_GetAttrString(tracebackModule, "format_exception"); 18 | Py_DecRef(tracebackModule); 19 | 20 | PyErr_NormalizeException(&type, &value, &traceback); 21 | PyException_SetTraceback(value, traceback); 22 | 23 | PyObject *lines = PyObject_CallFunctionObjArgs(formatFunc, value, NULL); 24 | PyObject *sep = PyUnicode_FromString(""); 25 | PyObject *joined = PyUnicode_Join(sep, lines); 26 | Py_DecRef(sep); 27 | Py_DecRef(lines); 28 | 29 | wchar_t *msg = PyUnicode_AsWideCharString(joined, NULL); 30 | MessageBoxW(NULL, msg, L"Error loading PyUnity Editor", 0x10L); 31 | PyMem_Free(msg); 32 | 33 | PyErr_Restore(type, value, traceback); 34 | } 35 | #else 36 | #define CHECK_ERROR() if (PyErr_Occurred() != NULL) { PyErr_Print(); exit(1); } 37 | #endif 38 | 39 | int main(int argc, char **argv) { 40 | wchar_t *path = Py_DecodeLocale("Lib\\python.zip;Lib\\;Lib\\win32", NULL); 41 | Py_SetPath(path); 42 | 43 | wchar_t **program = (wchar_t**)PyMem_Malloc(sizeof(wchar_t**) * argc); 44 | for (int i = 0; i < argc; i++) { 45 | program[i] = Py_DecodeLocale(argv[i], NULL); 46 | } 47 | if (program[0] == NULL) { 48 | #ifdef NOCONSOLE 49 | MessageBoxW(NULL, L"Fatal error: cannot decode argv[0]", L"Error loading PyUnity Editor", 0x10L); 50 | #else 51 | fprintf(stderr, "Fatal error: cannot decode argv[0]\n"); 52 | #endif 53 | exit(1); 54 | } 55 | Py_SetProgramName(program[0]); 56 | Py_Initialize(); 57 | PySys_SetArgvEx(argc, program, 0); 58 | CHECK_ERROR(); 59 | 60 | PyObject *left = Py_BuildValue("u", program[1]); 61 | PyObject *right1 = Py_BuildValue("s", "-i"); 62 | PyObject *right2 = Py_BuildValue("s", "--interactive"); 63 | if (PyUnicode_Compare(left, right1) == 0 || 64 | PyUnicode_Compare(left, right2) == 0) { 65 | #ifdef NOCONSOLE 66 | if (AllocConsole() == 0) { 67 | MessageBoxW(NULL, L"Cannot allocate console", L"Error loading PyUnity Editor", 0x10L); 68 | exit(1); 69 | } 70 | #endif 71 | program[1] = Py_DecodeLocale("-E", NULL); 72 | int retcode = Py_Main(argc, program); 73 | exit(retcode); 74 | } 75 | PyObject *right3 = Py_BuildValue("s", "-U"); 76 | PyObject *right4 = Py_BuildValue("s", "--update"); 77 | if (PyUnicode_Compare(left, right3) == 0 || 78 | PyUnicode_Compare(left, right4) == 0) { 79 | PyObject *updater = PyImport_ImportModule("pyunity_updater"); 80 | CHECK_ERROR(); 81 | PyObject *func = PyObject_GetAttrString(updater, "main"); 82 | CHECK_ERROR(); 83 | PyObject_CallFunction(func, NULL); 84 | CHECK_ERROR(); 85 | } else { 86 | PyObject *editor = PyImport_ImportModule("pyunity_editor.cli"); 87 | CHECK_ERROR(); 88 | 89 | #ifdef NOCONSOLE 90 | PyObject *func = PyObject_GetAttrString(editor, "gui"); 91 | #else 92 | PyObject *func = PyObject_GetAttrString(editor, "run"); 93 | #endif 94 | CHECK_ERROR(); 95 | 96 | PyObject_CallFunction(func, NULL); 97 | CHECK_ERROR(); 98 | } 99 | 100 | if (Py_FinalizeEx() < 0) { 101 | exit(1); 102 | } 103 | for (int i = 0; i < argc; i++) { 104 | PyMem_Free((void*)program[i]); 105 | } 106 | PyMem_Free((void*)program); 107 | PyMem_Free((void*)path); 108 | printf("Safely freed memory\n"); 109 | return 0; 110 | } 111 | 112 | #ifdef NOCONSOLE 113 | int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 114 | char* pCmdLine, int nShowCmd) { 115 | return main(__argc, __argv); 116 | } 117 | #endif 118 | -------------------------------------------------------------------------------- /pyunity_editor/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | theme/light/hmovetoolbar.svg 4 | theme/light/vmovetoolbar.svg 5 | theme/light/hsepartoolbar.svg 6 | theme/light/vsepartoolbars.svg 7 | theme/light/stylesheet-branch-end.svg 8 | theme/light/stylesheet-branch-end-closed.svg 9 | theme/light/stylesheet-branch-end-open.svg 10 | theme/light/stylesheet-vline.svg 11 | theme/light/stylesheet-branch-more.svg 12 | theme/light/branch_closed.svg 13 | theme/light/branch_closed-on.svg 14 | theme/light/branch_open.svg 15 | theme/light/branch_open-on.svg 16 | theme/light/down_arrow.svg 17 | theme/light/down_arrow_disabled.svg 18 | theme/light/down_arrow-hover.svg 19 | theme/light/left_arrow.svg 20 | theme/light/left_arrow_disabled.svg 21 | theme/light/right_arrow.svg 22 | theme/light/right_arrow_disabled.svg 23 | theme/light/up_arrow.svg 24 | theme/light/up_arrow_disabled.svg 25 | theme/light/up_arrow-hover.svg 26 | theme/light/sizegrip.svg 27 | theme/light/transparent.svg 28 | theme/light/close.svg 29 | theme/light/close-hover.svg 30 | theme/light/close-pressed.svg 31 | theme/light/undock.svg 32 | theme/light/undock-hover.svg 33 | theme/light/checkbox_checked-hover.svg 34 | theme/light/checkbox_checked.svg 35 | theme/light/checkbox_checked_disabled.svg 36 | theme/light/checkbox_indeterminate.svg 37 | theme/light/checkbox_indeterminate-hover.svg 38 | theme/light/checkbox_indeterminate_disabled.svg 39 | theme/light/checkbox_unchecked-hover.svg 40 | theme/light/checkbox_unchecked_disabled.svg 41 | theme/light/radio_checked-hover.svg 42 | theme/light/radio_checked.svg 43 | theme/light/radio_checked_disabled.svg 44 | theme/light/radio_unchecked-hover.svg 45 | theme/light/radio_unchecked_disabled.svg 46 | theme/dark/hmovetoolbar.svg 47 | theme/dark/vmovetoolbar.svg 48 | theme/dark/hsepartoolbar.svg 49 | theme/dark/vsepartoolbars.svg 50 | theme/dark/stylesheet-branch-end.svg 51 | theme/dark/stylesheet-branch-end-closed.svg 52 | theme/dark/stylesheet-branch-end-open.svg 53 | theme/dark/stylesheet-vline.svg 54 | theme/dark/stylesheet-branch-more.svg 55 | theme/dark/branch_closed.svg 56 | theme/dark/branch_closed-on.svg 57 | theme/dark/branch_open.svg 58 | theme/dark/branch_open-on.svg 59 | theme/dark/down_arrow.svg 60 | theme/dark/down_arrow_disabled.svg 61 | theme/dark/down_arrow-hover.svg 62 | theme/dark/left_arrow.svg 63 | theme/dark/left_arrow_disabled.svg 64 | theme/dark/right_arrow.svg 65 | theme/dark/right_arrow_disabled.svg 66 | theme/dark/up_arrow.svg 67 | theme/dark/up_arrow_disabled.svg 68 | theme/dark/up_arrow-hover.svg 69 | theme/dark/sizegrip.svg 70 | theme/dark/transparent.svg 71 | theme/dark/close.svg 72 | theme/dark/close-hover.svg 73 | theme/dark/close-pressed.svg 74 | theme/dark/undock.svg 75 | theme/dark/undock-hover.svg 76 | theme/dark/checkbox_checked.svg 77 | theme/dark/checkbox_checked_disabled.svg 78 | theme/dark/checkbox_indeterminate.svg 79 | theme/dark/checkbox_indeterminate_disabled.svg 80 | theme/dark/checkbox_unchecked.svg 81 | theme/dark/checkbox_unchecked_disabled.svg 82 | theme/dark/radio_checked.svg 83 | theme/dark/radio_checked_disabled.svg 84 | theme/dark/radio_unchecked.svg 85 | theme/dark/radio_unchecked_disabled.svg 86 | theme/light.qss 87 | theme/dark.qss 88 | 89 | -------------------------------------------------------------------------------- /pyunity_editor/smoothScroll.py: -------------------------------------------------------------------------------- 1 | __all__ = ["SmoothMode", "QAbstractSmoothScroller", "SmoothScroller", "QSmoothScrollArea", 2 | "QSmoothListWidget", "QSmoothTreeWidget"] 3 | 4 | from PySide6.QtCore import QTimer, Qt, QDateTime, QPoint 5 | from PySide6.QtWidgets import ( 6 | QAbstractScrollArea, QAbstractItemView, QApplication, QScrollArea, QListWidget, QTreeWidget) 7 | from PySide6.QtGui import QWheelEvent 8 | import math 9 | import enum 10 | 11 | class SmoothMode(enum.Enum): 12 | NO_SMOOTH = enum.auto() 13 | CONSTANT = enum.auto() 14 | LINEAR = enum.auto() 15 | QUADRATIC = enum.auto() 16 | COSINE = enum.auto() 17 | 18 | class WheelEventProxy: 19 | def __init__(self, event): 20 | self.position = event.position() 21 | self.globalPosition = event.globalPosition() 22 | self.buttons = event.buttons() 23 | self.phase = event.phase() 24 | self.inverted = event.inverted() 25 | self.source = event.source() 26 | 27 | class QAbstractSmoothScroller(QAbstractScrollArea): 28 | pass 29 | 30 | def SmoothScroller(cls): 31 | if QAbstractScrollArea not in cls.__mro__: 32 | raise Exception("Cannot create SmoothScroller for a class that does not " 33 | "inherit QAbstractScrollArea") 34 | 35 | def __init__(self, parent=None): 36 | cls.__bases__[0].__init__(self, parent) 37 | if issubclass(cls, QAbstractItemView): 38 | self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) 39 | self.lastWheelEvent = 0 40 | self.smoothMoveTimer = QTimer(self) 41 | self.smoothMoveTimer.timeout.connect(self.slotSmoothMove) 42 | 43 | self.scrollRatio = 1 44 | self.fps = 60 45 | self.duration = 200 46 | self.smoothMode = SmoothMode.COSINE 47 | self.acceleration = 0.5 48 | 49 | self.smallStepModifier = Qt.ShiftModifier 50 | self.smallStepRatio = 1/5 51 | self.bigStepModifier = Qt.AltModifier 52 | self.bigStepRatio = 5 53 | 54 | self.scrollStamps = [] 55 | self.stepsLeftQueue = [] 56 | 57 | def wheelEvent(self, event): 58 | if self.smoothMode == SmoothMode.NO_SMOOTH: 59 | cls.__bases__[0].wheelEvent(self, event) 60 | return 61 | 62 | now = QDateTime.currentDateTime().toMSecsSinceEpoch() 63 | self.scrollStamps.append(now) 64 | while now - self.scrollStamps[0] > 500: 65 | self.scrollStamps.pop(0) 66 | accelerationRatio = min(len(self.scrollStamps) / 15, 1) 67 | 68 | self.lastWheelEvent = WheelEventProxy(event) 69 | 70 | self.stepsTotal = self.fps * self.duration // 1000 71 | multiplier = self.scrollRatio 72 | delta = event.angleDelta().y() 73 | if QApplication.keyboardModifiers() & self.smallStepModifier: 74 | multiplier *= self.smallStepRatio 75 | if QApplication.keyboardModifiers() & self.bigStepModifier: 76 | multiplier *= self.bigStepRatio 77 | delta = event.angleDelta().x() 78 | delta *= multiplier 79 | if self.acceleration > 0: 80 | delta += delta * self.acceleration * accelerationRatio 81 | 82 | self.stepsLeftQueue.append([delta, self.stepsTotal]) 83 | self.smoothMoveTimer.start(1000 // self.fps) 84 | 85 | def slotSmoothMove(self): 86 | totalDelta = 0 87 | for pair in self.stepsLeftQueue: 88 | totalDelta += self.subDelta(*pair) 89 | pair[1] -= 1 90 | 91 | while len(self.stepsLeftQueue) and self.stepsLeftQueue[0][1] == 0: 92 | self.stepsLeftQueue.pop(0) 93 | 94 | event = QWheelEvent( 95 | self.lastWheelEvent.position, 96 | self.lastWheelEvent.globalPosition, 97 | QPoint(0, 0), 98 | QPoint(0, round(totalDelta)), 99 | self.lastWheelEvent.buttons, 100 | Qt.NoModifier, 101 | self.lastWheelEvent.phase, 102 | self.lastWheelEvent.inverted, 103 | self.lastWheelEvent.source 104 | ) 105 | QApplication.sendEvent(self.verticalScrollBar(), event) 106 | 107 | if not self.stepsLeftQueue: 108 | self.smoothMoveTimer.stop() 109 | 110 | def subDelta(self, delta, stepsLeft): 111 | assert self.smoothMode != SmoothMode.NO_SMOOTH 112 | 113 | m = self.stepsTotal / 2 114 | x = abs(self.stepsTotal - stepsLeft - m) 115 | 116 | if self.smoothMode == SmoothMode.CONSTANT: 117 | return delta / self.stepsTotal 118 | elif self.smoothMode == SmoothMode.LINEAR: 119 | return 2 * delta / self.stepsTotal * (m - x) / m 120 | elif self.smoothMode == SmoothMode.QUADRATIC: 121 | return 0.75 / m * (1 - x * x / m / m) * delta 122 | elif self.smoothMode == SmoothMode.COSINE: 123 | return (math.cos(x * math.pi / m) + 1) / (2 * m) * delta 124 | return 0 125 | 126 | for func in [__init__, wheelEvent, slotSmoothMove, subDelta]: 127 | setattr(cls, func.__name__, func) 128 | 129 | return cls 130 | 131 | @SmoothScroller 132 | class QSmoothScrollArea(QScrollArea): 133 | pass 134 | 135 | @SmoothScroller 136 | class QSmoothListWidget(QListWidget): 137 | pass 138 | 139 | @SmoothScroller 140 | class QSmoothTreeWidget(QTreeWidget): 141 | pass 142 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | our [discord server](https://discord.gg/zTn48BEbF9). 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. -------------------------------------------------------------------------------- /Test/Scenes/Scene.scene: -------------------------------------------------------------------------------- 1 | Scene : bb1e11ed-e89f-4d66-bc0f-0f4c59d285a6 2 | name: "Scene" 3 | mainCamera: 491d1e2b-1ec1-49e3-8e2a-59cef37ec44d 4 | GameObject : 098b57e9-c1a7-422b-acb9-1a68a586177f 5 | name: "Main Camera" 6 | tag: 0 7 | enabled: True 8 | transform: ac2460f2-2906-4292-9b04-e0c69884567a 9 | GameObject : 2108d695-126d-4296-8bb3-2ab1934aa438 10 | name: "Light" 11 | tag: 0 12 | enabled: True 13 | transform: 7ef6a6ce-b613-475f-93d2-f090caffd65e 14 | GameObject : ac89cb6e-c1d5-4109-8449-3c8dd9bfc503 15 | name: "Root" 16 | tag: 0 17 | enabled: True 18 | transform: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 19 | GameObject : 5006ffc0-556c-4369-a428-e88d16154487 20 | name: "Side" 21 | tag: 0 22 | enabled: True 23 | transform: 0724bb71-9f58-4e77-900b-83fc8d41a363 24 | GameObject : 38b8bdae-b0cf-45cd-8c4b-c85757c153fd 25 | name: "Side" 26 | tag: 0 27 | enabled: True 28 | transform: defc196e-0401-46ae-9638-adba0a56b1a3 29 | GameObject : 40e94d91-c0e1-4388-8322-41e70233de3a 30 | name: "Side" 31 | tag: 0 32 | enabled: True 33 | transform: 21a7e29d-26fe-4762-94dd-52c99a84dc57 34 | GameObject : 323d3480-3fe3-4951-a4b1-cbf8773ae4cf 35 | name: "Side" 36 | tag: 0 37 | enabled: True 38 | transform: 08aa3348-1f43-4d69-843a-a27a66879b1f 39 | GameObject : b2526374-81c1-41c9-858f-ac4336bf94a9 40 | name: "Side" 41 | tag: 0 42 | enabled: True 43 | transform: e09dd89f-65a7-47f2-a874-95e52fc68b23 44 | GameObject : af402935-72d3-4305-92e8-09649849e5e9 45 | name: "Side" 46 | tag: 0 47 | enabled: True 48 | transform: ba2eef43-9449-4d76-84a9-36f6331a3eea 49 | Transform(Component) : ac2460f2-2906-4292-9b04-e0c69884567a 50 | gameObject: 098b57e9-c1a7-422b-acb9-1a68a586177f 51 | enabled: True 52 | localPosition: Vector3(0, 5, -5) 53 | localRotation: Quaternion(0.9238795325112867, 0.3826834323650898, 0, 0) 54 | localScale: Vector3(1, 1, 1) 55 | parent: None 56 | Camera(Component) : 491d1e2b-1ec1-49e3-8e2a-59cef37ec44d 57 | gameObject: 098b57e9-c1a7-422b-acb9-1a68a586177f 58 | enabled: True 59 | near: 0.05 60 | far: 200 61 | clearColor: RGB(255, 255, 255) 62 | skyboxEnabled: False 63 | ortho: False 64 | shadows: True 65 | depthMapSize: 1024 66 | fov: 90 67 | orthoSize: 5 68 | canvas: None 69 | AudioListener(Component) : fa0b59b8-4419-4431-a58e-a1291fda1935 70 | gameObject: 098b57e9-c1a7-422b-acb9-1a68a586177f 71 | enabled: True 72 | Transform(Component) : 7ef6a6ce-b613-475f-93d2-f090caffd65e 73 | gameObject: 2108d695-126d-4296-8bb3-2ab1934aa438 74 | enabled: True 75 | localPosition: Vector3(10, 10, 10) 76 | localRotation: Quaternion(0.7745476983615946, 0.5943313524298387, -0.17171309068913929, 0.13176008867505531) 77 | localScale: Vector3(1, 1, 1) 78 | parent: None 79 | Light(Component) : bae19593-7be0-4d60-8789-dffee30b886d 80 | gameObject: 2108d695-126d-4296-8bb3-2ab1934aa438 81 | enabled: True 82 | intensity: 20 83 | color: RGB(255, 255, 255) 84 | type: 1 85 | Transform(Component) : d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 86 | gameObject: ac89cb6e-c1d5-4109-8449-3c8dd9bfc503 87 | enabled: True 88 | localPosition: Vector3(0, 0, 0) 89 | localRotation: Quaternion(1, 0, 0, 0) 90 | localScale: Vector3(1, 1, 1) 91 | parent: None 92 | Rotator(Behaviour) : e09b9f76-e02e-4e69-99a8-cfb08f978f2f 93 | gameObject: ac89cb6e-c1d5-4109-8449-3c8dd9bfc503 94 | enabled: True 95 | _script: 06c89310-7041-4314-93e7-61d2087a5801 96 | Oscillator2(Behaviour) : 967d6c67-e62e-47a6-92a6-42435e7ef6ef 97 | gameObject: ac89cb6e-c1d5-4109-8449-3c8dd9bfc503 98 | enabled: True 99 | speed: 10 100 | _script: bec0b07c-a62a-46ef-83c0-17fc576905d3 101 | Transform(Component) : 0724bb71-9f58-4e77-900b-83fc8d41a363 102 | gameObject: 5006ffc0-556c-4369-a428-e88d16154487 103 | enabled: True 104 | localPosition: Vector3(0, -1, 0) 105 | localRotation: Quaternion(0.7071067811865476, 0.7071067811865475, 0, 0) 106 | localScale: Vector3(1, 1, 1) 107 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 108 | MeshRenderer(Component) : 82844fba-0692-4add-88f8-a212d17f8bcf 109 | gameObject: 5006ffc0-556c-4369-a428-e88d16154487 110 | enabled: True 111 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 112 | mat: fc463003-ce33-4408-998e-c7c21e404129 113 | Oscillator(Behaviour) : 44bf8003-5248-4aad-a1d3-1b98a3b6221a 114 | gameObject: 5006ffc0-556c-4369-a428-e88d16154487 115 | enabled: True 116 | speed: 1 117 | renderer: 82844fba-0692-4add-88f8-a212d17f8bcf 118 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 119 | Transform(Component) : defc196e-0401-46ae-9638-adba0a56b1a3 120 | gameObject: 38b8bdae-b0cf-45cd-8c4b-c85757c153fd 121 | enabled: True 122 | localPosition: Vector3(0, 1, 0) 123 | localRotation: Quaternion(0.7071067811865476, -0.7071067811865475, 0, 0) 124 | localScale: Vector3(1, 1, 1) 125 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 126 | MeshRenderer(Component) : 741173ba-3ad3-4b29-85a3-a8a985b7fcd7 127 | gameObject: 38b8bdae-b0cf-45cd-8c4b-c85757c153fd 128 | enabled: True 129 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 130 | mat: fc463003-ce33-4408-998e-c7c21e404129 131 | Oscillator(Behaviour) : 4e20485c-4c6a-4c06-bcf6-cd53e1941ef2 132 | gameObject: 38b8bdae-b0cf-45cd-8c4b-c85757c153fd 133 | enabled: True 134 | speed: 2 135 | renderer: 741173ba-3ad3-4b29-85a3-a8a985b7fcd7 136 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 137 | Transform(Component) : 21a7e29d-26fe-4762-94dd-52c99a84dc57 138 | gameObject: 40e94d91-c0e1-4388-8322-41e70233de3a 139 | enabled: True 140 | localPosition: Vector3(-1, 0, 0) 141 | localRotation: Quaternion(0.7071067811865476, 0, -0.7071067811865475, 0) 142 | localScale: Vector3(1, 1, 1) 143 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 144 | MeshRenderer(Component) : d0bd943b-262f-4472-a47d-63134529efa8 145 | gameObject: 40e94d91-c0e1-4388-8322-41e70233de3a 146 | enabled: True 147 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 148 | mat: fc463003-ce33-4408-998e-c7c21e404129 149 | Oscillator(Behaviour) : 1eed5373-5f39-46bb-92bd-d66901e96d05 150 | gameObject: 40e94d91-c0e1-4388-8322-41e70233de3a 151 | enabled: True 152 | speed: 3 153 | renderer: d0bd943b-262f-4472-a47d-63134529efa8 154 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 155 | Transform(Component) : 08aa3348-1f43-4d69-843a-a27a66879b1f 156 | gameObject: 323d3480-3fe3-4951-a4b1-cbf8773ae4cf 157 | enabled: True 158 | localPosition: Vector3(1, 0, 0) 159 | localRotation: Quaternion(0.7071067811865476, 0, 0.7071067811865475, 0) 160 | localScale: Vector3(1, 1, 1) 161 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 162 | MeshRenderer(Component) : 3e14024d-508e-4047-8a5c-90106385cc20 163 | gameObject: 323d3480-3fe3-4951-a4b1-cbf8773ae4cf 164 | enabled: True 165 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 166 | mat: fc463003-ce33-4408-998e-c7c21e404129 167 | Oscillator(Behaviour) : d8aeefc2-3647-465e-9547-02cb7712e941 168 | gameObject: 323d3480-3fe3-4951-a4b1-cbf8773ae4cf 169 | enabled: True 170 | speed: 4 171 | renderer: 3e14024d-508e-4047-8a5c-90106385cc20 172 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 173 | Transform(Component) : e09dd89f-65a7-47f2-a874-95e52fc68b23 174 | gameObject: b2526374-81c1-41c9-858f-ac4336bf94a9 175 | enabled: True 176 | localPosition: Vector3(0, 0, -1) 177 | localRotation: Quaternion(1, 0, 0, 0) 178 | localScale: Vector3(1, 1, 1) 179 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 180 | MeshRenderer(Component) : 3b0b7298-9fe6-4d42-8d01-733fc92ed06a 181 | gameObject: b2526374-81c1-41c9-858f-ac4336bf94a9 182 | enabled: True 183 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 184 | mat: fc463003-ce33-4408-998e-c7c21e404129 185 | Oscillator(Behaviour) : b4805415-9b93-4c40-8540-4e672e654eed 186 | gameObject: b2526374-81c1-41c9-858f-ac4336bf94a9 187 | enabled: True 188 | speed: 5 189 | renderer: 3b0b7298-9fe6-4d42-8d01-733fc92ed06a 190 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 191 | Transform(Component) : ba2eef43-9449-4d76-84a9-36f6331a3eea 192 | gameObject: af402935-72d3-4305-92e8-09649849e5e9 193 | enabled: True 194 | localPosition: Vector3(0, 0, 1) 195 | localRotation: Quaternion(1, 0, 0, 0) 196 | localScale: Vector3(1, 1, 1) 197 | parent: d4a2352e-ccc9-48e6-baa4-a2e3b7784a02 198 | MeshRenderer(Component) : 012104bc-7504-4461-adf1-475548e860af 199 | gameObject: af402935-72d3-4305-92e8-09649849e5e9 200 | enabled: True 201 | mesh: e082e34b-5b4c-4927-8281-a7e0b18c7a23 202 | mat: fc463003-ce33-4408-998e-c7c21e404129 203 | Oscillator(Behaviour) : a1e8f74d-9bc6-40f8-b62a-ef6b23572cd0 204 | gameObject: af402935-72d3-4305-92e8-09649849e5e9 205 | enabled: True 206 | speed: 6 207 | renderer: 012104bc-7504-4461-adf1-475548e860af 208 | _script: 1b7423a5-b3b9-4a83-8e7e-8f96ceb3f0f7 -------------------------------------------------------------------------------- /pyunity_editor/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyunity as pyu 3 | # from PySide6.QtCore import QItemSelectionModel, QModelIndex 4 | from PySide6.QtCore import Qt 5 | from PySide6.QtGui import QIcon 6 | from PySide6.QtWidgets import * 7 | from .smoothScroll import QSmoothTreeWidget 8 | from .local import getPath 9 | 10 | class HierarchyItem(QTreeWidgetItem): 11 | def __init__(self, gameObject): 12 | super(HierarchyItem, self).__init__() 13 | self.setFlags(self.flags() | Qt.ItemIsEditable) 14 | self.setText(0, gameObject.name) 15 | self.name = gameObject.name 16 | self.gameObject = gameObject 17 | self.children = [] 18 | 19 | def add_child(self, child): 20 | self.children.append(child) 21 | self.addChild(child) 22 | 23 | def selectAll(self): 24 | self.setSelected(True) 25 | for child in self.children: 26 | child.selectAll() 27 | 28 | def rename(self, textedit): 29 | text = textedit.value 30 | self.setText(0, text) 31 | self.name = text 32 | assert self.gameObject.name == text 33 | 34 | def toggle(self): 35 | self.gameObject.enabled = not self.gameObject.enabled 36 | 37 | def setBold(self, bold): 38 | font = self.font(0) 39 | font.setBold(bold) 40 | self.setFont(0, font) 41 | 42 | class Hierarchy(QWidget): 43 | SPACER = None 44 | 45 | def __init__(self, parent=None): 46 | super(Hierarchy, self).__init__(parent) 47 | self.vbox_layout = QVBoxLayout(self) 48 | self.vbox_layout.setContentsMargins(2, 2, 2, 2) 49 | self.vbox_layout.setSpacing(2) 50 | 51 | self.hbox_layout = QHBoxLayout() 52 | self.hbox_layout.setStretch(0, 1) 53 | self.title = QLabel("Untitled Scene") 54 | self.vbox_layout.setContentsMargins(0, 0, 0, 0) 55 | self.vbox_layout.setSpacing(0) 56 | self.hbox_layout.addWidget(self.title) 57 | 58 | self.add_button = QToolButton(self) 59 | self.add_button.setIcon(QIcon(getPath("icons/inspector/add.png"))) 60 | self.add_button.setStyleSheet("padding: 3px;") 61 | self.add_button.setPopupMode(QToolButton.InstantPopup) 62 | self.add_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) 63 | 64 | self.menu = QMenu() 65 | self.menu.addAction("New Root GameObject", self.new) 66 | self.menu.addAction("New Child GameObject", self.new_child) 67 | self.menu.addAction("New Sibling GameObject", self.new_sibling) 68 | self.add_button.setMenu(self.menu) 69 | 70 | self.hbox_layout.addWidget(self.add_button) 71 | self.vbox_layout.addLayout(self.hbox_layout) 72 | 73 | self.items = [] 74 | self.tree_widget = CustomTreeWidget(self) 75 | self.vbox_layout.addWidget(self.tree_widget) 76 | self.tree_widget.itemChanged.connect(self.rename) 77 | self.tree_widget.itemSelectionChanged.connect(self.on_click) 78 | self.inspector = None 79 | self.preview = None 80 | 81 | def new(self): 82 | new = pyu.GameObject("GameObject") 83 | self.loaded.Add(new) 84 | newitem = self.add_item(new) 85 | self.tree_widget.clearSelection() 86 | newitem.setSelected(True) 87 | 88 | def new_child(self): 89 | item = self.tree_widget.currentItem() 90 | if item is None: 91 | return self.new() 92 | parent = item.gameObject 93 | new = pyu.GameObject("GameObject", parent) 94 | self.loaded.Add(new) 95 | newitem = self.add_item(new, item) 96 | self.tree_widget.clearSelection() 97 | item.setExpanded(True) 98 | newitem.setSelected(True) 99 | 100 | def new_sibling(self): 101 | sibling = self.tree_widget.currentItem() 102 | if sibling is None: 103 | return self.new() 104 | item = sibling.parent() 105 | if item is None: 106 | return self.new() 107 | parent = item.gameObject 108 | new = pyu.GameObject("GameObject", parent) 109 | self.loaded.Add(new) 110 | newitem = self.add_item(new, item) 111 | self.tree_widget.clearSelection() 112 | newitem.setSelected(True) 113 | 114 | def remove(self): 115 | items = self.tree_widget.selectedItems() 116 | if len(items) == 0: 117 | pyu.Logger.Log("Nothing selected") 118 | return 119 | 120 | for item in items: 121 | item.selectAll() 122 | items = self.tree_widget.selectedItems() 123 | self.items = [] 124 | for item in items: 125 | pyu.Logger.Log("Removing", item.gameObject.name) 126 | if item.parent() is not None: 127 | item.parent().removeChild(item) 128 | else: 129 | self.tree_widget.removeItemWidget(item, 0) 130 | if self.loaded.Has(item.gameObject): 131 | self.loaded.Destroy(item.gameObject) 132 | self.preview.update() 133 | 134 | def reparent(self, items): 135 | for item in items: 136 | index = self.tree_widget.indexFromItem(item).row() 137 | parent = item.parent() 138 | if parent is None: 139 | item.gameObject.transform.ReparentTo(None) 140 | print("Move", item.gameObject.name, "to root, index", index) 141 | else: 142 | print("Move", item.gameObject.name, "under", parent.gameObject.name, "index", index) 143 | parent.setExpanded(True) 144 | transform = item.gameObject.transform 145 | parentTransform = parent.gameObject.transform 146 | transform.ReparentTo(parentTransform) 147 | parentTransform.children.remove(transform) 148 | parentTransform.children.insert(index, transform) 149 | 150 | def rename(self, item, column): 151 | if self.inspector.name_input is not None: 152 | self.inspector.name_input.setText(item.text(column)) 153 | item.gameObject.name = item.text(column) 154 | 155 | def add_item(self, gameObject, parent=None): 156 | item = HierarchyItem(gameObject) 157 | if parent is None: 158 | self.items.append(item) 159 | self.tree_widget.addTopLevelItem(item) 160 | else: 161 | parent.add_child(item) 162 | return item 163 | 164 | def add_item_pos(self, gameObject, *args): 165 | item = HierarchyItem(gameObject) 166 | parent = self.items[args[0]] 167 | pos = args[1:] 168 | for num in pos: 169 | parent = parent.children[num] 170 | parent.add_child(item) 171 | return item 172 | 173 | def load_scene(self, scene): 174 | self.tree_widget.clear() 175 | self.loaded = scene 176 | self.title.setText(scene.name) 177 | items = {} 178 | for gameObject in self.loaded.rootGameObjects: 179 | items[gameObject] = self.add_item(gameObject) 180 | for gameObject in self.loaded.gameObjects: 181 | if gameObject.transform.parent is None: 182 | continue 183 | self.add_item(gameObject, 184 | items[gameObject.transform.parent.gameObject]) 185 | 186 | def on_click(self): 187 | items = self.tree_widget.selectedItems() 188 | if len(items) > 1: 189 | self.inspector.load([]) 190 | elif len(items) == 0: 191 | self.inspector.load(None) 192 | else: 193 | self.inspector.load(items[0]) 194 | 195 | def reset_bold(self): 196 | for item in self.items: 197 | item.setBold(False) 198 | 199 | class CustomTreeWidget(QSmoothTreeWidget): 200 | def __init__(self, parent): 201 | super(CustomTreeWidget, self).__init__(parent) 202 | self.setSelectionMode(QAbstractItemView.ExtendedSelection) 203 | self.header().setVisible(False) 204 | self.setAnimated(True) 205 | self.setIndentation(10) 206 | self.hierarchy = parent 207 | 208 | self.setDragEnabled(True) 209 | self.setDragDropOverwriteMode(False) 210 | self.setDragDropMode(QAbstractItemView.InternalMove) 211 | self.viewport().setAcceptDrops(True) 212 | self.setDropIndicatorShown(True) 213 | 214 | def selectAll(self): 215 | item = self.invisibleRootItem() 216 | for i in range(self.invisibleRootItem().childCount()): 217 | child = item.child(i) 218 | child.selectAll() 219 | 220 | def dropEvent(self, event): 221 | items = self.selectedItems() 222 | super(CustomTreeWidget, self).dropEvent(event) 223 | for item in items: 224 | item.setSelected(True) 225 | self.hierarchy.reparent(items) 226 | 227 | def contextMenuEvent(self, event): 228 | menu = QMenu() 229 | menu.addAction("New Root GameObject", self.hierarchy.new) 230 | menu.addAction("New Child GameObject", self.hierarchy.new_child) 231 | menu.addAction("New Sibling GameObject", self.hierarchy.new_sibling) 232 | 233 | num = len(self.selectedItems()) 234 | if num > 0: 235 | menu.addSeparator() 236 | if num == 1: 237 | menu.addAction("Delete GameObject", self.hierarchy.remove) 238 | else: 239 | menu.addAction("Delete GameObjects", self.hierarchy.remove) 240 | 241 | menu.exec(event.globalPos()) 242 | super(CustomTreeWidget, self).contextMenuEvent(event) 243 | -------------------------------------------------------------------------------- /standalone/pyunity_updater.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import py_compile 3 | import zipimport 4 | import tempfile 5 | import logging 6 | import zipfile 7 | import urllib.request 8 | import ctypes 9 | import shutil 10 | import glob 11 | import sys 12 | import os 13 | 14 | logging.basicConfig(filename="updater.log") 15 | logger = logging.getLogger("pyunity_updater") 16 | logger.setLevel(logging.INFO) 17 | 18 | ZIP_OPTIONS = {"compression": zipfile.ZIP_DEFLATED, "compresslevel": 9} 19 | 20 | originalFolder = os.getcwd() 21 | 22 | class ZipFile(zipfile.ZipFile): 23 | def remove(self, zinfo_or_arcname): 24 | """Remove a member from the archive.""" 25 | 26 | if self.mode not in ('w', 'x', 'a'): 27 | raise ValueError("remove() requires mode 'w', 'x', or 'a'") 28 | if not self.fp: 29 | raise ValueError( 30 | "Attempt to write to ZIP archive that was already closed") 31 | if self._writing: 32 | raise ValueError( 33 | "Can't write to ZIP archive while an open writing handle exists" 34 | ) 35 | 36 | # Make sure we have an existing info object 37 | if isinstance(zinfo_or_arcname, zipfile.ZipInfo): 38 | zinfo = zinfo_or_arcname 39 | # make sure zinfo exists 40 | if zinfo not in self.filelist: 41 | raise KeyError( 42 | 'There is no item %r in the archive' % zinfo_or_arcname) 43 | else: 44 | # get the info object 45 | zinfo = self.getinfo(zinfo_or_arcname) 46 | 47 | return self._remove_members({zinfo}) 48 | 49 | def _remove_members(self, members, *, remove_physical=True, chunk_size=2**20): 50 | """Remove members in a zip file. 51 | 52 | All members (as zinfo) should exist in the zip; otherwise the zip file 53 | will erroneously end in an inconsistent state. 54 | """ 55 | fp = self.fp 56 | entry_offset = 0 57 | member_seen = False 58 | 59 | # get a sorted filelist by header offset, in case the dir order 60 | # doesn't match the actual entry order 61 | filelist = sorted(self.filelist, key=lambda x: x.header_offset) 62 | for i in range(len(filelist)): 63 | info = filelist[i] 64 | is_member = info in members 65 | 66 | if not (member_seen or is_member): 67 | continue 68 | 69 | # get the total size of the entry 70 | try: 71 | offset = filelist[i + 1].header_offset 72 | except IndexError: 73 | offset = self.start_dir 74 | entry_size = offset - info.header_offset 75 | 76 | if is_member: 77 | member_seen = True 78 | entry_offset += entry_size 79 | 80 | # update caches 81 | self.filelist.remove(info) 82 | try: 83 | del self.NameToInfo[info.filename] 84 | except KeyError: 85 | pass 86 | continue 87 | 88 | # update the header and move entry data to the new position 89 | if remove_physical: 90 | old_header_offset = info.header_offset 91 | info.header_offset -= entry_offset 92 | read_size = 0 93 | while read_size < entry_size: 94 | fp.seek(old_header_offset + read_size) 95 | data = fp.read(min(entry_size - read_size, chunk_size)) 96 | fp.seek(info.header_offset + read_size) 97 | fp.write(data) 98 | fp.flush() 99 | read_size += len(data) 100 | 101 | # Avoid missing entry if entries have a duplicated name. 102 | # Reverse the order as NameToInfo normally stores the last added one. 103 | for info in reversed(self.filelist): 104 | self.NameToInfo.setdefault(info.filename, info) 105 | 106 | # update state 107 | if remove_physical: 108 | self.start_dir -= entry_offset 109 | self._didModify = True 110 | 111 | # seek to the start of the central dir 112 | fp.seek(self.start_dir) 113 | 114 | def errorMessage(msg): 115 | if sys.stderr is not None: 116 | sys.stderr.write(msg + "\n") 117 | else: 118 | ctypes.windll.user32.MessageBoxW(None, msg, "PyUnity Updater error", 0x10) 119 | exit(1) 120 | 121 | def infoMessage(msg): 122 | if sys.stdout is not None: 123 | sys.stdout.write(msg + "\n") 124 | else: 125 | ctypes.windll.user32.MessageBoxW(None, msg, "PyUnity Updater message", 0x40) 126 | 127 | def fixModulePaths(): 128 | # Replace all relative paths with absolute paths 129 | # TODO: Use absolute paths in C script instead of setting after Python initialization 130 | logger.info("Fixing module paths") 131 | logger.info("Fixing sys.path") 132 | mainDir = os.path.abspath(".") 133 | for i in range(len(sys.path)): 134 | if sys.path[i].startswith("Lib\\"): 135 | sys.path[i] = os.path.join(mainDir, sys.path[i]) 136 | 137 | logger.info("Fixing sys.path_importer_cache") 138 | removed = [] 139 | new = {} 140 | for path, importer in sys.path_importer_cache.items(): 141 | if isinstance(importer, zipimport.zipimporter): 142 | removed.append(path) 143 | newPath = os.path.join(mainDir, path) 144 | importer = zipimport.zipimporter(newPath) 145 | new[newPath] = importer 146 | for path in removed: 147 | sys.path_importer_cache.pop(path) 148 | for path in new: 149 | sys.path_importer_cache[path] = new[path] 150 | 151 | logger.info("Fixing sys.modules") 152 | for module in sys.modules.values(): 153 | if isinstance(module.__loader__, zipimport.zipimporter): 154 | newPath = os.path.join(mainDir, module.__loader__.archive, module.__loader__.prefix) 155 | loader = zipimport.zipimporter(newPath) 156 | module.__loader__ = loader 157 | module.__spec__.loader = loader 158 | module.__spec__.origin = os.path.join(mainDir, module.__spec__.origin) 159 | locations = module.__spec__.submodule_search_locations 160 | if locations is not None: 161 | for i in range(len(locations)): 162 | locations[i] = os.path.join(mainDir, locations[i]) 163 | 164 | def getPyUnity(): 165 | logger.info("Fetching latest pure python pyunity wheel build") 166 | url = "https://nightly.link/pyunity/pyunity/workflows/windows/develop/purepython.zip" 167 | print("GET", url, "-> pyunity-artifact.zip", flush=True) 168 | urllib.request.urlretrieve(url, "pyunity-artifact.zip") 169 | logger.info("Extracting pyunity wheel build") 170 | with zipfile.ZipFile("pyunity-artifact.zip") as zf: 171 | print("EXTRACT pyunity-artifact.zip", flush=True) 172 | zf.extractall("pyunity-artifact") 173 | file = glob.glob("pyunity-artifact/*.whl")[0] 174 | with zipfile.ZipFile(file) as zf: 175 | print("EXTRACT", os.path.basename(file), flush=True) 176 | zf.extractall("pyunity-package") 177 | 178 | def getPyUnityEditor(): 179 | logger.info("Fetching latest pure python pyunity-gui wheel build") 180 | url = "https://nightly.link/pyunity/pyunity-gui/workflows/wheel/master/purepython.zip" 181 | print("GET", url, "-> editor-artifact.zip", flush=True) 182 | urllib.request.urlretrieve(url, "editor-artifact.zip") 183 | logger.info("Extracting pyunity-editor wheel build") 184 | with zipfile.ZipFile("editor-artifact.zip") as zf: 185 | print("EXTRACT editor-artifact.zip", flush=True) 186 | zf.extractall("editor-artifact") 187 | file = glob.glob("editor-artifact/*.whl")[0] 188 | with zipfile.ZipFile(file) as zf: 189 | print("EXTRACT", os.path.basename(file), flush=True) 190 | zf.extractall("editor-package") 191 | 192 | # copied from builder.py 193 | def addPackage(zf, name, path, orig, distInfo=True): 194 | logger.info("Adding " + name + " to zip file") 195 | print("COMPILE", name, flush=True) 196 | os.chdir("..\\" + name) 197 | paths = glob.glob(path, recursive=True) 198 | if distInfo: 199 | paths.extend(glob.glob("*.dist-info\\**\\*", recursive=True)) 200 | for file in paths: 201 | if file.endswith(".py"): 202 | py_compile.compile(file, file + "c", file, doraise=True) 203 | zf.write(file + "c") 204 | elif not file.endswith(".pyc"): 205 | zf.write(file) 206 | os.chdir(orig) 207 | 208 | def updatePackages(workdir): 209 | logger.info("Fetching latest packages") 210 | getPyUnity() 211 | getPyUnityEditor() 212 | with ZipFile(os.path.dirname(__file__), "a", **ZIP_OPTIONS) as zf: 213 | logger.info("Deleting old files") 214 | removed = set() 215 | for file in zf.filelist: 216 | for folder in ["pyunity/", "pyunity-", "pyunity_editor/", "pyunity_editor-"]: 217 | if file.filename.startswith(folder): 218 | removed.add(file) 219 | break 220 | zf._remove_members(removed) 221 | 222 | logger.info("Adding new packages") 223 | os.chdir(os.path.join(workdir, "pyunity-package")) 224 | addPackage(zf, "pyunity-package", "pyunity\\**\\*", workdir) 225 | os.chdir(os.path.join(workdir, "editor-package")) 226 | addPackage(zf, "editor-package", "pyunity_editor\\**\\*", workdir) 227 | 228 | def main(): 229 | logger.info("Started update script") 230 | if not os.path.isfile("Lib\\python.zip"): 231 | errorMessage("Zip file not locatable") 232 | 233 | logger.info("Located zip file") 234 | 235 | fixModulePaths() 236 | 237 | workdir = tempfile.mkdtemp() 238 | os.chdir(workdir) 239 | logger.info("Using directory " + workdir) 240 | try: 241 | updatePackages(workdir) 242 | logger.info("Updated packages successfully") 243 | infoMessage("Updated packages successfully") 244 | except Exception as e: 245 | errorMessage("".join(traceback.format_exception(type(e), e, e.__traceback__))) 246 | finally: 247 | logger.info("Cleaning up directory " + workdir) 248 | print("Cleaning up") 249 | os.chdir(originalFolder) 250 | shutil.rmtree(workdir) 251 | 252 | def injectIntoZip(): 253 | logger.info("Started update script injector") 254 | source = os.path.abspath(__file__) 255 | filename = os.path.basename(__file__) 256 | os.chdir(os.path.dirname(source)) 257 | if not os.path.isfile("Lib\\python.zip"): 258 | errorMessage("Zip file not locatable") 259 | 260 | logger.info("Located zip file") 261 | 262 | try: 263 | logger.info("Compiling updater into bytecode") 264 | py_compile.compile(filename, filename + "c") 265 | logger.info("Adding bytecode into zip file") 266 | with zipfile.ZipFile("Lib\\python.zip", "a") as zf: 267 | zf.write(filename + "c") 268 | except Exception as e: 269 | errorMessage("".join(traceback.format_exception(type(e), e, e.__traceback__))) 270 | else: 271 | logger.info("Injected script successfully") 272 | infoMessage("Injected script successfully") 273 | finally: 274 | logger.info("Removing bytecode") 275 | if os.path.isfile(filename + "c"): 276 | os.remove(filename + "c") 277 | 278 | if __name__ == "__main__": 279 | source = os.path.abspath(__file__) 280 | if os.path.dirname(source).endswith(".zip"): 281 | main() 282 | else: 283 | injectIntoZip() 284 | -------------------------------------------------------------------------------- /pyunity_editor/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | os.environ["PYUNITY_DEBUG_MODE"] = "1" 4 | from PySide6.QtGui import QFont 5 | from PySide6.QtCore import QThread, QObject, Signal, QTimer 6 | from PySide6.QtWidgets import QApplication, QMessageBox, QFileDialog 7 | from .window import Editor, SceneButtons, Window 8 | from .views import Hierarchy, HierarchyItem 9 | from .inspector import Inspector 10 | from .render import OpenGLFrame, Console 11 | from pyunity import SceneManager, Logger 12 | import io 13 | import sys 14 | import contextlib 15 | 16 | def testing(string): 17 | def inner(): 18 | Logger.Log(string) 19 | return inner 20 | 21 | class VersionWorker(QObject): 22 | finished = Signal() 23 | 24 | def run(self): 25 | from pyunity.info import printInfo 26 | r = io.StringIO() 27 | with contextlib.redirect_stdout(r): 28 | printInfo() 29 | self.lines = r.getvalue().rstrip().split("\n")[3:] 30 | self.finished.emit() 31 | 32 | class Application(QApplication): 33 | def __init__(self, path): 34 | super(Application, self).__init__(sys.argv) 35 | self.setFont(QFont("Segoe UI", 10)) 36 | 37 | self.window = Window(self) 38 | self.setWindowIcon(self.window.icon) 39 | 40 | self.buttons = SceneButtons(self.window) 41 | self.buttons.add_button("play.png", "Run the scene") 42 | self.buttons.add_button("pause.png", "Pause the scene") 43 | self.buttons.add_button("stop.png", "Stop the current running scene", True) 44 | self.buttons.setMaximumHeight(self.buttons.sizeHint().height()) 45 | self.window.vbox_layout.addWidget(self.buttons) 46 | 47 | # Tabs 48 | self.editor = Editor(self.window) 49 | self.window.vbox_layout.addWidget(self.editor) 50 | self.scene = self.editor.add_tab("Scene", 0, 0) 51 | self.game = self.editor.add_tab("Game", 1, 0) 52 | self.console = self.editor.add_tab("Console", 1, 0) 53 | self.hierarchy = self.editor.add_tab("Hierarchy", 0, 1) 54 | self.files = self.editor.add_tab("Files", 1, 1) 55 | self.mixer = self.editor.add_tab("Audio Mixer", 1, 1) 56 | self.inspector = self.editor.add_tab("Inspector", 0, 2) 57 | self.navigation = self.editor.add_tab("Navigation", 0, 2) 58 | 59 | self.editor.set_stretch((3, 1, 1)) 60 | 61 | # Views 62 | self.game_content = self.game.set_window(OpenGLFrame(path=path)) 63 | self.game_content.file_tracker.app = self 64 | self.game_content.set_buttons(self.buttons) 65 | 66 | self.inspector_content = self.inspector.set_window(Inspector()) 67 | self.inspector_content.project = self.game_content.file_tracker.project 68 | 69 | self.hierarchy_content = self.hierarchy.set_window(Hierarchy()) 70 | self.hierarchy_content.inspector = self.inspector_content 71 | self.hierarchy_content.preview = self.game_content 72 | 73 | self.console_content = self.console.set_window(Console()) 74 | self.game_content.console = self.console_content 75 | 76 | self.setup_toolbar() 77 | 78 | def loadScene(self, scene, uuids=None): 79 | self.loaded = scene 80 | self.game_content.loadScene(self.loaded) 81 | self.hierarchy_content.load_scene(self.loaded) 82 | if uuids is not None: 83 | for uuid in uuids: 84 | gameObject = self.game_content.file_tracker.project._idMap[uuid] 85 | selected = None 86 | stack = [self.hierarchy_content.tree_widget.invisibleRootItem()] 87 | while stack: 88 | item = stack.pop(0) 89 | if isinstance(item, HierarchyItem) and item.gameObject is gameObject: 90 | selected = item 91 | break 92 | for i in range(item.childCount()): 93 | stack.append(item.child(i)) 94 | if selected is not None: 95 | current = selected 96 | while current.parent() is not None: 97 | current.parent().setExpanded(True) 98 | current = current.parent() 99 | selected.setSelected(True) 100 | self.hierarchy_content.on_click() 101 | 102 | def start(self): 103 | os.environ["PYUNITY_EDITOR_LOADED"] = "1" 104 | self.window.showMaximized() 105 | 106 | scene = SceneManager.GetSceneByIndex( 107 | self.game_content.file_tracker.project.firstScene) 108 | self.loadScene(scene) 109 | self.game_content.file_tracker.start(1) 110 | 111 | QTimer.singleShot(100, self.window.activateWindow) 112 | self.exec() 113 | 114 | def open(self): 115 | Logger.Log("Choosing folder...") 116 | project = self.game_content.file_tracker.project 117 | while True: 118 | file, _ = QFileDialog.getOpenFileName( 119 | None, "Select scene to open", str(project.path), 120 | "PyUnity Scenes (*.scene)") 121 | if not file: 122 | return 123 | fp = Path(file).resolve() 124 | if not fp.is_relative_to(project.path): 125 | message_box = QMessageBox( 126 | QMessageBox.Information, "Error", 127 | "Please select a scene that is in the project.") 128 | message_box.exec() 129 | else: 130 | break 131 | 132 | localPath = str(fp.relative_to(project.path).as_posix()) 133 | uuid = project.filePaths[localPath].uuid 134 | scene = project._idMap[uuid] 135 | 136 | message_box = QMessageBox( 137 | QMessageBox.Information, "Quit", 138 | "Are you sure you want to open a different scene?", 139 | parent=self.window) 140 | message_box.setInformativeText("You may lose unsaved changes.") 141 | message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) 142 | message_box.setDefaultButton(QMessageBox.Cancel) 143 | ret = message_box.exec() 144 | if ret == QMessageBox.Cancel: 145 | return 146 | 147 | self.loadScene(scene) 148 | 149 | def save(self): 150 | self.game_content.save() 151 | self.inspector_content.reset_bold() 152 | self.hierarchy_content.reset_bold() 153 | 154 | def quit_wrapper(self): 155 | message_box = QMessageBox( 156 | QMessageBox.Information, "Quit", 157 | "Are you sure you want to quit?", 158 | parent=self.window) 159 | message_box.setInformativeText("You may lose unsaved changes.") 160 | message_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) 161 | message_box.setDefaultButton(QMessageBox.Cancel) 162 | ret = message_box.exec() 163 | if ret == QMessageBox.Ok: 164 | self.quit() 165 | exit(0) 166 | 167 | def showVersion(self): 168 | if self.worker is not None: 169 | return 170 | self.worker = VersionWorker() 171 | self.vThread = QThread() 172 | self.worker.moveToThread(self.vThread) 173 | self.vThread.started.connect(self.worker.run) 174 | self.worker.finished.connect(self.vThread.quit) 175 | self.worker.finished.connect(self.worker.deleteLater) 176 | self.vThread.finished.connect(self.cleanUpWorker) 177 | self.vThread.finished.connect(self.vThread.deleteLater) 178 | self.vThread.start() 179 | 180 | def cleanUpWorker(self): 181 | msg = QMessageBox() 182 | msg.setText("PyUnity Version Info") 183 | msg.setInformativeText("\n".join( 184 | [x for x in self.worker.lines if not x.startswith("Warning: ")])) 185 | msg.setWindowTitle("PyUnity information") 186 | msg.setStandardButtons(QMessageBox.Ok) 187 | msg.exec() 188 | 189 | self.worker = None 190 | self.vThread = None 191 | 192 | def setup_toolbar(self): 193 | self.window.toolbar.add_action("New", "File", "Ctrl+N", "Create a new project", testing("new")) 194 | self.window.toolbar.add_action("Open", "File", "Ctrl+O", "Open another Scene", self.open) 195 | self.window.toolbar.add_separator("File") 196 | self.window.toolbar.add_action("Save", "File", "Ctrl+S", "Save the current Scene", self.save) 197 | self.window.toolbar.add_action("Save As", "File", "Ctrl+Shift+S", "Save the current Scene as new file", testing("save as")) 198 | self.window.toolbar.add_action("Save a Copy As", "File", "Ctrl+Alt+S", "Save a copy of the current Scene", testing("save copy as")) 199 | self.window.toolbar.add_separator("File") 200 | self.window.toolbar.add_action("Quit", "File", "Ctrl+Q", "Close the Editor", self.quit_wrapper) 201 | 202 | self.window.toolbar.add_action("Undo", "Edit", "Ctrl+Z", "Undo the last action", testing("undo")) 203 | self.window.toolbar.add_action("Redo", "Edit", "Ctrl+Shift+Z", "Redo the last action", testing("redo")) 204 | self.window.toolbar.add_separator("Edit") 205 | self.window.toolbar.add_action("Cut", "Edit", "Ctrl+X", "Deletes item and adds to clipboard", testing("cut")) 206 | self.window.toolbar.add_action("Copy", "Edit", "Ctrl+C", "Adds item to clipboard", testing("copy")) 207 | self.window.toolbar.add_action("Paste", "Edit", "Ctrl+V", "Pastes item from clipboard", testing("paste")) 208 | self.window.toolbar.add_separator("Edit") 209 | self.window.toolbar.add_action("Rename", "Edit", "F2", "Renames the selected item", self.window.rename) 210 | self.window.toolbar.add_action("Duplicate", "Edit", "Ctrl+D", "Duplicates the selected item(s)", testing("duplicate")) 211 | self.window.toolbar.add_action("Delete", "Edit", "Delete", "Deletes item", self.hierarchy_content.remove) 212 | self.window.toolbar.add_separator("Edit") 213 | self.window.toolbar.add_action("Select All", "Edit", "Ctrl+A", "Selects all items in the current Scene", self.hierarchy_content.tree_widget.selectAll) 214 | self.window.toolbar.add_action("Select None", "Edit", "Escape", "Deselects all items", self.window.select_none) 215 | 216 | self.window.toolbar.add_sub_action("Start/Stop", "View", "Game", "Ctrl+Return", "Starts and stops the game", self.buttons.buttons[0].click) 217 | self.window.toolbar.add_sub_action("Pause/Unpause", "View", "Game", "Space", "Toggles the pause state", self.buttons.buttons[1].click) 218 | 219 | self.window.toolbar.add_sub_action("Folder", "Assets", "Create", "", "", testing("new folder")) 220 | self.window.toolbar.add_sub_action("File", "Assets", "Create", "", "", testing("new file")) 221 | self.window.toolbar.add_sub_separator("Assets", "Create") 222 | self.window.toolbar.add_sub_action("Script", "Assets", "Create", "", "", testing("new script")) 223 | self.window.toolbar.add_sub_separator("Assets", "Create") 224 | self.window.toolbar.add_sub_action("Scene", "Assets", "Create", "", "", testing("new scene")) 225 | self.window.toolbar.add_sub_action("Prefab", "Assets", "Create", "", "", testing("new prefab")) 226 | self.window.toolbar.add_sub_action("Material", "Assets", "Create", "", "", testing("new mat")) 227 | self.window.toolbar.add_sub_separator("Assets", "Create") 228 | self.window.toolbar.add_sub_action("Physic Material", "Assets", "Create", "", "", testing("new phys mat")) 229 | 230 | self.window.toolbar.add_action("Open", "Assets", "", "Opens the selected asset", testing("open asset")) 231 | self.window.toolbar.add_action("Delete", "Assets", "", "Deletes the selected asset", testing("del asset")) 232 | 233 | self.worker = None 234 | self.vThread = None 235 | self.window.toolbar.add_action("Show PyUnity information", "Window", "", "Show information about PyUnity, the PyUnity Editor and all its dependencies.", self.showVersion) 236 | self.window.toolbar.add_action("Toggle Theme", "Window", "Ctrl+L", "Toggle theme between light and dark", self.window.toggle_theme) 237 | -------------------------------------------------------------------------------- /pyunity_editor/window.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt 2 | from PySide6.QtWidgets import * 3 | from PySide6.QtGui import QIcon, QAction, QFont, QColor 4 | from qframelesswindow import FramelessMainWindow 5 | import os 6 | from .local import getPath 7 | from .resources import qInitResources 8 | qInitResources() 9 | 10 | class Window(FramelessMainWindow): 11 | def __init__(self, app): 12 | super(Window, self).__init__() 13 | 14 | statusBar = QStatusBar(self) 15 | label = QLabel(statusBar) 16 | label.setText("Hover over a button to view what it does") 17 | label.setFont(QFont("Segoe UI", 9)) 18 | label.setStyleSheet("QLabel {padding-left: 2px;}") 19 | statusBar.insertWidget(0, label) 20 | self.setStatusBar(statusBar) 21 | 22 | self.app = app 23 | self.toolbar = ToolBar(self) 24 | self.titleBar.layout().insertWidget(0, self.toolbar, 0, Qt.AlignLeft) 25 | self.setMenuWidget(self.titleBar) 26 | 27 | self.titleLabel = QLabel(self) 28 | self.titleLabel.setFont(QFont("Segoe UI", 12)) 29 | self.titleBar.layout().insertWidget(1, self.titleLabel, 1, Qt.AlignRight | Qt.AlignVCenter) 30 | 31 | self.iconLabel = QLabel(self) 32 | self.iconLabel.setStyleSheet("QLabel {padding-left: 4px}") 33 | self.titleBar.layout().insertWidget(0, self.iconLabel, 0, Qt.AlignLeft) 34 | 35 | self.mainWidget = QWidget(self) 36 | self.mainWidget.setObjectName("main-widget") 37 | self.mainWidget.setStyleSheet("#main-widget:focus {border: none;}") 38 | self.vbox_layout = QVBoxLayout(self.mainWidget) 39 | self.vbox_layout.setStretch(0, 0) 40 | self.vbox_layout.setStretch(1, 1) 41 | self.vbox_layout.setSpacing(0) 42 | self.vbox_layout.setContentsMargins(2, 2, 2, 2) 43 | 44 | self.setStyleSheet("Window:focus {border: none;}") 45 | self.setWindowTitle("PyUnity Editor") 46 | self.setFocusPolicy(Qt.StrongFocus) 47 | self.titleBar.raise_() 48 | 49 | self.buttonColors = { 50 | "dark": { 51 | "normal": QColor(239, 240, 241), 52 | "hover": QColor(239, 240, 241), 53 | "pressed": QColor(0, 0, 0), 54 | "hoverbg": QColor(255, 255, 255, 26), 55 | "pressedbg": QColor(255, 255, 255, 51), 56 | }, 57 | "light": { 58 | "normal": QColor(49, 54, 59), 59 | "hover": QColor(49, 54, 59), 60 | "pressed": QColor(255, 255, 255), 61 | "hoverbg": QColor(0, 0, 0, 26), 62 | "pressedbg": QColor(0, 0, 0, 51), 63 | } 64 | } 65 | 66 | # self.titleColors = { 67 | # "dark": "#485057", 68 | # "light": "#d4d7d9" 69 | # } 70 | self.styleSheets = {} 71 | for style in ["dark", "light"]: 72 | with open(getPath(f"theme/{style}.qss")) as f: 73 | self.styleSheets[style] = f.read() 74 | self.setTheme("dark") 75 | 76 | self.icon = QIcon() 77 | for size in [16, 24, 32, 48, 64, 128, 256]: 78 | filename = f"icon{size}x{size}.png" 79 | fullPath = os.path.join("icons", "window", filename) 80 | self.icon.addFile(getPath(fullPath)) 81 | self.setWindowIcon(self.icon) 82 | 83 | def setWindowIcon(self, icon): 84 | super(Window, self).setWindowIcon(icon) 85 | self.iconLabel.setPixmap(icon.pixmap(24, 24)) 86 | self.titleBar.update() 87 | 88 | def setWindowTitle(self, title): 89 | super(Window, self).setWindowTitle(title) 90 | self.titleLabel.setText(title) 91 | 92 | def toggle_theme(self): 93 | if self.theme == "dark": 94 | self.setTheme("light") 95 | else: 96 | self.setTheme("dark") 97 | 98 | def setTheme(self, theme): 99 | self.theme = theme 100 | self.app.setStyleSheet(self.styleSheets[self.theme]) 101 | # self.titleBar.setStyleSheet(f"background-color: {self.titleColors[self.theme]};") 102 | self.titleBar.closeBtn.setNormalColor(self.buttonColors[theme]["normal"]) 103 | for button in [self.titleBar.minBtn, self.titleBar.maxBtn]: 104 | button.setNormalColor(self.buttonColors[theme]["normal"]) 105 | button.setHoverColor(self.buttonColors[theme]["hover"]) 106 | button.setPressedColor(self.buttonColors[theme]["pressed"]) 107 | button.setHoverBackgroundColor(self.buttonColors[theme]["hoverbg"]) 108 | button.setPressedBackgroundColor(self.buttonColors[theme]["pressedbg"]) 109 | self.update() 110 | 111 | def closeEvent(self, event): 112 | self.app.quit_wrapper() 113 | event.ignore() 114 | 115 | def select_none(self): 116 | if not isinstance(self.app.focusWidget(), QLineEdit): 117 | self.app.hierarchy_content.tree_widget.clearSelection() 118 | 119 | def rename(self): 120 | self.app.hierarchy.tab_widget.setCurrentWidget(self.app.hierarchy) 121 | items = self.app.hierarchy_content.tree_widget.selectedItems() 122 | if len(items) == 1: 123 | self.app.hierarchy_content.tree_widget.editItem(items[0]) 124 | 125 | def mousePressEvent(self, event): 126 | focused = self.focusWidget() 127 | if isinstance(focused, QLineEdit): 128 | focused.clearFocus() 129 | super(Window, self).mousePressEvent(event) 130 | 131 | def resizeEvent(self, event): 132 | super(Window, self).resizeEvent(event) 133 | padding = self.statusBar().height() + self.titleBar.height() 134 | self.mainWidget.resize(self.width(), self.height() - padding) 135 | self.mainWidget.move(0, self.titleBar.height()) 136 | self.titleBar.raise_() 137 | self.app.editor.readjust() 138 | 139 | class SceneButtons(QWidget): 140 | def __init__(self, window): 141 | super(SceneButtons, self).__init__(window) 142 | self.buttons = [] 143 | 144 | self.hbox_layout = QHBoxLayout(self) 145 | self.hbox_layout.setSpacing(0) 146 | self.hbox_layout.setContentsMargins(0, 0, 0, 0) 147 | 148 | spacer1 = QSpacerItem(0, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 149 | spacer2 = QSpacerItem(0, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 150 | self.hbox_layout.addSpacerItem(spacer1) 151 | self.hbox_layout.addSpacerItem(spacer2) 152 | self.spacers = [spacer1, spacer2] 153 | 154 | def add_button(self, icon, tip="", on=False): 155 | button = QToolButton(self) 156 | button.setIcon(QIcon(getPath("icons/buttons/" + icon))) 157 | button.setStatusTip(tip) 158 | button.setCheckable(True) 159 | button.setChecked(on) 160 | self.buttons.append(button) 161 | self.hbox_layout.insertWidget(len(self.buttons), button) 162 | 163 | class ToolBar(QMenuBar): 164 | def __init__(self, instance): 165 | super(ToolBar, self).__init__(instance) 166 | self.setStyleSheet("ToolBar {vertical-align: middle; padding: 4px;}") 167 | self.setFont(QFont("Segoe UI", 10)) 168 | self.instance = instance 169 | self.menus = {} 170 | self.sub_menus = {} 171 | 172 | def add_menu(self, name): 173 | if name in self.menus: 174 | return 175 | menu = self.addMenu("&" + name) 176 | self.menus[name] = menu 177 | self.sub_menus[name] = {} 178 | return menu 179 | 180 | def add_action(self, name, menu, shortcut, tip, *funcs): 181 | action = QAction(name, self.instance) 182 | if shortcut: 183 | action.setShortcut(shortcut) 184 | action.setStatusTip(tip) 185 | for func in funcs: 186 | action.triggered.connect(func) 187 | 188 | if menu not in self.menus: 189 | menu_tab = self.add_menu(menu) 190 | else: 191 | menu_tab = self.menus[menu] 192 | 193 | menu_tab.addAction(action) 194 | 195 | def add_sub_menu(self, name, menu): 196 | if menu not in self.menus: 197 | menu_tab = self.add_menu(menu) 198 | else: 199 | menu_tab = self.menus[menu] 200 | sub_menu = menu_tab.addMenu(name) 201 | self.sub_menus[menu][name] = sub_menu 202 | return sub_menu 203 | 204 | def add_sub_action(self, name, menu, sub_menu, shortcut, tip, func): 205 | action = QAction(name, self.instance) 206 | if shortcut is not None: 207 | action.setShortcut(shortcut) 208 | action.setStatusTip(tip) 209 | action.triggered.connect(func) 210 | 211 | if menu not in self.menus: 212 | menu_tab = self.add_sub_menu(sub_menu, menu) 213 | else: 214 | menu_tab = self.sub_menus[menu][sub_menu] 215 | 216 | menu_tab.addAction(action) 217 | 218 | def add_separator(self, menu): 219 | if menu in self.menus: 220 | self.menus[menu].addSeparator() 221 | 222 | def add_sub_separator(self, menu, sub): 223 | if menu in self.menus and sub in self.sub_menus[menu]: 224 | self.sub_menus[menu][sub].addSeparator() 225 | 226 | class Editor(QSplitter): 227 | def __init__(self, window): 228 | super(Editor, self).__init__(Qt.Horizontal, window) 229 | self.setHandleWidth(2) 230 | self.setChildrenCollapsible(False) 231 | self.columnWidgets = [] 232 | self.stretch = [] 233 | self.splitterMoved.connect(self.setWidth) 234 | 235 | def readjust(self): 236 | part = self.width() / sum(self.stretch) 237 | self.setSizes([int(stretch * part) for stretch in self.stretch]) 238 | 239 | for column in self.columnWidgets: 240 | column.readjust() 241 | 242 | def setWidth(self, pos, index): 243 | part = self.height() / sum(self.stretch) 244 | original = self.stretch[index - 1] * part 245 | diff = (pos - original) / part 246 | self.stretch[index - 1] += diff 247 | self.stretch[index] -= diff 248 | 249 | def add_tab(self, name, row, column): 250 | if len(self.columnWidgets) <= column: 251 | column = len(self.columnWidgets) 252 | columnWidget = Column(self) 253 | self.addWidget(columnWidget) 254 | self.stretch.append(1) 255 | self.columnWidgets.append(columnWidget) 256 | else: 257 | columnWidget = self.columnWidgets[column] 258 | return columnWidget.add_tab(name, row) 259 | 260 | def set_stretch(self, stretch): 261 | if len(stretch) != len(self.columnWidgets): 262 | raise ValueError("Argument 1: expected %d length, got %d length" % \ 263 | (len(stretch), len(self.columnWidgets))) 264 | self.stretch = list(stretch) 265 | 266 | class Column(QSplitter): 267 | def __init__(self, parent): 268 | super(Column, self).__init__(Qt.Vertical, parent) 269 | self.setHandleWidth(2) 270 | self.setChildrenCollapsible(False) 271 | self.tab_widgets = [] 272 | self.tabs = [] 273 | self.stretch = [] 274 | self.splitterMoved.connect(self.setWidth) 275 | 276 | def readjust(self): 277 | part = self.height() / sum(self.stretch) 278 | self.setSizes([int(stretch * part) for stretch in self.stretch]) 279 | 280 | def setWidth(self, pos, index): 281 | part = self.height() / sum(self.stretch) 282 | original = self.stretch[index - 1] * part 283 | diff = (pos - original) / part 284 | self.stretch[index - 1] += diff 285 | self.stretch[index] -= diff 286 | 287 | def add_tab(self, name, row): 288 | if len(self.tabs) <= row: 289 | row = len(self.tabs) 290 | tab_widget = TabGroup(self) 291 | self.addWidget(tab_widget) 292 | self.stretch.append(1) 293 | self.tab_widgets.append(tab_widget) 294 | self.tabs.append([]) 295 | else: 296 | tab_widget = self.tab_widgets[row] 297 | tab = Tab(tab_widget, name) 298 | self.tabs[row].append(tab) 299 | return tab 300 | 301 | class TabGroup(QTabWidget): 302 | def __init__(self, parent): 303 | super(TabGroup, self).__init__(parent) 304 | self.setMovable(True) 305 | self.currentChanged.connect(self.tab_change) 306 | 307 | def tab_change(self, index): 308 | widget = self.currentWidget() 309 | if hasattr(widget, "content") and widget.content is not None: 310 | if hasattr(widget.content, "on_switch"): 311 | widget.content.on_switch() 312 | 313 | class Tab(QWidget): 314 | def __init__(self, tab_widget, name): 315 | super(Tab, self).__init__(tab_widget) 316 | self.tab_widget = tab_widget 317 | self.name = name 318 | 319 | self.vbox_layout = QVBoxLayout(self) 320 | self.vbox_layout.setSpacing(0) 321 | self.vbox_layout.setContentsMargins(0, 0, 0, 0) 322 | self.setLayout(self.vbox_layout) 323 | 324 | self.spacer = QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Expanding) 325 | self.vbox_layout.addSpacerItem(self.spacer) 326 | 327 | self.tab_widget.addTab(self, self.name) 328 | self.content = None 329 | 330 | def set_window(self, content): 331 | self.content = content 332 | self.content.setParent(self) 333 | self.vbox_layout.insertWidget(0, self.content) 334 | if hasattr(type(content), "SPACER"): 335 | self.vbox_layout.removeItem(self.spacer) 336 | del self.spacer 337 | return self.content 338 | -------------------------------------------------------------------------------- /pyunity_editor/render.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import * 2 | from PySide6.QtCore import * 3 | from PySide6.QtGui import QFont, QIcon 4 | from PySide6.QtOpenGLWidgets import QOpenGLWidget 5 | from .smoothScroll import QSmoothListWidget 6 | from .files import FileTracker 7 | from .local import getPath 8 | from pyunity import (Logger, SceneManager, KeyCode, 9 | MouseCode, KeyState, Loader, Window, WaitForUpdate, WaitForRender, 10 | config, render) 11 | from pyunity.scenes.runner import Runner, ChangeScene 12 | import os 13 | import copy 14 | import time 15 | 16 | def logPatch(func): 17 | def inner(*args, **kwargs): 18 | try: 19 | return func(*args, **kwargs) 20 | except Exception as e: 21 | Logger.LogException(e, stacklevel=2) 22 | return inner 23 | 24 | class QRunner(Runner): 25 | def __init__(self, frame): 26 | super(QRunner, self).__init__() 27 | self.frame = frame 28 | 29 | def open(self): 30 | super(QRunner, self).open() 31 | os.environ["PYUNITY_GL_CONTEXT"] = "1" 32 | self.window = WidgetWindow("Editor") 33 | 34 | def load(self): 35 | super(QRunner, self).load() 36 | 37 | self.eventLoopManager.schedule( 38 | self.scene.updateScripts, self.window.updateFunc, 39 | ups=config.fps, waitFor=WaitForUpdate) 40 | self.eventLoopManager.schedule( 41 | self.scene.Render, self.window.refresh, 42 | main=True, waitFor=WaitForRender) 43 | if self.scene.mainCamera is not None: 44 | self.window.setResize(self.scene.mainCamera.Resize) 45 | self.scene.startOpenGL() 46 | self.scene.startLoop() 47 | 48 | def initialize(self): 49 | self.eventLoopManager.setup() 50 | self.updateFunc = self.eventLoopManager.update 51 | 52 | class PreviewFrame(QOpenGLWidget): 53 | def __init__(self, parent): 54 | super(PreviewFrame, self).__init__(parent) 55 | self.runner = parent.runner 56 | 57 | def initializeGL(self): 58 | Logger.LogLine(Logger.DEBUG, "Compiling objects") 59 | Logger.elapsed.tick() 60 | Logger.LogLine(Logger.INFO, "Compiling shaders") 61 | render.compileShaders() 62 | Logger.LogSpecial(Logger.INFO, Logger.ELAPSED_TIME) 63 | Logger.LogLine(Logger.INFO, "Loading skyboxes") 64 | render.compileSkyboxes() 65 | Logger.LogSpecial(Logger.INFO, Logger.ELAPSED_TIME) 66 | 67 | def paintGL(self): 68 | if self.runner.opened: 69 | try: 70 | self.runner.updateFunc() 71 | except ChangeScene: 72 | self.runner.changeScene() 73 | self.runner.initialize() 74 | elif self.parent().original is not None: 75 | self.parent().original.Render() 76 | 77 | def resizeGL(self, width, height): 78 | if self.runner.opened: 79 | self.runner.scene.mainCamera.Resize(width, height) 80 | elif self.parent().original is not None: 81 | self.parent().original.mainCamera.Resize(width, height) 82 | self.update() 83 | 84 | class OpenGLFrame(QWidget): 85 | SPACER = None 86 | 87 | def __init__(self, parent=None, path=""): 88 | super(OpenGLFrame, self).__init__(parent) 89 | self.vboxLayout = QVBoxLayout(self) 90 | self.vboxLayout.setContentsMargins(2, 2, 2, 2) 91 | self.vboxLayout.setSpacing(2) 92 | self.setLayout(self.vboxLayout) 93 | 94 | self.optionsBar = QWidget(self) 95 | self.hboxLayout = QHBoxLayout(self.optionsBar) 96 | self.hboxLayout.setContentsMargins(0, 0, 0, 0) 97 | self.optionsBar.setLayout(self.hboxLayout) 98 | self.vboxLayout.addWidget(self.optionsBar, 0) 99 | 100 | self.runner = QRunner(self) 101 | SceneManager.runner = self.runner 102 | 103 | self.frame = PreviewFrame(self) 104 | self.frame.setFocusPolicy(Qt.StrongFocus) 105 | self.frame.setMouseTracking(True) 106 | self.vboxLayout.addWidget(self.frame, 1) 107 | 108 | self.timer = QTimer(self) 109 | self.timer.timeout.connect(self.frame.update) 110 | 111 | self.console = None 112 | self.original = None 113 | self.paused = False 114 | self.file_tracker = FileTracker(path) 115 | 116 | def set_buttons(self, buttons): 117 | self.buttons = buttons.buttons 118 | self.buttons[0].clicked.connect(self.start) 119 | self.buttons[1].clicked.connect(self.pause) 120 | self.buttons[2].clicked.connect(self.stop) 121 | 122 | def loadScene(self, scene): 123 | self.original = scene 124 | self.frame.makeCurrent() 125 | self.original.startOpenGL() 126 | self.original.Render() 127 | self.frame.update() 128 | 129 | @logPatch 130 | def start(self, on=None): 131 | if self.runner.opened: 132 | self.stop() 133 | else: 134 | self.frame.makeCurrent() 135 | if self.console.clear_on_run: 136 | self.console.clear() 137 | self.buttons[2].setChecked(False) 138 | self.file_tracker.stop() 139 | 140 | self.runner.setScene(copy.deepcopy(self.original)) 141 | if not self.runner.opened: 142 | self.runner.open() 143 | self.runner.load() 144 | self.runner.initialize() 145 | 146 | self.runner.scene.mainCamera.Resize(self.width(), self.height()) 147 | if not self.paused: 148 | duration = 0 if config.fps == 0 else 1000 / config.fps 149 | self.timer.start(duration) 150 | else: 151 | self.timer.stop() 152 | 153 | @logPatch 154 | def stop(self, on=None): 155 | if self.runner.opened: 156 | self.runner.quit() 157 | self.buttons[0].setChecked(False) 158 | self.buttons[1].setChecked(False) 159 | self.buttons[2].setChecked(True) 160 | self.paused = False 161 | self.frame.update() 162 | self.file_tracker.start(5) 163 | else: 164 | self.buttons[2].setChecked(True) 165 | 166 | def pause(self, on=None): 167 | self.paused = not self.paused 168 | if self.runner.opened: 169 | if self.paused: 170 | self.timer.stop() 171 | else: 172 | self.runner.scene.lastFrame = time.perf_counter() 173 | self.runner.scene.lastFixedFrame = time.perf_counter() 174 | duration = 0 if config.fps == 0 else 1000 / config.fps 175 | self.timer.start(duration) 176 | 177 | def save(self): 178 | if self.runner.opened: 179 | message = QMessageBox() 180 | message.setText("Cannot save scene while running!") 181 | message.setWindowTitle(self.file_tracker.project.name) 182 | message.setStandardButtons(QMessageBox.StandardButton.Ok) 183 | message.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint) 184 | message.setFont(QFont("Segoe UI", 12)) 185 | message.exec() 186 | return 187 | 188 | def callback(): 189 | Loader.ResaveScene(self.original, self.file_tracker.project) 190 | message.done(0) 191 | 192 | message = QMessageBox() 193 | message.setText("Saving scene...") 194 | message.setWindowTitle(self.file_tracker.project.name) 195 | message.setStandardButtons(QMessageBox.StandardButton.NoButton) 196 | message.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint) 197 | QTimer.singleShot(2000, callback) 198 | message.setFont(QFont("Segoe UI", 12)) 199 | message.exec() 200 | 201 | def on_switch(self): 202 | self.console.timer.stop() 203 | 204 | mousemap = { 205 | Qt.LeftButton: MouseCode.Left, 206 | Qt.RightButton: MouseCode.Right, 207 | Qt.MiddleButton: MouseCode.Middle, 208 | } 209 | 210 | keymap = { 211 | Qt.Key_A: KeyCode.A, 212 | Qt.Key_B: KeyCode.B, 213 | Qt.Key_C: KeyCode.C, 214 | Qt.Key_D: KeyCode.D, 215 | Qt.Key_E: KeyCode.E, 216 | Qt.Key_F: KeyCode.F, 217 | Qt.Key_G: KeyCode.G, 218 | Qt.Key_H: KeyCode.H, 219 | Qt.Key_I: KeyCode.I, 220 | Qt.Key_J: KeyCode.J, 221 | Qt.Key_K: KeyCode.K, 222 | Qt.Key_L: KeyCode.L, 223 | Qt.Key_M: KeyCode.M, 224 | Qt.Key_N: KeyCode.N, 225 | Qt.Key_O: KeyCode.O, 226 | Qt.Key_P: KeyCode.P, 227 | Qt.Key_Q: KeyCode.Q, 228 | Qt.Key_R: KeyCode.R, 229 | Qt.Key_S: KeyCode.S, 230 | Qt.Key_T: KeyCode.T, 231 | Qt.Key_U: KeyCode.U, 232 | Qt.Key_V: KeyCode.V, 233 | Qt.Key_W: KeyCode.W, 234 | Qt.Key_X: KeyCode.X, 235 | Qt.Key_Y: KeyCode.Y, 236 | Qt.Key_Z: KeyCode.Z, 237 | Qt.Key_Space: KeyCode.Space, 238 | Qt.Key_0: KeyCode.Alpha0, 239 | Qt.Key_1: KeyCode.Alpha1, 240 | Qt.Key_2: KeyCode.Alpha2, 241 | Qt.Key_3: KeyCode.Alpha3, 242 | Qt.Key_4: KeyCode.Alpha4, 243 | Qt.Key_5: KeyCode.Alpha5, 244 | Qt.Key_6: KeyCode.Alpha6, 245 | Qt.Key_7: KeyCode.Alpha7, 246 | Qt.Key_8: KeyCode.Alpha8, 247 | Qt.Key_9: KeyCode.Alpha9, 248 | Qt.Key_F1: KeyCode.F1, 249 | Qt.Key_F2: KeyCode.F2, 250 | Qt.Key_F3: KeyCode.F3, 251 | Qt.Key_F4: KeyCode.F4, 252 | Qt.Key_F5: KeyCode.F5, 253 | Qt.Key_F6: KeyCode.F6, 254 | Qt.Key_F7: KeyCode.F7, 255 | Qt.Key_F8: KeyCode.F8, 256 | Qt.Key_F9: KeyCode.F9, 257 | Qt.Key_F10: KeyCode.F10, 258 | Qt.Key_F11: KeyCode.F11, 259 | Qt.Key_F12: KeyCode.F12, 260 | Qt.Key_Up: KeyCode.Up, 261 | Qt.Key_Down: KeyCode.Down, 262 | Qt.Key_Left: KeyCode.Left, 263 | Qt.Key_Right: KeyCode.Right, 264 | } 265 | 266 | numberkeys = { 267 | Qt.Key_0: KeyCode.Keypad0, 268 | Qt.Key_1: KeyCode.Keypad1, 269 | Qt.Key_2: KeyCode.Keypad2, 270 | Qt.Key_3: KeyCode.Keypad3, 271 | Qt.Key_4: KeyCode.Keypad4, 272 | Qt.Key_5: KeyCode.Keypad5, 273 | Qt.Key_6: KeyCode.Keypad6, 274 | Qt.Key_7: KeyCode.Keypad7, 275 | Qt.Key_8: KeyCode.Keypad8, 276 | Qt.Key_9: KeyCode.Keypad9, 277 | } 278 | 279 | def mouseMoveEvent(self, event): 280 | super(OpenGLFrame, self).mouseMoveEvent(event) 281 | if hasattr(self.runner, "window"): 282 | self.runner.window.mpos = [event.x(), event.y()] 283 | 284 | def mousePressEvent(self, event): 285 | super(OpenGLFrame, self).mousePressEvent(event) 286 | if hasattr(self.runner, "window"): 287 | self.runner.window.mbuttons[self.mousemap[event.button()]] = KeyState.DOWN 288 | 289 | def mouseReleaseEvent(self, event): 290 | super(OpenGLFrame, self).mouseReleaseEvent(event) 291 | if hasattr(self.runner, "window"): 292 | self.runner.window.mbuttons[self.mousemap[event.button()]] = KeyState.UP 293 | 294 | def keyPressEvent(self, event): 295 | super(OpenGLFrame, self).keyPressEvent(event) 296 | if hasattr(self.runner, "window"): 297 | if event.key() not in self.keymap: 298 | return 299 | if event.key() in self.numberkeys and event.modifiers() & Qt.KeypadModifier: 300 | self.runner.window.keys[self.numberkeys[event.key()]] = KeyState.DOWN 301 | else: 302 | self.runner.window.keys[self.keymap[event.key()]] = KeyState.DOWN 303 | 304 | def keyReleaseEvent(self, event): 305 | super(OpenGLFrame, self).keyReleaseEvent(event) 306 | if hasattr(self.runner, "window"): 307 | if event.key() not in self.keymap: 308 | return 309 | if event.key() in self.numberkeys and event.modifiers() & Qt.KeypadModifier: 310 | self.runner.window.keys[self.numberkeys[event.key()]] = KeyState.UP 311 | else: 312 | self.runner.window.keys[self.keymap[event.key()]] = KeyState.UP 313 | 314 | class WidgetWindow(Window.ABCWindow): 315 | def __init__(self, name): 316 | self.name = name 317 | self.mpos = [0, 0] 318 | self.mbuttons = [KeyState.NONE, KeyState.NONE, KeyState.NONE] 319 | self.keys = [KeyState.NONE for i in range(KeyCode.Right + 1)] 320 | 321 | def setResize(self, resize): 322 | self.resize = resize 323 | 324 | def checkKeys(self): 325 | for i in range(len(self.keys)): 326 | if self.keys[i] == KeyState.UP: 327 | self.keys[i] = KeyState.NONE 328 | elif self.keys[i] == KeyState.DOWN: 329 | self.keys[i] = KeyState.PRESS 330 | 331 | def checkMouse(self): 332 | for i in range(len(self.mbuttons)): 333 | if self.mbuttons[i] == KeyState.UP: 334 | self.mbuttons[i] = KeyState.NONE 335 | elif self.mbuttons[i] == KeyState.DOWN: 336 | self.mbuttons[i] = KeyState.PRESS 337 | 338 | def getMouse(self, mousecode, keystate): 339 | if keystate == KeyState.PRESS: 340 | if self.mbuttons[mousecode] in [KeyState.PRESS, KeyState.DOWN]: 341 | return True 342 | if self.mbuttons[mousecode] == keystate: 343 | return True 344 | return False 345 | 346 | def getKey(self, keycode, keystate): 347 | if keystate == KeyState.PRESS: 348 | if self.keys[keycode] in [KeyState.PRESS, KeyState.DOWN]: 349 | return True 350 | if self.keys[keycode] == keystate: 351 | return True 352 | return False 353 | 354 | def getMousePos(self): 355 | return self.mpos 356 | 357 | def quit(self): 358 | pass 359 | 360 | def updateFunc(self): 361 | pass 362 | 363 | def refresh(self): 364 | pass 365 | 366 | class SceneEditor: 367 | pass 368 | 369 | class Console(QSmoothListWidget): 370 | SPACER = None 371 | 372 | def __init__(self, parent=None): 373 | super(Console, self).__init__(parent) 374 | self.scrollRatio = 0.5 375 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 376 | self.setIconSize(QSize(50, 50)) 377 | self.entries = [] 378 | self.pending_entries = [] 379 | self.clear_on_run = True 380 | 381 | self.timer = QTimer(self) 382 | self.timer.timeout.connect(self.on_switch) 383 | Logger.LogLine = self.modded_log(Logger.LogLine) 384 | 385 | def add_entry(self, timestamp, level, text): 386 | entry = ConsoleEntry(timestamp, level, text) 387 | self.entries.append(entry) 388 | self.addItem(entry) 389 | 390 | def clear(self): 391 | self.entries = [] 392 | self.pending_entries = [] 393 | super(Console, self).clear() 394 | 395 | def modded_log(self, func): 396 | def inner(*args, **kwargs): 397 | timestamp, msg = func(*args, **kwargs) 398 | if args[0] != Logger.DEBUG: 399 | self.pending_entries.append([timestamp, args[0], msg]) 400 | return timestamp, msg 401 | return inner 402 | 403 | def on_switch(self): 404 | self.pending_entries = self.pending_entries[-100:] 405 | if len(self.pending_entries) == 100: 406 | self.clear() 407 | for entry in self.pending_entries: 408 | self.add_entry(*entry) 409 | self.pending_entries = [] 410 | self.timer.start(100) 411 | 412 | class ConsoleEntry(QListWidgetItem): 413 | icon_map = { 414 | Logger.ERROR: "error.png", 415 | Logger.INFO: "info.png", 416 | Logger.OUTPUT: "output.png", 417 | Logger.WARN: "warning.png" 418 | } 419 | def __init__(self, timestamp, level, text): 420 | super(ConsoleEntry, self).__init__(text + "\n" + timestamp) 421 | self.setFont(QFont("Segoe UI", 12)) 422 | self.setIcon(QIcon(getPath( 423 | "icons/console/" + ConsoleEntry.icon_map[level]))) 424 | -------------------------------------------------------------------------------- /pyunity_editor/inspector.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import ( 2 | Signal, Qt, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation, QTimer) 3 | from PySide6.QtWidgets import * 4 | from PySide6.QtGui import * 5 | from .smoothScroll import QSmoothListWidget, QSmoothScrollArea 6 | from pathlib import Path 7 | import pyunity as pyu 8 | import re 9 | 10 | # test string "clearBoxColor5_3withoutLines" 11 | # turns into "Clear Box Color 5 3 Without Lines" 12 | 13 | regex = re.compile("(?