├── 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 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/branch_open-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/branch_open.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/branch_closed.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/branch_open-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/branch_open.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/branch_closed-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/branch_closed-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/stylesheet-vline.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/undock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/hsepartoolbar.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/stylesheet-branch-end.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/stylesheet-branch-more.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/stylesheet-branch-end.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/stylesheet-branch-more.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/stylesheet-branch-end-closed.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/stylesheet-branch-end-open.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/hsepartoolbar.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/stylesheet-branch-end-closed.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/stylesheet-branch-end-open.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/sizegrip.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/sizegrip.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/hmovetoolbar.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/hmovetoolbar.svg:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/checkbox_unchecked.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_unchecked-hover.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_unchecked_disabled.svg:
--------------------------------------------------------------------------------
1 |
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 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_checked.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/radio_unchecked.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_checked-hover.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/checkbox_checked_disabled.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/undock-hover.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_checked_disabled.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/undock-hover.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/radio_unchecked_disabled.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/radio_unchecked-hover.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/radio_unchecked_disabled.svg:
--------------------------------------------------------------------------------
1 |
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 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/radio_checked.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/radio_checked-hover.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/radio_checked_disabled.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/radio_checked_disabled.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/right_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/spinup_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/spinup_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/right_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/right_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/checkbox_indeterminate.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_indeterminate.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/checkbox_indeterminate_disabled.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/close-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_indeterminate-hover.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/checkbox_indeterminate_disabled.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/close-pressed.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/close-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/close-pressed.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/up_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/up_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/down_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/up_arrow-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/up_arrow-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/left_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/up_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/down_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/right_arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/up_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/down_arrow-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/down_arrow-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/left_arrow.svg:
--------------------------------------------------------------------------------
1 |
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 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/left_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/down_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/left_arrow_disabled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/vsepartoolbars.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/vsepartoolbars.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/dark/vmovetoolbar.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/pyunity_editor/theme/light/vmovetoolbar.svg:
--------------------------------------------------------------------------------
1 |
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 | [](https://github.com/pyunity/pyunity-gui/blob/master/LICENSE)
4 | [](https://pypi.python.org/pypi/pyunity-gui)
5 | [](https://pypi.python.org/pypi/pyunity-gui)
6 | [](https://lgtm.com/projects/g/pyunity/pyunity-gui/context:python)
7 | [](https://lgtm.com/projects/g/pyunity/pyunity-gui/alerts/)
8 | [](https://discord.gg/zTn48BEbF9)
9 | [](https://gitter.im/pyunity/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
10 | [](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("(?