diff --git a/.all-contributorsrc b/.all-contributorsrc
index e840bc6..a68ae0a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2,7 +2,7 @@
"files": [
"README.md"
],
- "imageSize": 100,
+ "imageSize": 96,
"commit": false,
"contributors": [
{
@@ -52,7 +52,7 @@
]
}
],
- "contributorsPerLine": 7,
+ "contributorsPerLine": 6,
"projectName": "scratch2python",
"projectOwner": "Secret-chest",
"repoType": "github",
diff --git a/.gitignore b/.gitignore
index 58ffaad..5fdc31e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# Project exclude paths
/venv/
/.idea/
-/download/**
\ No newline at end of file
+/download/**
+/assets/**
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 02dd564..f1f87fb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,87 @@
-## Coding Style
+## British English for docs, comments and names
+Also correct the old ones written in American English. I recently switched (excuse me), and there's no going back.
+This includes ending words in -ise and -yse, not -ize and -yze.
+I have set my LanguageTool (I use PyCharm) to catch American English spellings, if you have it too, it may help.
+The only exception is “colour”, American English is the
+standard in programming so please use “color” in names. You should still use “colour” when not referring to a name.
+If you don't know the differences, [this article on Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_American_and_British_English) may help you.
+
+## Code style guidelines
+For the most part, we follow PEP 8.
+
+### Naming style
Use `mixedCase` for most names. For class names, use `CamelCase`.
-Always use DOUBLE QUOTES (like ")!
-## Methods
+### Quotes in code
+Always use DOUBLE QUOTES (like `"this"`)! In some fonts single quotes look invisible.
+
+### Indentation
+Only use 4 spaces. I did not make commit checks yet (I will), but you should be sorry if you use anything other than 4
+spaces in Python.
+
+#### Hanging indents
+For most hanging indents the rule is to align them with the opening, like these:
+```python
+someList = [1, 2, 3,
+ 4, 5, 6,]
+
+if (1 == 1
+ and 1 + 1 == 2
+ and not "this".startswith("that")):
+ # At least I have no problem with the confusion described in PEP 8.
+ pass
+```
+
+### Comments
+**Always** put a space after the pound sign (#)! Separate inline comments with two spaces from the rest of the line.
+
+### Break before operators!
+> To solve this readability problem, mathematicians and their publishers follow the opposite convention. Donald Knuth explains the traditional rule in his Computers and Typesetting series: “Although formulas within a paragraph always break after binary operations and relations, displayed formulas always break before binary operations”.
+> Following the tradition from mathematics usually results in more readable code
+> **- PEP 8**
+```python
+# Do it like described in PEP 8.
+
+sum_ = (10000
+ + 20000
+ + 30000)
+```
+
+### Avoid backslashes. Use parentheses to be able to break lines when possible.
+
+### Blank lines
+> Surround top-level function and class definitions with two blank lines.
+> Method definitions inside a class are surrounded by a single blank line.
+> **- PEP 8**
+
+Also group related code together and add some blank lines to separate it. Look at the source code already defined
+to see examples of this. Do not add blank line _after_ comments! Don't worry, I will not reject your PR if you don't separate the code like I do.
+
+### Imports on separate lines
+```python
+# This is right:
+import os
+import sys
+
+# This is wrong:
+import sys, os
+
+# This is OK though:
+from subprocess import Popen, PIPE
+```
+
+### Wildcard imports
+Wildcard imports look like this:
+```python
+from json import *
+```
+Do not use them as they create confusion
-Please look for methods instead of writing it again. If it may be done many times, create a method.
+### Whitespace should make sense
+Whitespace in Python literally follows the rules of whitespace in English for punctuation.
+For operators, the slice colon should have no space on either side, but all others should have one space
+on either side. The equal in arguments (`function(x=y)`) should have no space though.
-## GNU/Linux
+Avoid trailing whitespace as it's invisible and confusing.
-Scratch2Python is developed on GNU/Linux. While Windows support is important, Linux is too. Don't do anything that dosen't work on GNU/Linux.
diff --git a/MainMenu.glade b/MainMenu.glade~
similarity index 85%
rename from MainMenu.glade
rename to MainMenu.glade~
index 31543e3..ae851aa 100644
--- a/MainMenu.glade
+++ b/MainMenu.glade~
@@ -334,6 +334,12 @@
4
10
+
diff --git a/MainMenu.ui~ b/MainMenu.ui~
new file mode 100644
index 0000000..4934b8c
--- /dev/null
+++ b/MainMenu.ui~
@@ -0,0 +1,396 @@
+
+
+
+
+
+ True
+ False
+ dialog-information
+
+
+ 576
+ 320
+ False
+ 8
+ Scratch2Python
+ False
+ scratch
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Scratch2Python project loader
+ 0
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ version {VERSION}
+ 0
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ center
+ center
+ 8
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-executable
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Load local project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from your computer
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-partial-download
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Cache project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from scratch.mit.edu
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ user-bookmarks
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Featured projects
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ by us, TurboWarp, etc.
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ preferences-system
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Settings
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ Need a GUI for browsing projects on scratch.mit.edu?
+ True
+ True
+ True
+ none
+ https://example.org
+
+
+ False
+ True
+ 0
+
+
+
+
+ About
+ True
+ True
+ True
+ aboutIcon
+ True
+
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 9554bd8..d934216 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,62 @@
+#  Scratch2Python
+
+
+
[](#contributors-)
-
-
+Scratch2Python is a Scratch project interpreter that runs Scratch projects in Python, **using pygame to render your sprites**.
+Go to #23 if you want to change the name.
-# Scratch2Python
-Scratch2Python is a Scratch project interpreter that runs Scratch projects in Python, using pygame to render your sprites.
+Unlike others, Scratch2Python can actually _display_ your projects, not only process the variables, control flow, math and logic in the project,
+and simply print speech bubble blocks.
-A GUI for accessing the Scratch website is planned. It will most probably use... Qt. I would love using GTK, but **WINDOWS...** [Or maybe not](https://www.gtk.org/docs/installations/windows).
+It's not finished though. It only supports a few blocks, but it supports them well. The goal is to support almost everything
+(save for online services like text-to-speech) when we'll declare it as done. (Of course we'll still update things like the GUI.)
-Scratch2Python may only be the temporary name, as this may get confused with [Scratch2Py](https://github.com/The-Cloud-Dev/scratch2py). See #23 for more information.
-## 📝 Requirements
-Install from requirements.txt (Scratch2Python also needs Python 3.8 or newer).
+Scratch2Python is not *yet* a transpiler.
-On Windows, Scratch2Python needs to be installed in a non-protected folder.
-By default, the "Documents" folder is protected. Installing it anywhere else will work.
+## 📝 Requirements and installation
+[📥 Visit the website to easily download Scratch2Python](https://secret-chest.github.io/s2p/download/)
-## 📘 Docs
-[Read the wiki here](https://github.com/Secret-chest/scratch2python/wiki). Also read `CONTRIBUTING.md`.
+Then install from requirements.txt using:
-## 🔨 How to use
-Assuming that you installed all necessary requirements, place your sb3 files somewhere accessible, or in the Scratch2Python folder. You can use an absolute or relative path.
-Then, go to `config.py` and change the projectFileName variable to your project file.
-There you can also choose to use a command-line argument, or an interactive prompt. The variable option is the default as it's more useful for testing.
+`pip install -r requirements.txt`
-Now, just run `python3 main.py` and the project will start!
+Scratch2Python also needs Python 3.8 or newer. [Click here to download Python for Windows and Mac](https://www.python.org/downloads/).
+On recent GNU/Linux distros (Ubuntu 20.04+, Debian 11+, Linux Mint 20+, Fedora 32+, updated rolling-release distros),
+Python 3 is preinstalled or downloadable from the repositories. Check your distro's documentation for more info.
+
+
+Getting errors on Windows?
+
+On Windows, Scratch2Python needs to be installed in a non-protected folder.
+By default, the “Documents” folder is protected. Installing it anywhere else will work.
-### ✅ Config
-The `config.py` file contains some more configuration options.
+To unprotect the “Documents” folder, go to its Properties and uncheck the “Read-only” checkbox.
-Each of them is nicely explained, so why not just check it out?
+
-## 🌐 Localization
-To translate Scratch2Python, add a new file in the `lang` directory. Copy the English file for reference, and replace the string.
+## ▶️ Setting the project
+Now that you have downloaded Scratch2Python, let's run a project.
-Then add it on the supported languages list both here and in `config.py`.
-Though I would not recommend translating it right now. It is still very WIP and you would have to update your language
-file very frequently.
+### Using test mode
+For now Scratch2Python is in test mode by default. That means it will always run the project
+defined in the `projectFileName` variable in `config.py`. The variable can be a path, which will load the project there,
+or a Scratch ID / URL, which will download and cache the specified online project. No support for downloading unshared
+projects is provided! (The Scratch Team will implement access control, and it won't be possible anyway soon too.)
-Currently supported languages:
+### GUI (experimental)
+Change the `testMode` variable to `False` so Scratch2Python will open a GUI when started. For now this is a basic filechooser,
+but we'll implement a proper full GUI soon.
-| Language code | Language name (English) | Language name (translated) | Flag |
-|---------------|-------------------------|----------------------------|------|
-| en | English | English | 🇬🇧 |
-| ro | Romanian | limba română | 🇷🇴 |
+## 📖 Wiki
+Our [GitHub Wiki](https://github.com/Secret-chest/scratch2python/wiki) may have some outdated information and it is short, but still useful.
-## ✨ Contributors
+## 🧑💻 Contributors
-These people have contributed to this project and helped shape it ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
+Thanks to the people listed here for contributing to Scratch2Python! ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
diff --git a/__pycache__/block.cpython-310.pyc b/__pycache__/block.cpython-310.pyc
index a6a0104..eab4568 100644
Binary files a/__pycache__/block.cpython-310.pyc and b/__pycache__/block.cpython-310.pyc differ
diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc
index 3e6aaa3..d03bbf1 100644
Binary files a/__pycache__/config.cpython-310.pyc and b/__pycache__/config.cpython-310.pyc differ
diff --git a/__pycache__/downloader.cpython-310.pyc b/__pycache__/downloader.cpython-310.pyc
new file mode 100644
index 0000000..115ecd8
Binary files /dev/null and b/__pycache__/downloader.cpython-310.pyc differ
diff --git a/__pycache__/eventContainer.cpython-310.pyc b/__pycache__/eventContainer.cpython-310.pyc
new file mode 100644
index 0000000..55f4405
Binary files /dev/null and b/__pycache__/eventContainer.cpython-310.pyc differ
diff --git a/__pycache__/sb3Unpacker.cpython-310.pyc b/__pycache__/sb3Unpacker.cpython-310.pyc
index 79bd126..c956b51 100644
Binary files a/__pycache__/sb3Unpacker.cpython-310.pyc and b/__pycache__/sb3Unpacker.cpython-310.pyc differ
diff --git a/__pycache__/scratch.cpython-310.pyc b/__pycache__/scratch.cpython-310.pyc
index 2a2213c..8e46dc7 100644
Binary files a/__pycache__/scratch.cpython-310.pyc and b/__pycache__/scratch.cpython-310.pyc differ
diff --git a/__pycache__/target.cpython-310.pyc b/__pycache__/target.cpython-310.pyc
index 97d419b..e59f57d 100644
Binary files a/__pycache__/target.cpython-310.pyc and b/__pycache__/target.cpython-310.pyc differ
diff --git a/__pycache__/targetSprite.cpython-310.pyc b/__pycache__/targetSprite.cpython-310.pyc
index a0415b7..40b61d0 100644
Binary files a/__pycache__/targetSprite.cpython-310.pyc and b/__pycache__/targetSprite.cpython-310.pyc differ
diff --git a/assets/4af792de2989798e6848a92117092aca.svg b/assets/4af792de2989798e6848a92117092aca.svg
deleted file mode 100644
index 8372e4c..0000000
--- a/assets/4af792de2989798e6848a92117092aca.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/83c36d806dc92327b9e7049a565c6bff.wav b/assets/83c36d806dc92327b9e7049a565c6bff.wav
deleted file mode 100644
index 45742d5..0000000
Binary files a/assets/83c36d806dc92327b9e7049a565c6bff.wav and /dev/null differ
diff --git a/assets/bcf454acf82e4504149f7ffe07081dbc.svg b/assets/bcf454acf82e4504149f7ffe07081dbc.svg
deleted file mode 100644
index 03df23e..0000000
--- a/assets/bcf454acf82e4504149f7ffe07081dbc.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/cd21514d0531fdffb22204e0ec5ed84a.svg b/assets/cd21514d0531fdffb22204e0ec5ed84a.svg
deleted file mode 100644
index 15f7311..0000000
--- a/assets/cd21514d0531fdffb22204e0ec5ed84a.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/metadata.json b/assets/metadata.json
deleted file mode 100644
index 73d5518..0000000
--- a/assets/metadata.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":729175960,"title":"EventHandlers - A Scratch2Python Test Project","description":"","instructions":"","visibility":"visible","public":true,"comments_allowed":true,"is_published":true,"author":{"id":57831979,"username":"mumu245","scratchteam":false,"history":{"joined":"1900-01-01T00:00:00.000Z"},"profile":{"id":null,"images":{"90x90":"https://cdn2.scratch.mit.edu/get_image/user/57831979_90x90.png?v=","60x60":"https://cdn2.scratch.mit.edu/get_image/user/57831979_60x60.png?v=","55x55":"https://cdn2.scratch.mit.edu/get_image/user/57831979_55x55.png?v=","50x50":"https://cdn2.scratch.mit.edu/get_image/user/57831979_50x50.png?v=","32x32":"https://cdn2.scratch.mit.edu/get_image/user/57831979_32x32.png?v="}}},"image":"https://cdn2.scratch.mit.edu/get_image/project/729175960_480x360.png","images":{"282x218":"https://cdn2.scratch.mit.edu/get_image/project/729175960_282x218.png?v=1663778011","216x163":"https://cdn2.scratch.mit.edu/get_image/project/729175960_216x163.png?v=1663778011","200x200":"https://cdn2.scratch.mit.edu/get_image/project/729175960_200x200.png?v=1663778011","144x108":"https://cdn2.scratch.mit.edu/get_image/project/729175960_144x108.png?v=1663778011","135x102":"https://cdn2.scratch.mit.edu/get_image/project/729175960_135x102.png?v=1663778011","100x80":"https://cdn2.scratch.mit.edu/get_image/project/729175960_100x80.png?v=1663778011"},"history":{"created":"2022-09-07T15:49:39.000Z","modified":"2022-09-21T16:33:31.000Z","shared":"2022-09-21T16:33:31.000Z"},"stats":{"views":1,"loves":0,"favorites":0,"remixes":0},"remix":{"parent":null,"root":null},"project_token":"1663778466_0ba90b15796edf4cf6d4b45e68197a5a074256d2"}
\ No newline at end of file
diff --git a/assets/project.json b/assets/project.json
index e210dba..a89e23f 100644
--- a/assets/project.json
+++ b/assets/project.json
@@ -1 +1 @@
-{"targets":[{"isStage":true,"name":"Stage","variables":{},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"name":"backdrop1","dataFormat":"svg","assetId":"cd21514d0531fdffb22204e0ec5ed84a","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":"ro"},{"isStage":false,"name":"Cat","variables":{},"lists":{},"broadcasts":{},"blocks":{"4NEH+1=zgYzgtfTbI_g_":{"opcode":"event_whenkeypressed","next":"m=bR4H@v7G-#HYSLw?zr","parent":null,"inputs":{},"fields":{"KEY_OPTION":["space",null]},"shadow":false,"topLevel":true,"x":264,"y":311},"m=bR4H@v7G-#HYSLw?zr":{"opcode":"motion_changexby","next":null,"parent":"4NEH+1=zgYzgtfTbI_g_","inputs":{"DX":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"cat-a","bitmapResolution":1,"dataFormat":"svg","assetId":"bcf454acf82e4504149f7ffe07081dbc","md5ext":"bcf454acf82e4504149f7ffe07081dbc.svg","rotationCenterX":48,"rotationCenterY":50}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":2,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"},{"isStage":false,"name":"Cat2","variables":{},"lists":{},"broadcasts":{},"blocks":{"2**JJA=gqz.x-q-C:[)y":{"opcode":"event_whenkeypressed","next":"iDb,X8,-{Eg-.!~@q+1i","parent":null,"inputs":{},"fields":{"KEY_OPTION":["space"]},"shadow":false,"topLevel":true,"x":120,"y":24},"iDb,X8,-{Eg-.!~@q+1i":{"opcode":"motion_changexby","next":null,"parent":"2**JJA=gqz.x-q-C:[)y","inputs":{"DX":[1,[4,"-10"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"cat-a","bitmapResolution":1,"dataFormat":"svg","assetId":"4af792de2989798e6848a92117092aca","md5ext":"4af792de2989798e6848a92117092aca.svg","rotationCenterX":47.67898252524472,"rotationCenterY":49.49923017660271}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"1.1.6","agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.101 Safari/537.36"}}
\ No newline at end of file
+{"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["my variable",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"name":"Hay Field","bitmapResolution":2,"dataFormat":"png","assetId":"da102a69d135973e0fc139131dec785a","rotationCenterX":480,"rotationCenterY":360}],"sounds":[{"name":"pop","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"Sprite1","variables":{},"lists":{},"broadcasts":{},"blocks":{"D:TgXIVyJrJ_k|v^Lisx":{"opcode":"event_whenflagclicked","next":"ptHi3IywC$(o5mihO!ET","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":500,"y":579},"ptHi3IywC$(o5mihO!ET":{"opcode":"control_forever","next":null,"parent":"D:TgXIVyJrJ_k|v^Lisx","inputs":{"SUBSTACK":[2,"vrpIp+Y+4DNbk)DEO@kn"]},"fields":{},"shadow":false,"topLevel":false},"vrpIp+Y+4DNbk)DEO@kn":{"opcode":"motion_pointtowards","next":null,"parent":"ptHi3IywC$(o5mihO!ET","inputs":{"TOWARDS":[1,"9fYz0RDgmONr6LZ3ggiH"]},"fields":{},"shadow":false,"topLevel":false},"9fYz0RDgmONr6LZ3ggiH":{"opcode":"motion_pointtowards_menu","next":null,"parent":"vrpIp+Y+4DNbk)DEO@kn","inputs":{},"fields":{"TOWARDS":["Crystal",null]},"shadow":true,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"Arrow1-a","bitmapResolution":1,"dataFormat":"svg","assetId":"be8fcd10da0b082f8d4775088ef7bd52","rotationCenterX":28,"rotationCenterY":23}],"sounds":[{"name":"Meow","assetId":"83c36d806dc92327b9e7049a565c6bff","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":102.09042871030996,"draggable":false,"rotationStyle":"all around"},{"isStage":false,"name":"Crystal","variables":{},"lists":{},"broadcasts":{},"blocks":{"LM+.Uo#HB1kE6,D02hS}":{"opcode":"event_whenflagclicked","next":"k[fbM51((#Fz45U+/c}U","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":499,"y":460},"k[fbM51((#Fz45U+/c}U":{"opcode":"control_forever","next":null,"parent":"LM+.Uo#HB1kE6,D02hS}","inputs":{"SUBSTACK":[2,"u/fFS`XChmDRFm;%GOp:"]},"fields":{},"shadow":false,"topLevel":false},"u/fFS`XChmDRFm;%GOp:":{"opcode":"motion_turnright","next":"Fpi~c0Aar+ud+MdN#CkK","parent":"k[fbM51((#Fz45U+/c}U","inputs":{"DEGREES":[1,[4,"15"]]},"fields":{},"shadow":false,"topLevel":false},"Fpi~c0Aar+ud+MdN#CkK":{"opcode":"motion_movesteps","next":null,"parent":"u/fFS`XChmDRFm;%GOp:","inputs":{"STEPS":[1,[4,"20"]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"name":"crystal-a","bitmapResolution":1,"dataFormat":"svg","assetId":"ecd1e7805b37db4caf207b7eef2b7a42","md5ext":"ecd1e7805b37db4caf207b7eef2b7a42.svg","rotationCenterX":15,"rotationCenterY":15},{"name":"crystal-b","bitmapResolution":1,"dataFormat":"svg","assetId":"0a7b872042cecaf30cc154c0144f002b","md5ext":"0a7b872042cecaf30cc154c0144f002b.svg","rotationCenterX":12,"rotationCenterY":24}],"sounds":[{"name":"Magic Spell","assetId":"1cb60ecdb1075c8769cb346d5c2a22c7","dataFormat":"wav","format":"adpcm","rate":22050,"sampleCount":43689,"md5ext":"1cb60ecdb1075c8769cb346d5c2a22c7.wav"},{"name":"collect","assetId":"32514c51e03db680e9c63857b840ae78","dataFormat":"wav","format":"adpcm","rate":22050,"sampleCount":14225,"md5ext":"32514c51e03db680e9c63857b840ae78.wav"}],"volume":100,"layerOrder":2,"visible":true,"x":50.26527836193807,"y":-10.767159525430722,"size":100,"direction":105,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"1.5.76","agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"}}
\ No newline at end of file
diff --git a/block.py b/block.py
index d61d7fa..d571ea8 100644
--- a/block.py
+++ b/block.py
@@ -11,6 +11,8 @@
import config
import math
import pygame
+import eventContainer
+from scratch import KEY_MAPPING
i18n.set("locale", config.language)
i18n.set("filename_format", "{locale}.{format}")
@@ -35,14 +37,16 @@ def __init__(self):
self.timeDelay = 0 # wait time from the wait block
self.target = None # parent
self.substack = set() # blocks inside
+ self.substack2 = set() # only for if then else
self.script = set() # blocks below
self.screenRefresh = False # do a screen refresh
self.inEventLoop = False
self.value = None # reported value
self.repeatCounter = None # for repeat block
+ self.canRun = None # for if then / if then else
# Evaluates block value (for reporters)
- def evaluateBlockValue(self):
+ def evaluateBlockValue(self, eventContainer=eventContainer.EventContainer):
if self.opcode == "operator_add": # () + ()
self.value = float(self.getInputValue("num1")) + float(self.getInputValue("num2"))
return self.value
@@ -58,6 +62,7 @@ def evaluateBlockValue(self):
except ZeroDivisionError:
raise ZeroDivisionError(_("zero-division-error"))
return self.value
+
elif self.opcode == "operator_random": # pick random from () to ()
decimals1 = len(str(math.modf(float(self.getInputValue("from"))))) - 2
decimals2 = len(str(math.modf(float(self.getInputValue("to"))))) - 2
@@ -67,34 +72,66 @@ def evaluateBlockValue(self):
decimals = decimals2
self.value = random.randint(int(self.getInputValue("from")) * 10 ** decimals, int(self.getInputValue("to")) * 10 ** decimals) / 10 ** decimals
return self.value
+
+ elif self.opcode == "operator_equals": # () = ()
+ self.value = self.getInputValue("operand1") == self.getInputValue("operand2")
+ return self.value
+ elif self.opcode == "operator_lt": # () < ()
+ self.value = self.getInputValue("operand1") < self.getInputValue("operand2")
+ return self.value
+ elif self.opcode == "operator_gt": # () > ()
+ self.value = self.getInputValue("operand1") > self.getInputValue("operand2")
+ return self.value
+
elif self.opcode == "motion_xposition": # x position
self.value = self.target.x
return self.value
elif self.opcode == "motion_xposition": # y position
self.value = self.target.y
return self.value
+ elif self.opcode == "motion_direction": # y position
+ self.value = self.target.direction
+ return self.value
+
elif self.opcode == "sensing_mousex": # mouse x
newX, newY = pygame.mouse.get_pos()
newX = newX - config.screenWidth // 2
self.value = newX
- return newX
+ return self.value
elif self.opcode == "sensing_mousey": # mouse y
newX, newY = pygame.mouse.get_pos()
- newY = newY - config.screenWidth // 2
+ newY = newY - config.screenHeight // 2
self.value = newY
- return newY
+ return self.value
+
+ elif self.opcode == "sensing_keypressed": # key pressed?
+ self.value = KEY_MAPPING[self.getMenuValue("key_option")] in eventContainer.keys
+ return self.value
+ elif self.opcode == "sensing_mousedown": # mouse down?
+ self.value = pygame.mouse.get_pressed()[0]
+ return self.value
+
+ elif self.opcode == "operator_not": # not <>
+ self.value = not self.target.blocks[self.getBlockInputValue("operand")].evaluateBlockValue(eventContainer)
+ return self.value
+ elif self.opcode == "operator_and": # <> and <>
+ self.value = self.target.blocks[self.getBlockInputValue("operand1")].evaluateBlockValue(eventContainer) and self.target.blocks[self.getBlockInputValue("operand2")].evaluateBlockValue(eventContainer)
+ return self.value
+ elif self.opcode == "operator_or": # <> or <>
+ self.value = self.target.blocks[self.getBlockInputValue("operand1")].evaluateBlockValue(eventContainer) or self.target.blocks[self.getBlockInputValue("operand2")].evaluateBlockValue(eventContainer)
+ return self.value
# Returns block input value
def getBlockInputValue(self, inputId):
return self.inputs[inputId.upper()][1]
# Returns block input value
- def getInputValue(self, inputId, lookIn=(1, 1)):
- if self.inputs[inputId.upper()][lookIn[0]][0] in {4, 0, 5, 6}:
+ def getInputValue(self, inputId, lookIn=(1, 1), eventContainer=eventContainer.EventContainer()):
+ if self.inputs[inputId.upper()][lookIn[0]][0] in {4, 0, 5, 6, 8}:
return self.inputs[inputId.upper()][lookIn[0]][1] or 0
elif self.inputs[inputId.upper()][0] == 3:
blockLink = self.inputs[inputId.upper()][1]
- return self.target.blocks[blockLink].evaluateBlockValue()
+ return self.target.blocks[blockLink].evaluateBlockValue(eventContainer)
else:
pass
@@ -104,7 +141,7 @@ def getCustomInputValue(self, number, lookIn=(1, 1)):
# Returns dropdown menu value (menus are separate blocks)
def getMenuValue(self, menuId):
- return self.inputs[menuId.upper()][1].fields[menuId.upper()][0]
+ return self.target.blocks[self.inputs[menuId.upper()][1]].fields[menuId.upper()][0]
# Returns field value (menus are separate blocks)
def getFieldValue(self, fieldId, lookIn=0):
diff --git a/config.py b/config.py
index 12dd604..6dbab2c 100644
--- a/config.py
+++ b/config.py
@@ -24,7 +24,7 @@
# Project file name
# If in test mode, set the Scratch project file to load.
-projectFileName: str = "https://scratch.mit.edu/projects/729175960/"
+projectFileName: str = "projects/PointTowardsSprite.sb3"
# Download cache size
# Number of recent downloaded projects stored. 0 means infinity.
@@ -47,9 +47,11 @@
# pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z)
# Hello from the pygame community. https://www.pygame.org/contribute.html"
# message.
-
pygameWelcomeMessage: bool = True
+# Disable print function
+disablePrint: bool = False
+
# Enable Scratch Addons debugger logs
# This allows projects using Scratch Addons to print messages to the console. Vanilla Scratch doesn't support it.
showSALogs: bool = True
@@ -68,7 +70,7 @@
# Screen width/height
# Stage size. You can change that, but most projects won't work with it.
# A scaling mode will be added later.
-# Vanilla is 480x360.
+# Vanilla is 480x360. Try 640x360 for 16/9 widescreen.
screenWidth: int = 480
screenHeight: int = 360
diff --git a/costume.py b/costume.py
index 608f8b8..9d0ccbd 100644
--- a/costume.py
+++ b/costume.py
@@ -13,6 +13,7 @@ def __init__(self):
self.dataFormat = "svg"
self.rotationCenterX = 0
self.rotationCenterY = 0
+ self.offset = None
self.bitmapResolution = 1
self.file = None
self.name = "" # display name
diff --git a/eventContainer.py b/eventContainer.py
new file mode 100644
index 0000000..3e1d957
--- /dev/null
+++ b/eventContainer.py
@@ -0,0 +1,5 @@
+class EventContainer:
+ def __init__(self):
+ self.keys = set()
+ self.keyEvents = set()
+ self.otherEvents = set()
diff --git a/fonts/Grand9K-Pixel.ttf b/fonts/Grand9K-Pixel.ttf
new file mode 100644
index 0000000..cf6fdf4
Binary files /dev/null and b/fonts/Grand9K-Pixel.ttf differ
diff --git a/fonts/Griffy-Regular.ttf b/fonts/Griffy-Regular.ttf
new file mode 100644
index 0000000..22eea37
Binary files /dev/null and b/fonts/Griffy-Regular.ttf differ
diff --git a/fonts/Knewave.ttf b/fonts/Knewave.ttf
new file mode 100644
index 0000000..7d1545b
Binary files /dev/null and b/fonts/Knewave.ttf differ
diff --git a/fonts/LICENSE_CC_BY-SA_3.0.txt b/fonts/LICENSE_CC_BY-SA_3.0.txt
new file mode 100644
index 0000000..48ae233
--- /dev/null
+++ b/fonts/LICENSE_CC_BY-SA_3.0.txt
@@ -0,0 +1,5 @@
+Applies to: Grand9K-Pixel.ttf
+
+This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this
+license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, PO Box 1866,
+Mountain View, CA 94042, USA.
diff --git a/fonts/LICENSE_SIL_OFL.txt b/fonts/LICENSE_SIL_OFL.txt
new file mode 100644
index 0000000..2fd9b6e
--- /dev/null
+++ b/fonts/LICENSE_SIL_OFL.txt
@@ -0,0 +1,87 @@
+Applies to: Griffy-Regular.ttf, handlee-regular.ttf, Knewave.ttf, NotoSans-Medium.ttf, SourceSansPro-Regular.ttf, SourceSerifPro-Regular.ttf
+
+SIL OPEN FONT LICENSE
+Version 1.1 - 26 February 2007
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/fonts/NotoSans-Medium.ttf b/fonts/NotoSans-Medium.ttf
new file mode 100644
index 0000000..25050f7
Binary files /dev/null and b/fonts/NotoSans-Medium.ttf differ
diff --git a/fonts/SourceSansPro-Regular.ttf b/fonts/SourceSansPro-Regular.ttf
new file mode 100644
index 0000000..98e8579
Binary files /dev/null and b/fonts/SourceSansPro-Regular.ttf differ
diff --git a/fonts/SourceSerifPro-Regular.otf b/fonts/SourceSerifPro-Regular.otf
new file mode 100644
index 0000000..bcad74a
Binary files /dev/null and b/fonts/SourceSerifPro-Regular.otf differ
diff --git a/fonts/handlee-regular.ttf b/fonts/handlee-regular.ttf
new file mode 100644
index 0000000..02a94e1
Binary files /dev/null and b/fonts/handlee-regular.ttf differ
diff --git a/gui.py b/gui.py
new file mode 100644
index 0000000..c4286b0
--- /dev/null
+++ b/gui.py
@@ -0,0 +1,51 @@
+import gi
+import os
+import sys
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+
+def pause(button):
+ print("PAUSE", file=p.stdin)
+ print("PAUSE")
+
+
+def stop(button):
+ print("STOP", file=p.stdin)
+ print("STOP")
+
+
+handlers = {
+ "onDestroy": Gtk.main_quit,
+ "pause": pause,
+ "stop": stop,
+}
+
+builder = Gtk.Builder()
+builder.add_from_file("main.glade")
+builder.connect_signals(handlers)
+
+window = builder.get_object("test")
+window.show_all()
+
+stdin = sys.stdin.fileno() # usually 0
+stdout = sys.stdout.fileno() # usually 1
+
+parentStdin, childStdout = os.pipe()
+childStdin, parentStdout = os.pipe()
+pid = os.fork()
+if pid:
+ # parent process
+ os.close(childStdout)
+ os.close(childStdin)
+ os.dup2(parentStdin, stdin)
+ os.dup2(parentStdout, stdout)
+ Gtk.main()
+else:
+ # child process
+ os.close(parentStdin)
+ os.close(parentStdout)
+ os.dup2(childStdin, stdin)
+ os.dup2(childStdout, stdout)
+ os.execl("python", "main.py", "main.py")
diff --git a/lang/en.yml b/lang/en.yml
index 4cc700d..a165c8c 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -48,4 +48,5 @@ en:
zero-division-error: "Project was trying to divide by 0"
costumes-count: "%{sprite} has %{costumes} costumes"
stage-not-found: "There is no stage sprite! Invalid project."
- no-any-key: "Sorry, there is no support for the \"any\" key option yet. We're working on other things and it will be added when the code for the main key block is done."
\ No newline at end of file
+ new-sprite-rotation: "Sprite %{name} rotated to %{rot} degrees"
+ new-sprite-size: "Sprite %{name} resized to %{size}%"
\ No newline at end of file
diff --git a/lang/pl.yml b/lang/pl.yml
new file mode 100644
index 0000000..3a244ec
--- /dev/null
+++ b/lang/pl.yml
@@ -0,0 +1,52 @@
+pl:
+ start: "Scratch2Python %{version}, %{os}"
+ unrecognized-os: "Niestety Scratch2Python nie rozpoznaje twojego systemu operacyjnego. Twój platform string to: %{platform}. Jeżeli wystąpi jakiś błąd, proszę go zgłosić tutaj: %{url}"
+ filename-prompt: "Nazwa pliku projektu:"
+ sb3-desc: "Projekty Scratch 3"
+ all-files-desc: "Wszystkie pliki (*.*)"
+ choose-project-title: "Wybierz projekt do załadowania"
+ invalid-setting-error: "Nieznane ustawienie %{setting}"
+ paused-message: "Zapausowane (naciśnij przycisk %{keybind}, aby odpauzować)"
+ window-title: "%{projectName} - %{s2pVersionString}"
+ extracting-project: "Wypakowywanie projektu"
+ screen-width-prompt: "Szerokość:"
+ screen-height-prompt: "Wysokość:"
+ ok: "OK"
+ cancel: "Anuluj"
+ project-started: "Zaczęto projekt"
+ player-closed: "Zamknięto odtwarzacz"
+ nothing-to-see-here: "Nie ma co tu zobaczyć"
+ help-title: "Pomoc"
+ extract-title: "Wypakuj projekt"
+ project-info-title: "Informacje projektu"
+ fps-title: "FPS (klatki na sekundę)"
+ screen-title: "Rozdzielczość ekranu"
+ fps-prompt: "Wprowadź ilość klatek na sekundę (FPS)"
+ fps-message: "Ustawiono FPS na:"
+ extract-prompt: "Wypakować wszystko z projektu?"
+ screen-message: "Ustawiono rozdzielczość ekranu na:"
+ redraw-message: "Ekran przerysowany"
+ config-warning-screen-too-small: "Ta rozdzielczość jest bardzo mała. Zalecana rozdzielczość minimalna to %{res}. Jeżeli chcesz zignorować ten błąd, włącz zmienną INSANE w config.py."
+ config-warning-screen-too-large: "Ta rozdzielczość jest bardzo duża. Zalecana rozdzielczość maksymalna to %{res}. Jeżeli chcesz zignorować ten błąd, włącz zmienną INSANE w config.py."
+ project-file-not-found: "Nie znaleziono pliku projektu."
+ loading-project: "Ładowanie projektu"
+ debug-prefix: "DEBUG:"
+ block-waiting: "Czekanie %{time} ms"
+ keypress-handling: "Naciśnięto %{keyName}"
+ key-any: "każdy"
+ key-left: "strzałka w lewo"
+ key-right: "strzałka w prawo"
+ key-up: "strzałka w górę"
+ key-down: "strzałka w dół"
+ key-space: "spacja"
+ project-log: "PROJEKT:"
+ project-warn: "OSTRZEŻENIE PROJEKTU:"
+ project-error: "BŁĄD PROJEKTU:"
+ unknown-opcode: "Nieznany kod operacji:"
+ new-sprite-position: "Nowa pozycja (%{x}, %{y}) dla duszka %{name}"
+ stage: "Tło"
+ zero-division-error: "Projekt próbował dzielić przez 0"
+ costumes-count: "Duszek %{sprite} ma %{costumes} kostiumów"
+ stage-not-found: "Nie ma tła! Nieprawidłowy projekt."
+ new-sprite-rotation: "Duszek %{name} obrócony %{rot} stopni"
+ new-sprite-size: "Nowy rozmiar duszka %{name}: %{size}%"
diff --git a/lang/ro.yml b/lang/ro.yml
index dd3995c..3bc73de 100644
--- a/lang/ro.yml
+++ b/lang/ro.yml
@@ -24,7 +24,7 @@ ro:
fps-prompt: "Introdu numărul de cadre pe secundă"
fps-message: "Rata de cadre pe secundă a fost setată la:"
extract-prompt: "Extrage toate resursele proiectului?"
- screen-message: "Rezoulția ecranului a fost setată la:"
+ screen-message: "Rezoluția ecranului a fost setată la:"
redraw-message: "Ecran redesenat"
config-warning-screen-too-small: "Această rezoluție este foarte mică. Rezoluția minimă recomandată este cel puțin %{res}. Dacă vrei să nu apară acest mesaj, setează variabila INSANE în config.py pe True."
config-warning-screen-too-large: "Această rezoluție este foarte mare. Rezoluția maximă recomandată este cel mult %{res}. Dacă vrei să nu apară acest mesaj, setează variabila INSANE în config.py pe True."
@@ -48,4 +48,5 @@ ro:
zero-division-error: "Proiectul a încercat să împartă la 0"
costumes-count: "%{sprite} are %{costumes} costume"
stage-not-found: "Nu există scena! Proiect invalid."
- no-any-key: "Scuze, nu există suport pentru opțiunea de taste „oricare” încă. Lucrăm la altceva și va fi adăugat când blocul principal e gata."
\ No newline at end of file
+ new-sprite-rotation: "Personajul %{name} a fost rotit la %{rot} grade"
+ new-sprite-size: "Personajul %{name} a fost redimensionat la %{size}%"
\ No newline at end of file
diff --git a/main.glade b/main.glade
new file mode 100644
index 0000000..c2e12c8
--- /dev/null
+++ b/main.glade
@@ -0,0 +1,1648 @@
+
+
+
+
+
+ True
+ False
+ dialog-information
+
+
+ 576
+ 320
+ False
+ 8
+ Scratch2Python
+ False
+ scratch
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Scratch2Python project loader
+ 0
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ version {VERSION}
+ 0
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ center
+ center
+ 8
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-executable
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Load local project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from your computer
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-partial-download
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Cache project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from scratch.mit.edu
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ preferences-system
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Settings
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ Need a GUI for browsing projects on scratch.mit.edu?
+ True
+ True
+ True
+ none
+ https://example.org
+
+
+ False
+ True
+ 0
+
+
+
+
+ About
+ True
+ True
+ True
+ aboutIcon
+ True
+
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ True
+ False
+ dialog-information
+
+
+ 64
+ 2
+ 10
+
+
+ 4096
+ 300
+ 4
+ 10
+
+
+ 3000
+ 250
+ 25
+ 10
+
+
+ 240
+ 15
+ 10
+
+
+
+ False
+ bottom
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Cache and load project
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ projectIDValue
+ Project ID or URL
+
+
+ False
+ True
+ 1
+
+
+
+
+ Load
+ True
+ True
+ True
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ 16
+ 2048
+ 360
+ 8
+ 10
+
+
+ 16
+ 2048
+ 480
+ 8
+ 10
+
+
+ False
+ Scratch2Python Settings
+ 540
+ 360
+
+
+ True
+ True
+ left
+ True
+ True
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Download cache size
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Number of stored recently downloaded projects before they start getting deleted. 0 means infinity.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ cacheSize
+ True
+ 4
+
+
+ 1
+ 0
+
+
+
+
+ Clear download cache
+ True
+ True
+ True
+
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ Clear cache
+ 0
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ folder-download
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Download cache
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Maximum framerate
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ The maximum framerate for projects. Sometimes changing it can improve things, but most projects will feel too fast or too slow.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ 0
+ maxFPS
+ True
+ 30.000000000223519
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Turbo mode
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Makes screen refreshes as fast as possible.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 1
+
+
+
+
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Screen resolution
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Change the stage size. Most projects won't adapt properly. Try 640x360 for a widescreen version of the default stage.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 2
+
+
+
+
+
+ True
+ False
+
+
+ True
+ True
+ screenWidth
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ W
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ H
+
+
+ 0
+ 1
+
+
+
+
+ True
+ True
+ 480
+ screenHeight
+ 360
+
+
+ 1
+ 1
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Allow off-screen sprites
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Allow sprites to bypass sprite fencing and completely leave the stage by normal means. Projects may break.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 3
+
+
+
+
+ True
+ True
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ Reset all project defaults
+ 0
+
+
+
+
+
+ 0
+ 5
+
+
+
+
+ Reset
+ True
+ True
+ True
+
+
+
+
+ 1
+ 5
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Clone limit
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Maximum number of clones allowed at one time. 0 means unlimited.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 4
+
+
+
+
+ True
+ True
+ cloneLimit
+
+
+ 1
+ 4
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ computer
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Project defaults
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 1
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+
+
+
+
+ 2
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ preferences-desktop-locale
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Languages
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 2
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Extract project on run
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ If enabled, Scratch2Python will automatically extract the project that is being run into the "assets" directory inside the Scratch2Python install location.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ Allow debug messages in terminal
+ 0
+
+
+
+
+
+ 0
+ 2
+
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ Allow Scratch Addons logs in terminal
+ 0
+
+
+
+
+
+ 0
+ 3
+
+
+
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ Allow terminal output
+ 0
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+ 1
+ 1
+
+
+
+
+
+ 1
+ 4
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Allow pygame welcome message
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Allow pygame to display
+"pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z)
+Hello from the pygame community. https://www.pygame.org/contribute.html"
+on startup.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 4
+
+
+
+
+
+
+
+
+ 3
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ utilities-system-monitor
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Debug
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 3
+ False
+
+
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Key delay
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Milliseconds before keys will start repeating, 0 means keys won't repeat.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ 4
+ keyDelay
+ True
+ 4
+
+
+ 1
+ 0
+
+
+
+
+ 4
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ preferences-desktop-accessibility
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Accesibility
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 4
+ False
+
+
+
+
+
+
+ False
+
+
+ True
+ False
+ 8
+ 8
+ 8
+ 8
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Test controls
+ 0
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ center
+ center
+ 8
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ media-playback-pause
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Pause/resume project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ media-playback-stop
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Quit player
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ Need a GUI for browsing projects on scratch.mit.edu?
+ True
+ True
+ True
+ none
+ https://example.org
+
+
+ False
+ True
+ 0
+
+
+
+
+ About
+ True
+ True
+ True
+ aboutIcon1
+ True
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
diff --git a/main.glade~ b/main.glade~
new file mode 100644
index 0000000..c2e12c8
--- /dev/null
+++ b/main.glade~
@@ -0,0 +1,1648 @@
+
+
+
+
+
+ True
+ False
+ dialog-information
+
+
+ 576
+ 320
+ False
+ 8
+ Scratch2Python
+ False
+ scratch
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Scratch2Python project loader
+ 0
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ version {VERSION}
+ 0
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ center
+ center
+ 8
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-executable
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Load local project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from your computer
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ application-x-partial-download
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Cache project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ from scratch.mit.edu
+
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ preferences-system
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Settings
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ Need a GUI for browsing projects on scratch.mit.edu?
+ True
+ True
+ True
+ none
+ https://example.org
+
+
+ False
+ True
+ 0
+
+
+
+
+ About
+ True
+ True
+ True
+ aboutIcon
+ True
+
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ True
+ False
+ dialog-information
+
+
+ 64
+ 2
+ 10
+
+
+ 4096
+ 300
+ 4
+ 10
+
+
+ 3000
+ 250
+ 25
+ 10
+
+
+ 240
+ 15
+ 10
+
+
+
+ False
+ bottom
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Cache and load project
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ True
+ projectIDValue
+ Project ID or URL
+
+
+ False
+ True
+ 1
+
+
+
+
+ Load
+ True
+ True
+ True
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ 16
+ 2048
+ 360
+ 8
+ 10
+
+
+ 16
+ 2048
+ 480
+ 8
+ 10
+
+
+ False
+ Scratch2Python Settings
+ 540
+ 360
+
+
+ True
+ True
+ left
+ True
+ True
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Download cache size
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Number of stored recently downloaded projects before they start getting deleted. 0 means infinity.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ cacheSize
+ True
+ 4
+
+
+ 1
+ 0
+
+
+
+
+ Clear download cache
+ True
+ True
+ True
+
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ Clear cache
+ 0
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ folder-download
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Download cache
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Maximum framerate
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ The maximum framerate for projects. Sometimes changing it can improve things, but most projects will feel too fast or too slow.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ 0
+ maxFPS
+ True
+ 30.000000000223519
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Turbo mode
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Makes screen refreshes as fast as possible.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 1
+
+
+
+
+ True
+ True
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Screen resolution
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Change the stage size. Most projects won't adapt properly. Try 640x360 for a widescreen version of the default stage.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 2
+
+
+
+
+
+ True
+ False
+
+
+ True
+ True
+ screenWidth
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ W
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ H
+
+
+ 0
+ 1
+
+
+
+
+ True
+ True
+ 480
+ screenHeight
+ 360
+
+
+ 1
+ 1
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Allow off-screen sprites
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Allow sprites to bypass sprite fencing and completely leave the stage by normal means. Projects may break.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 3
+
+
+
+
+ True
+ True
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ Reset all project defaults
+ 0
+
+
+
+
+
+ 0
+ 5
+
+
+
+
+ Reset
+ True
+ True
+ True
+
+
+
+
+ 1
+ 5
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Clone limit
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Maximum number of clones allowed at one time. 0 means unlimited.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 4
+
+
+
+
+ True
+ True
+ cloneLimit
+
+
+ 1
+ 4
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ computer
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Project defaults
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 1
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+
+
+
+
+ 2
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ preferences-desktop-locale
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Languages
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 2
+ False
+
+
+
+
+ True
+ True
+ 8
+ 8
+ 8
+ 8
+ in
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Extract project on run
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ If enabled, Scratch2Python will automatically extract the project that is being run into the "assets" directory inside the Scratch2Python install location.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ Allow debug messages in terminal
+ 0
+
+
+
+
+
+ 0
+ 2
+
+
+
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ Allow Scratch Addons logs in terminal
+ 0
+
+
+
+
+
+ 0
+ 3
+
+
+
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ Allow terminal output
+ 0
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+
+ 1
+ 1
+
+
+
+
+
+ 1
+ 4
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Allow pygame welcome message
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Allow pygame to display
+"pygame X.Y.Z (SDL X.Y.Z, Python 3.Y.Z)
+Hello from the pygame community. https://www.pygame.org/contribute.html"
+on startup.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 4
+
+
+
+
+
+
+
+
+ 3
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ utilities-system-monitor
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Debug
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 3
+ False
+
+
+
+
+
+ True
+ False
+ 8
+ 4
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Key delay
+ 0
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Milliseconds before keys will start repeating, 0 means keys won't repeat.
+ True
+ 0
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ True
+ 4
+ keyDelay
+ True
+ 4
+
+
+ 1
+ 0
+
+
+
+
+ 4
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ preferences-desktop-accessibility
+ 3
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Accesibility
+ 1
+
+
+ False
+ True
+ 1
+
+
+
+
+ 4
+ False
+
+
+
+
+
+
+ False
+
+
+ True
+ False
+ 8
+ 8
+ 8
+ 8
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ Test controls
+ 0
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ center
+ center
+ 8
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ media-playback-pause
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Pause/resume project
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ 160
+ 128
+ True
+ True
+ True
+ center
+ center
+
+
+
+ True
+ False
+ center
+ center
+ vertical
+
+
+ True
+ False
+ 48
+ media-playback-stop
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Quit player
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+ True
+ False
+
+
+ Need a GUI for browsing projects on scratch.mit.edu?
+ True
+ True
+ True
+ none
+ https://example.org
+
+
+ False
+ True
+ 0
+
+
+
+
+ About
+ True
+ True
+ True
+ aboutIcon1
+ True
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
diff --git a/main.py b/main.py
index c12db62..5ebd641 100644
--- a/main.py
+++ b/main.py
@@ -19,7 +19,7 @@
along with this program. If not, see .
"""
-__version__ = "v0.1.0"
+__version__ = "v0.8.0"
__author__ = "Secret-chest"
import tkinter.simpledialog
@@ -73,6 +73,9 @@
from tkinter.simpledialog import *
from tkinter import filedialog
from targetSprite import TargetSprite
+import eventContainer
+import select
+from scratch import display, setProject, mainWindow
sys.stdout = sys.__stdout__
@@ -82,6 +85,13 @@
if not config.enableDebugMessages:
sys.stderr = open(os.devnull, "w")
+# Disable the print function
+def decorator(func):
+ def disabledPrint(*args, **kwargs):
+ if not config.disablePrint:
+ func(*args, **kwargs)
+ return disabledPrint
+print = decorator(print)
# Define a dialog class for screen resolution
class SizeDialog(tkinter.simpledialog.Dialog):
@@ -117,34 +127,17 @@ def buttonbox(self):
self.bind("", lambda event: self.cancelPressed())
-# Start tkinter for showing some popups, and hide main window
-mainWindow = tk.Tk()
-mainWindow.withdraw()
+# Clean the cache if limit is exceeded
+downloads = sorted(Path("./download/").iterdir(), key=os.path.getmtime)
+downloadsToDelete = downloads[config.cachedDownloads:]
+for f in downloadsToDelete:
+ os.remove(f)
-# Get project file name based on options and arguments
-if len(sys.argv) > 1:
- setProject = sys.argv[1]
-else:
- if config.testMode:
- if not config.projectFileName.endswith(".sb3"):
- if "http" not in config.projectFileName\
- or "https" not in config.projectFileName:
- setProject = downloader.downloadByID(config.projectFileName, "./download")
- else:
- setProject = downloader.downloadByURL(config.projectFileName, "./download")
- else:
- setProject = config.projectFileName
- else:
- fileTypes = [(_("sb3-desc"), ".sb3"), (_("all-files-desc"), ".*")]
- setProject = filedialog.askopenfilename(parent=mainWindow,
- initialdir=os.getcwd(),
- title=_("choose-project-title"),
- filetypes=fileTypes)
# Get project data and create sprites
targets, project = sb3Unpacker.sb3Unpack(setProject)
allSprites = pygame.sprite.Group()
-for t in targets:
+for t in sorted(targets, key=lambda t: t.layerOrder):
sprite = TargetSprite(t)
t.sprite = sprite
allSprites.add(sprite)
@@ -155,8 +148,8 @@ def buttonbox(self):
pygame.mixer.pre_init(22050, -16, 1, 12193)
pygame.init()
-font = pygame.font.SysFont(pygame.font.get_default_font(), 16)
-fontXl = pygame.font.SysFont(pygame.font.get_default_font(), 36)
+font = pygame.font.Font("./fonts/SourceSansPro-Regular.ttf", 16)
+fontXl = pygame.font.Font("./fonts/SourceSansPro-Regular.ttf", 36)
# Create paused message
paused = fontXl.render(_("paused-message", keybind="F6"), True, (0, 0, 0))
@@ -166,15 +159,6 @@ def buttonbox(self):
HEIGHT = config.projectScreenHeight
WIDTH = config.projectScreenWidth
-# Get project name and set icon
-projectName = Path(setProject).stem
-icon = pygame.image.load("icon.svg")
-
-# Create project player and window
-display = pygame.display.set_mode([WIDTH, HEIGHT])
-pygame.display.set_caption(_("window-title", projectName=projectName, s2pVersionString="Scratch2Python " + __version__))
-pygame.display.set_icon(icon)
-
# Extract if requested
if config.extractOnProjectRun:
print(_("extracting-project"))
@@ -224,16 +208,27 @@ def buttonbox(self):
if nextBlock:
# Add the next block to the queue
toExecute.append(nextBlock)
- elif block.opcode.startswith("event_"): # add "when I start as a clone" code later
+ elif block.opcode.startswith("event_"): # add “when I start as a clone” code later
eventHandlers.append(block)
-s = None
+s = None # so we don't mix it up
# Prepare keyboard
pygame.key.set_repeat(config.keyDelay, 1000 // config.projectMaxFPS)
-keyEvents = set()
+keyEventContainer = eventContainer.EventContainer()
# Mainloop
+lastTime = time.time_ns()
while projectRunning:
+ rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
+ if rlist:
+ line = sys.stdin.readline().strip()
+ if line:
+ if line.startswith("STOP"):
+ pygame.quit()
+ elif line.startswith("PAUSE"):
+ isPaused = not isPaused
+
+ keyEventContainer.keyEvents = set()
# Process Pygame events
for event in pygame.event.get():
# Window quit (ALT-F4 / X button / etc.)
@@ -242,33 +237,36 @@ def buttonbox(self):
projectRunning = False
# Debug and utility functions
- keyEvents = set()
+ # TODO why are the events correct here but not in execute???
+ keyEventContainer.keyEvents = set()
if event.type == pygame.KEYDOWN:
- keyEvents.add(event.key)
+ keyEventContainer.keyEvents.add(event.key)
+ # print(event.key, "+" + str((time.time_ns() - lastTime) // 1000000))
+ lastTime = time.time_ns()
keysRaw = pygame.key.get_pressed()
- keys = set(k for k in scratch.KEY_MAPPING.values() if keysRaw[k])
+ keyEventContainer.keys = set(k for k in scratch.KEY_MAPPING.values() if keysRaw[k])
- if pygame.K_F1 in keys: # Help
+ if pygame.K_F1 in keyEventContainer.keys: # Help
showinfo(helpTitle, nothingToSeeHere)
- if pygame.K_F4 in keys: # Project info
+ if pygame.K_F4 in keyEventContainer.keys: # Project info
showinfo(projectInfoTitle, nothingToSeeHere)
- if pygame.K_F3 in keys: # Extract
+ if pygame.K_F3 in keyEventContainer.keys: # Extract
confirm = askokcancel(extractTitle, extractPrompt)
if confirm:
print(extractMessage)
shutil.rmtree("assets")
os.mkdir("assets")
project.extractall("assets")
- if pygame.K_F6 in keys: # Pause
+ if pygame.K_F6 in keyEventContainer.keys: # Pause
isPaused = not isPaused
- if pygame.K_F7 in keys: # Set new FPS
+ if pygame.K_F7 in keyEventContainer.keys: # Set new FPS
# Open dialog
newFPS = askinteger(title=fpsTitle, prompt=fpsPrompt)
if newFPS is not None:
print(fpsMessage, newFPS)
config.projectMaxFPS = newFPS
pygame.key.set_repeat(1000, 1000 // config.projectMaxFPS)
- if pygame.K_F8 in keys: # Set new screen resolution
+ if pygame.K_F8 in keyEventContainer.keys: # Set new screen resolution
try:
# Open special dialog
dialog = SizeDialog(mainWindow, title=screenTitle)
@@ -286,7 +284,7 @@ def buttonbox(self):
print(screenMessage, str(HEIGHT) + "x" + str(WIDTH))
except ValueError:
pass
- if pygame.K_F5 in keys: # Redraw
+ if pygame.K_F5 in keyEventContainer.keys: # Redraw
# Redraw everything and recalculate sprite operations
display = pygame.display.set_mode([config.projectScreenWidth, config.projectScreenHeight])
HEIGHT = config.projectScreenHeight
@@ -297,19 +295,22 @@ def buttonbox(self):
print(redrawMessage)
display.fill((255, 255, 255))
- if toExecute:
- for block in toExecute:
- pass
- # print("Running block", block.blockID, "of type", block.opcode)
if not isPaused:
+ # print("starting new frame", keyEventContainer.keyEvents)
for e in eventHandlers:
- if e.opcode == "event_whenkeypressed" and keys and not e.blockRan:
+ # TODO why does it run so many times???
+ if e.opcode == "event_whenkeypressed" and keyEventContainer.keyEvents and not e.blockRan:
+ # print("running", e.blockID)
+
e.blockRan = True
- nextBlock = scratch.execute(e, e.target.sprite, keys, keyEvents)
+ nextBlock = scratch.execute(e, e.target.sprite, keyEventContainer)
if nextBlock and isinstance(nextBlock, list):
toExecute.extend(nextBlock)
+ # print("next:", (b.blockID for b in nextBlock))
elif nextBlock:
toExecute.append(nextBlock)
+ # print("next:", nextBlock.blockID)
+ # print(keyEventContainer.keyEvents, "in main.py", "(" + str(len(eventHandlers)) + ")")
if e.opcode == "event_whenkeypressed":
# print(s.target.blocks, e.script)
@@ -328,13 +329,12 @@ def buttonbox(self):
block.blockRan = True
nextBlocks.append(block.target.blocks[block.next])
block.executionTime, block.timeDelay = 0, 0
- if not block.blockRan and not block.opcode.startswith("event"):
- nextBlock = scratch.execute(block, block.target.sprite, keys, keyEvents)
+ if not block.blockRan and not block.opcode.startswith("event"): # TODO add broadcast blocks
+ nextBlock = scratch.execute(block, block.target.sprite, keyEventContainer)
if not block.next \
and block.top \
and block.top.opcode.startswith("event") \
and block.top.opcode != "event_whenflagclicked":
- print(block.top.blockRan, block.top.blockID)
waitFinished = False
waitFinishedFor = set()
for b in block.top.script:
@@ -354,7 +354,8 @@ def buttonbox(self):
allSprites.draw(display)
allSprites.update()
else:
- display.blit(paused, (WIDTH // 2 - pausedWidth // 2, WIDTH // 2 - pausedHeight // 2))
+ display.blit(paused, (WIDTH // 2 - pausedWidth // 2, HEIGHT // 2 - pausedHeight // 2))
+
pygame.display.flip()
mainWindow.update()
doScreenRefresh = False
diff --git a/projects/AbsoluteRotation.sb3 b/projects/AbsoluteRotation.sb3
new file mode 100644
index 0000000..733a624
Binary files /dev/null and b/projects/AbsoluteRotation.sb3 differ
diff --git a/projects/Balls.sb3 b/projects/Balls.sb3
new file mode 100644
index 0000000..676e6be
Binary files /dev/null and b/projects/Balls.sb3 differ
diff --git a/projects/ChangeColour.sb3 b/projects/ChangeColour.sb3
new file mode 100644
index 0000000..59b841b
Binary files /dev/null and b/projects/ChangeColour.sb3 differ
diff --git a/projects/ChangeColourWithElse.sb3 b/projects/ChangeColourWithElse.sb3
new file mode 100644
index 0000000..e502930
Binary files /dev/null and b/projects/ChangeColourWithElse.sb3 differ
diff --git a/projects/GtkPauseTest.sb3 b/projects/GtkPauseTest.sb3
new file mode 100644
index 0000000..be76316
Binary files /dev/null and b/projects/GtkPauseTest.sb3 differ
diff --git a/projects/Layering.sb3 b/projects/Layering.sb3
new file mode 100644
index 0000000..15c84e9
Binary files /dev/null and b/projects/Layering.sb3 differ
diff --git a/projects/LeftRight.sb3 b/projects/LeftRight.sb3
new file mode 100644
index 0000000..facb82e
Binary files /dev/null and b/projects/LeftRight.sb3 differ
diff --git a/projects/MouseDown.sb3 b/projects/MouseDown.sb3
new file mode 100644
index 0000000..6ca55f4
Binary files /dev/null and b/projects/MouseDown.sb3 differ
diff --git a/projects/MoveSteps.sb3 b/projects/MoveSteps.sb3
index b520bd6..1f0ee37 100644
Binary files a/projects/MoveSteps.sb3 and b/projects/MoveSteps.sb3 differ
diff --git a/projects/NotKeyPressed.sb3 b/projects/NotKeyPressed.sb3
new file mode 100644
index 0000000..e07a8d9
Binary files /dev/null and b/projects/NotKeyPressed.sb3 differ
diff --git a/projects/PointTowardsMouse.sb3 b/projects/PointTowardsMouse.sb3
new file mode 100644
index 0000000..299db65
Binary files /dev/null and b/projects/PointTowardsMouse.sb3 differ
diff --git a/projects/PointTowardsSprite.sb3 b/projects/PointTowardsSprite.sb3
new file mode 100644
index 0000000..72d7efb
Binary files /dev/null and b/projects/PointTowardsSprite.sb3 differ
diff --git a/projects/Rotation.sb3 b/projects/Rotation.sb3
new file mode 100644
index 0000000..e1467d3
Binary files /dev/null and b/projects/Rotation.sb3 differ
diff --git a/projects/Rotation2-medium.sb3 b/projects/Rotation2-medium.sb3
new file mode 100644
index 0000000..d4b0fcd
Binary files /dev/null and b/projects/Rotation2-medium.sb3 differ
diff --git a/projects/Rotation2-small.sb3 b/projects/Rotation2-small.sb3
new file mode 100644
index 0000000..486abab
Binary files /dev/null and b/projects/Rotation2-small.sb3 differ
diff --git a/projects/Rotation2.sb3 b/projects/Rotation2.sb3
new file mode 100644
index 0000000..9cb388d
Binary files /dev/null and b/projects/Rotation2.sb3 differ
diff --git a/projects/RotoZoom.sb3 b/projects/RotoZoom.sb3
new file mode 100644
index 0000000..4c570ec
Binary files /dev/null and b/projects/RotoZoom.sb3 differ
diff --git a/projects/Time.sb3 b/projects/Time.sb3
new file mode 100644
index 0000000..d57e686
Binary files /dev/null and b/projects/Time.sb3 differ
diff --git a/projects/VariableStorage.sb3 b/projects/VariableStorage.sb3
new file mode 100644
index 0000000..aad905c
Binary files /dev/null and b/projects/VariableStorage.sb3 differ
diff --git a/projects/WaitUntil.sb3 b/projects/WaitUntil.sb3
new file mode 100644
index 0000000..e152547
Binary files /dev/null and b/projects/WaitUntil.sb3 differ
diff --git a/sb3Unpacker.py b/sb3Unpacker.py
index df8f3a6..6657226 100644
--- a/sb3Unpacker.py
+++ b/sb3Unpacker.py
@@ -9,7 +9,7 @@
import zipfile as zf
import json
import config
-import target, costume, sound, block, variable, monitor # , broadcast
+import target, costume, sound, block, monitor # , broadcast
from pathlib import Path
import io
import pygame
@@ -51,8 +51,10 @@ def sb3Unpack(sb3):
t.y = targetObj["y"]
t.direction = targetObj["direction"]
t.size = targetObj["size"]
+ t.rotationStyle = targetObj["rotationStyle"]
t.currentCostume = targetObj["currentCostume"]
t.isStage = targetObj["isStage"]
+ t.layerOrder = targetObj["layerOrder"]
t.name = targetObj["name"]
# Get costumes
@@ -62,6 +64,7 @@ def sb3Unpack(sb3):
c.md5ext = costumeObj["md5ext"]
c.rotationCenterX, c.rotationCenterY = costumeObj["rotationCenterX"], costumeObj["rotationCenterY"]
c.dataFormat = costumeObj["dataFormat"]
+ c.offset = pygame.math.Vector2(c.rotationCenterX, c.rotationCenterY)
c.file = project.read(costumeObj["assetId"] + "." + costumeObj["dataFormat"])
c.name = costumeObj["name"]
if costumeObj["dataFormat"] != "svg":
@@ -82,6 +85,10 @@ def sb3Unpack(sb3):
s.name = soundObj["name"]
t.sounds.append(s)
+ # Get variables
+ for variable in targetObj["variables"]:
+ t.variables[variable] = targetObj["variables"][variable]
+
# Set blocks to their correct values
for blockId, blockObj in targetObj["blocks"].items():
b = block.Block()
diff --git a/scratch.py b/scratch.py
index 21ca7f7..73b0b7a 100644
--- a/scratch.py
+++ b/scratch.py
@@ -14,6 +14,15 @@
import bs4
import time
from datetime import datetime
+import eventContainer
+from pathlib import Path
+import tkinter as tk
+import downloader
+from tkinter import filedialog
+import tkinter.simpledialog
+
+__version__ = "v0.8.0"
+__author__ = "Secret-chest"
i18n.set("locale", config.language)
i18n.set("filename_format", "{locale}.{format}")
@@ -31,9 +40,42 @@ class SpriteNotFoundError(Exception):
sys.stdout = open(os.devnull, "w")
+# Start tkinter for showing some popups, and hide main window
+mainWindow = tk.Tk()
+mainWindow.withdraw()
+
+
+# Get project file name based on options and arguments
+if len(sys.argv) > 1:
+ setProject = sys.argv[1]
+else:
+ if config.testMode:
+ if not config.projectFileName.endswith(".sb3"):
+ if "http" not in config.projectFileName\
+ or "https" not in config.projectFileName:
+ setProject = downloader.downloadByID(config.projectFileName, "./download")
+ else:
+ setProject = downloader.downloadByURL(config.projectFileName, "./download")
+ else:
+ setProject = config.projectFileName
+ else:
+ fileTypes = [(_("sb3-desc"), ".sb3"), (_("all-files-desc"), ".*")]
+ setProject = filedialog.askopenfilename(parent=mainWindow,
+ initialdir=os.getcwd(),
+ title=_("choose-project-title"),
+ filetypes=fileTypes)
+
+
HEIGHT = config.projectScreenHeight
WIDTH = config.projectScreenWidth
+# Create project player and window
+projectName = Path(setProject).stem
+icon = pygame.image.load("icon.svg")
+display = pygame.display.set_mode([WIDTH, HEIGHT])
+pygame.display.set_caption(_("window-title", projectName=projectName, s2pVersionString="Scratch2Python " + __version__))
+pygame.display.set_icon(icon)
+
# Key maps to convert the key option in blocks to Pygame constants
KEY_MAPPING = {
"up arrow": pygame.K_UP,
@@ -151,18 +193,21 @@ def getStage():
# Run the given block object
-def execute(block, s, keys=set(), keyEvents=set()):
+def execute(block, s, events=eventContainer.EventContainer()):
# Get block values
opcode = block.opcode
- id = block.blockID
blockRan = block.blockRan
inputs = block.inputs
fields = block.fields
shadow = block.shadow
nextBlock = None
+ # Get keys
+ keys = events.keys
+ keyEvents = events.keyEvents
+
if opcode == "motion_gotoxy": # go to x: () y: ()
- s.setXy(int(block.getInputValue("x")), int(block.getInputValue("y")))
+ s.setXy(float(block.getInputValue("x", eventContainer=events)), float(block.getInputValue("y", eventContainer=events)))
elif opcode == "motion_goto":
nextBlock = block.getBlockInputValue("to")
@@ -190,31 +235,93 @@ def execute(block, s, keys=set(), keyEvents=set()):
return
elif opcode == "motion_setx": # set x to ()
- s.setXy(int(block.getInputValue("x")), s.y)
+ s.setXy(float(block.getInputValue("x", eventContainer=events)), s.y)
elif opcode == "motion_changexby": # change x by ( )
- s.setXyDelta(int(block.getInputValue("dx")), 0)
+ s.setXyDelta(float(block.getInputValue("dx", eventContainer=events)), 0)
elif opcode == "motion_sety": # set y to ()
- s.setXy(s.x, int(block.getInputValue("y")))
+ s.setXy(s.x, float(block.getInputValue("y", eventContainer=events)))
elif opcode == "motion_changeyby": # change y by ()
- s.setXyDelta(0, int(block.getInputValue("dy")))
+ s.setXyDelta(0, float(block.getInputValue("dy", eventContainer=events)))
+
+ elif opcode == "motion_turnleft": # turn ccw () degrees
+ s.setRotDelta(0 - float(block.getInputValue("degrees", eventContainer=events)))
+
+ elif opcode == "motion_turnright": # turn cw () degrees
+ s.setRotDelta(float(block.getInputValue("degrees", eventContainer=events)))
+
+ elif opcode == "motion_pointindirection": # point in direction ()
+ s.setRot(float(block.getInputValue("direction", eventContainer=events)))
+
+ elif opcode == "looks_changesizeby": # turn cw () degrees
+ s.setSizeDelta(float(block.getInputValue("change", eventContainer=events)))
+
+ elif opcode == "looks_setsizeto": # turn cw () degrees
+ s.setSize(float(block.getInputValue("size", eventContainer=events)))
+
+ elif opcode == "motion_pointindirection": # point in direction ()
+ s.setRot(float(block.getInputValue("direction", eventContainer=events)))
+
+ elif opcode == "motion_pointtowards":
+ nextBlock = block.getBlockInputValue("towards")
+ return s.target.blocks[nextBlock]
+
+ elif opcode == "motion_pointtowards_menu":
+ if block.getFieldValue("towards") == "_mouse_": # go to [mouse pointer v]
+ newX, newY = pygame.mouse.get_pos()
+ newX = newX - WIDTH // 2
+ newY = HEIGHT // 2 - newY
+ s.pointTowards(newX, newY)
+ if s.target.blocks[block.parent].next:
+ return s.target.blocks[s.target.blocks[block.parent].next]
+ return
+
+ elif block.getFieldValue("towards") == "_random_": # go to [random position v]
+ minX = 0 - WIDTH // 2
+ maxX = WIDTH // 2
+ minY = 0 - HEIGHT // 2
+ maxY = HEIGHT // 2
+ newX, newY = (random.randint(minX, maxX), random.randint(minY, maxY))
+ s.setXy(newX, newY)
+ if s.target.blocks[block.parent].next:
+ return s.target.blocks[s.target.blocks[block.parent].next]
+ return
+
+ # TODO: sprite lookup table
+
+ elif opcode == "motion_movesteps": # move () steps
+ offset = pygame.math.Vector2(float(block.getInputValue("steps", eventContainer=events)), 0)
+ offset.rotate_ip(90 + s.direction)
+ s.setXyDelta(-offset.x, -offset.y)
elif opcode == "control_wait": # wait () seconds
block.screenRefresh = True
if not block.waiting:
# Get time delay and convert it to milliseconds
- block.timeDelay = int(round(float(float(block.getInputValue("duration"))) * 1000))
+ block.timeDelay = int(round(float(float(block.getInputValue("duration", eventContainer=events))) * 1000))
block.waiting = True
block.executionTime = 0
print(_("debug-prefix"), _("block-waiting", time=block.timeDelay), file=sys.stderr)
return block
+ elif opcode == "control_wait_until": # wait until <>
+ block.screenRefresh = True
+ truth = block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events)
+ if truth:
+ block.blockRan = True
+ nextBlock = s.target.blocks[block.next]
+ return nextBlock
+ else:
+ return block
+
elif opcode == "event_whenflagclicked": # when green flag clicked
pass
elif opcode == "event_whenkeypressed":
+ # print(time.time_ns(), "in whenkeypressed")
+
# print("Handling key event")
# if not block.waiting:
# # Get time delay and convert it to milliseconds
@@ -226,25 +333,8 @@ def execute(block, s, keys=set(), keyEvents=set()):
# print(key)
if key == "any": # when key [any v] pressed
- # TODO any key
-
- pass
-
- elif KEY_MAPPING[key] in keys and block.next: # when key [. . . v] pressed
- if KEY_MAPPING[key] in keys:
- if key == "left arrow":
- keyName = _("key-left")
- elif key == "right arrow":
- keyName = _("key-right")
- elif key == "up arrow":
- keyName = _("key-up")
- elif key == "down arrow":
- keyName = _("key-down")
- elif key == "space":
- keyName = _("key-space")
- else:
- keyName = key
- print(_("debug-prefix"), _("keypress-handling", keyName=keyName), file=sys.stderr)
+ if keyEvents and keys and block.next:
+ print(_("debug-prefix"), _("keypress-handling", keyName=_("key-any")), file=sys.stderr)
# print(time.time_ns() // 1000000, keyName)
for b in block.script:
s.target.blocks[b].blockRan = False
@@ -265,12 +355,48 @@ def execute(block, s, keys=set(), keyEvents=set()):
if nb:
block.script.add(nb.blockID)
block.script.remove(block.blockID)
- print("script:", block.script)
nb.blockRan = False
nextBlock = s.target.blocks[block.next]
return nextBlock
+ elif KEY_MAPPING[key] in keyEvents and block.next: # when key [. . . v] pressed
+ # print(keyEvents, "received in execute()")
+ if key == "left arrow":
+ keyName = _("key-left")
+ elif key == "right arrow":
+ keyName = _("key-right")
+ elif key == "up arrow":
+ keyName = _("key-up")
+ elif key == "down arrow":
+ keyName = _("key-down")
+ elif key == "space":
+ keyName = _("key-space")
+ else:
+ keyName = key
+ print(_("debug-prefix"), _("keypress-handling", keyName=keyName), file=sys.stderr)
+ # print(time.time_ns() // 1000000, keyName)
+ for b in block.script:
+ s.target.blocks[b].blockRan = False
+ nb = block # s.target.blocks[block.next]
+ # nb.blockRan = False
+ block.script.add(nb.blockID)
+ nb = s.target.blocks[nb.next]
+ while nb.next and nb.next != block.blockID:
+ # Reset block
+ nb.blockRan = False
+ nb.timeDelay = 0
+ nb.executionTime = 0
+
+ block.script.add(nb.blockID)
+ nb = s.target.blocks[nb.next]
+ if not nb.next:
+ nb.next = block.blockID
+ if nb:
+ block.script.add(nb.blockID)
+ block.script.remove(block.blockID)
+ nb.blockRan = False
+ nextBlock = s.target.blocks[block.next]
+ return nextBlock
else:
- # print(f"Unknown event: { key } in { keyEvents }, all keys: { keys }")
pass
block.blockRan = False
@@ -301,7 +427,7 @@ def execute(block, s, keys=set(), keyEvents=set()):
elif opcode == "control_repeat": # repeat (10) {...}
if block.repeatCounter is None:
- block.repeatCounter = int(block.getInputValue("times"))
+ block.repeatCounter = int(block.getInputValue("times", eventContainer=events))
# Don't mark the loop as ran until done, and do a screen refresh
if block.repeatCounter > 0:
block.blockRan = False
@@ -335,6 +461,73 @@ def execute(block, s, keys=set(), keyEvents=set()):
nb.next = block.blockID
return nextBlock
+ elif opcode == "control_if": # if <> then {...}
+ if block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events):
+ # If there are blocks, get them
+ if inputs["SUBSTACK"][1]:
+ # No blocks will be flagged as ran inside a forever loop
+ for b in block.substack:
+ s.target.blocks[b].blockRan = False
+ nextBlock = s.target.blocks[inputs["SUBSTACK"][1]]
+ nb = s.target.blocks[inputs["SUBSTACK"][1]]
+ block.substack.add(nb.blockID)
+ while nb.next and nb.next not in block.substack:
+ nb.blockRan = False
+ nb.waiting = False
+ nb.timeDelay = 0
+ nb.executionTime = 0
+ nb = s.target.blocks[nb.next]
+ block.substack.add(nb.blockID)
+ nb.next = block.next
+ block.blockRan = True
+ return nextBlock
+ block.blockRan = True
+ else:
+ block.blockRan = True
+ return s.target.blocks[block.next]
+
+ elif opcode == "control_if_else": # if <> then {...}
+ if block.target.blocks[inputs["CONDITION"][1]].evaluateBlockValue(events):
+ # If there are blocks, get them
+ if inputs["SUBSTACK"][1]:
+ # No blocks will be flagged as ran inside a forever loop
+ for b in block.substack:
+ s.target.blocks[b].blockRan = False
+ nextBlock = s.target.blocks[inputs["SUBSTACK"][1]]
+ nb = s.target.blocks[inputs["SUBSTACK"][1]]
+ block.substack.add(nb.blockID)
+ while nb.next and nb.next not in block.substack:
+ nb.blockRan = False
+ nb.waiting = False
+ nb.timeDelay = 0
+ nb.executionTime = 0
+ nb = s.target.blocks[nb.next]
+ block.substack.add(nb.blockID)
+ nb.next = block.next
+ block.blockRan = True
+ return nextBlock
+ block.blockRan = True
+ else:
+ # If there are blocks, get them
+ if inputs["SUBSTACK2"][1]:
+ # No blocks will be flagged as ran inside a forever loop
+ for b in block.substack2:
+ s.target.blocks[b].blockRan = False
+ nextBlock = s.target.blocks[inputs["SUBSTACK2"][1]]
+ nb = s.target.blocks[inputs["SUBSTACK2"][1]]
+ block.substack2.add(nb.blockID)
+ while nb.next and nb.next not in block.substack2:
+ nb.blockRan = False
+ nb.waiting = False
+ nb.timeDelay = 0
+ nb.executionTime = 0
+ nb = s.target.blocks[nb.next]
+ block.substack2.add(nb.blockID)
+ nb.next = block.next
+ block.blockRan = True
+ return nextBlock
+ block.blockRan = True
+
elif opcode == "looks_switchcostumeto": # switch costume to [... v]
nextBlock = block.getBlockInputValue("costume")
return s.target.blocks[nextBlock]
@@ -398,9 +591,9 @@ def execute(block, s, keys=set(), keyEvents=set()):
if block.proccode == "log %s": # Scratch Addons log ()
print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-log"), block.getCustomInputValue(0), file=sys.stderr)
elif block.proccode == "warn %s": # Scratch Addons warn ()
- print(_("project-warn"), block.getCustomInputValue(0), file=sys.stderr)
+ print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-warn"), block.getCustomInputValue(0), file=sys.stderr)
elif block.proccode == "error %s": # Scratch Addons error ()
- print(_("project-error"), block.getCustomInputValue(0), file=sys.stderr)
+ print("[", datetime.now().strftime("%H:%M:%S:%f"), "]", _("project-error"), block.getCustomInputValue(0), file=sys.stderr)
else:
print(_("unknown-opcode"), opcode)
diff --git a/sprite.png b/sprite.png
new file mode 100644
index 0000000..498e7bc
Binary files /dev/null and b/sprite.png differ
diff --git a/target.py b/target.py
index e1cefe6..8faf2c3 100644
--- a/target.py
+++ b/target.py
@@ -27,3 +27,14 @@ def __init__(self):
self.rotationStyle = "all around" # all around, left-right or do not rotate
self.sprite = None
self.name = ""
+
+ # Variable functions
+ def getVariableValue(self, varId):
+ return self.variables[varId][1]
+
+ def getVariableName(self, varId):
+ return self.variables[varId][0]
+
+ def setVariableValue(self, varId, newValue):
+ self.variables[varId][1] = newValue
+ return newValue
diff --git a/targetSprite.py b/targetSprite.py
index e32ddb8..d17b39a 100644
--- a/targetSprite.py
+++ b/targetSprite.py
@@ -4,7 +4,7 @@
Targets as pygame sprites
"""
import time
-
+import math
import pygame
import cairosvg
import io
@@ -12,6 +12,8 @@
import config
import sys
import i18n
+import copy
+import random
i18n.set("locale", config.language)
i18n.set("filename_format", "{locale}.{format}")
@@ -20,44 +22,52 @@
sprites = set()
+# Disable the print function
+def decorator(func):
+ def disabledPrint(*args, **kwargs):
+ if not config.disablePrint:
+ func(*args, **kwargs)
+ return disabledPrint
+print = decorator(print)
class TargetSprite(pygame.sprite.Sprite):
def __init__(self, target):
sprites.add(self)
pygame.sprite.Sprite.__init__(self)
- self.padX = 0
- self.padY = 0
self.target = target
self.target.currentCostume = target.currentCostume
# Load costume
if target.costumes[self.target.currentCostume].dataFormat != "svg":
+ self.isBitmap = True
sprite = pygame.image.load(io.BytesIO(target.costumes[target.currentCostume].file))
initialWidth = sprite.get_width()
initialHeight = sprite.get_height()
- sprite = pygame.transform.smoothscale(sprite, (sprite.get_width() // target.costumes[target.currentCostume].bitmapResolution, sprite.get_height() // target.costumes[target.currentCostume].bitmapResolution))
- # self.padX = initialWidth - sprite.get_width()
- # self.padY = initialHeight - sprite.get_height()
+ sprite = pygame.transform.smoothscale(sprite, (initialWidth / 2, initialHeight / 2))
else:
+ self.isBitmap = False
sprite = scratch.loadSvg(target.costumes[target.currentCostume].file)
- sprite = pygame.transform.rotate(sprite, 90 - target.direction)
- self.x = target.x + self.padX // 2
- self.y = target.y - self.padY // 2
+ self.x = target.x
+ self.y = target.y
+ self.direction = target.direction
self.size = target.size
- self.image = sprite
+ self.sprite = sprite
+ self.image = self.sprite.copy()
self.rect = self.image.get_rect()
+ self.spriteRect = self.sprite.get_rect()
self.isStage = target.isStage
+ self.rotationStyle = target.rotationStyle
+ self.imageSize = sprite.get_size()
+ self.flipped = False
+ self.layerOrder = target.layerOrder
if self.target.name == "Stage":
self.name = _("stage")
else:
self.name = self.target.name
- self.setXy(self.x, self.y)
- # # Convert Scratch coordinates into Pygame coordinates
- # self.rect.x = (self.x + scratch.WIDTH // 2 - self.target.costumes[self.target.currentCostume].rotationCenterX)
- # self.rect.y = (scratch.HEIGHT // 2 - self.y - self.target.costumes[self.target.currentCostume].rotationCenterY)
- # pygame.transform.scale(self.image, (int(round(self.rect.width * self.size / 100)), int(round(self.rect.height * self.size / 100))))
- #
- # print(_("costumes-count", sprite=self.name, costumes=len(self.target.costumes)))
+ self.setXy(self.x, self.y)
+ self.setRot(self.direction)
+ self.setSize(self.size)
+ print(self.rect.size)
# Set self position
def setXy(self, x, y):
@@ -84,19 +94,88 @@ def setXy(self, x, y):
y = scratch.HEIGHT / 2 + self.rect.height / 2 - 16
elif y < scratch.HEIGHT / -2 - self.rect.height / 2 + 16:
y = scratch.HEIGHT / -2 - self.rect.height / 2 + 16
- # Set X and Y
- self.x = x + self.padX // 2
- self.y = y - self.padY // 2
- print(_("debug-prefix"), _("new-sprite-position", x=x, y=y, name=self.name), file=sys.stderr)
- self.rect.x = self.x + scratch.WIDTH // 2 - round(self.target.costumes[self.target.currentCostume].rotationCenterX)
- self.rect.y = scratch.HEIGHT // 2 - self.y - round(self.target.costumes[self.target.currentCostume].rotationCenterY)
-
- # Move
+
+ self.x = x
+ self.y = y
+ # print(_("debug-prefix"), _("new-sprite-position", x=x, y=y, name=self.name), file=sys.stderr)
+ #rect = self.sprite.get_rect(topleft=(self.x - self.target.costumes[self.target.currentCostume].rotationCenterX, self.y - self.target.costumes[self.target.currentCostume].rotationCenterY))
+ if not self.isStage:
+ self.image = pygame.transform.smoothscale(self.sprite, (self.size / 100 * self.imageSize[0], self.size / 100 * self.imageSize[1]))
+ else:
+ self.image = self.sprite
+
+ offset = self.target.costumes[self.target.currentCostume].offset.elementwise() * self.size / 100 - pygame.math.Vector2(self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100
+
+ if self.rotationStyle == "all around":
+ self.image = pygame.transform.rotate(self.image, 90 - self.direction)
+ offset.rotate_ip(90 + self.direction)
+ elif self.rotationStyle == "left-right":
+ angle = self.direction % 360
+ print(angle, self.flipped)
+ if angle > 180:
+ self.flipped = True
+ else:
+ self.flipped = False
+ if self.flipped:
+ self.image = pygame.transform.flip(self.image, True, False)
+ offset = self.target.costumes[self.target.currentCostume].offset.elementwise() * self.size / 100 - pygame.math.Vector2(self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100
+ else:
+ offset = pygame.math.Vector2(-self.target.costumes[self.target.currentCostume].offset.x, self.target.costumes[self.target.currentCostume].offset.y).elementwise() * self.size / 100 - pygame.math.Vector2(-self.sprite.get_width() / 2, self.sprite.get_height() / 2).elementwise() * self.size / 100
+ else:
+ self.image = self.image
+
+ relativePosition = pygame.math.Vector2(self.spriteRect.centerx, self.spriteRect.centery)
+ position = pygame.math.Vector2(self.x - self.sprite.get_width() / 2 + scratch.WIDTH / 2, self.y - self.sprite.get_height() / 2 + scratch.HEIGHT / 2)
+
+ if not self.isStage:
+ self.rect = self.image.get_rect(center=position+relativePosition+offset)
+ else:
+ self.rect = self.image.get_rect(center=(scratch.WIDTH / 2, scratch.HEIGHT / 2))
+
+ # Relatively set self position
def setXyDelta(self, dx, dy):
x = self.x + dx
y = self.y + dy
self.setXy(x, y)
+ # Set self rotation
+ def setRot(self, rot):
+ self.direction = rot
+ print(_("debug-prefix"), _("new-sprite-rotation", rot=rot, name=self.name), file=sys.stderr)
+
+ self.setXy(self.x, self.y)
+
+ # Relatively set self rotation (turn)
+ def setRotDelta(self, drot):
+ rot = self.direction + drot
+ self.setRot(rot)
+
+ def pointTowards(self, x, y):
+ if self.y == y:
+ if self.y < y:
+ direction = -90
+ else:
+ direction = 90
+ else:
+ if self.y < y:
+ direction = math.degrees(math.atan(((self.x - x) / (self.y - y))))
+ else:
+ direction = math.degrees(math.atan(((self.x - x) / (self.y - y)))) + 180
+
+ self.setRot(direction)
+
+ # Set self rotation
+ def setSize(self, size):
+ self.size = size
+ print(_("debug-prefix"), _("new-sprite-size", size=size, name=self.name), file=sys.stderr)
+
+ self.setXy(self.x, self.y)
+
+ # Relatively set self rotation (turn)
+ def setSizeDelta(self, dsize):
+ size = self.size + dsize
+ self.setSize(size)
+
# Change costume
def setCostume(self, costumeId):
self.target.currentCostume = costumeId % len(self.target.costumes)
@@ -107,10 +186,9 @@ def setCostume(self, costumeId):
initialWidth = sprite.get_width()
initialHeight = sprite.get_height()
sprite = pygame.transform.smoothscale(sprite, (sprite.get_width() // self.target.costumes[self.target.currentCostume].bitmapResolution, sprite.get_height() // self.target.costumes[self.target.currentCostume].bitmapResolution))
- self.padX = initialWidth - sprite.get_width()
- self.padY = initialHeight - sprite.get_height()
else:
sprite = scratch.loadSvg(self.target.costumes[self.target.currentCostume].file)
self.image = sprite
+ self.imageSize = sprite.get_size()
self.rect = self.image.get_rect()
self.setXy(self.x, self.y)
diff --git a/test-gtk.py b/test-gtk.py
new file mode 100644
index 0000000..d9a1ba5
--- /dev/null
+++ b/test-gtk.py
@@ -0,0 +1,108 @@
+import pygame
+import os
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import GdkX11
+
+class GameWindow(Gtk.Window):
+ def __init__(self):
+ Gtk.Window.__init__(self)
+ vbox = Gtk.VBox(False, 2)
+ vbox.show()
+ self.add(vbox)
+
+ #create the menu
+ file_menu = Gtk.Menu()
+
+ accel_group = Gtk.AccelGroup()
+ self.add_accel_group(accel_group)
+
+ dialog_item = Gtk.MenuItem()
+ dialog_item.set_label("Dialog")
+ dialog_item.show()
+ dialog_item.connect("activate",self.show_dialog)
+ file_menu.append(dialog_item)
+ dialog_item.show()
+
+ quit_item = Gtk.MenuItem()
+ quit_item.set_label("Quit")
+ quit_item.show()
+ quit_item.connect("activate",self.quit)
+ file_menu.append(quit_item)
+ quit_item.show()
+
+ menu_bar = Gtk.MenuBar()
+ vbox.pack_start(menu_bar, False, False, 0)
+ menu_bar.show()
+
+ file_item = Gtk.MenuItem()
+ file_item.set_label("_File")
+ file_item.set_use_underline(True)
+ file_item.show()
+
+ file_item.set_submenu(file_menu)
+ menu_bar.append(file_item)
+
+ #create the drawing area
+ da = Gtk.DrawingArea()
+ da.set_size_request(300,300)
+ da.show()
+ vbox.pack_end(da, False, False, 0)
+ da.connect("realize",self._realized)
+
+ #set up the pygame objects
+ self.image = pygame.image.load("sprite.png")
+ self.background = pygame.image.load("background.png")
+ self.x = 150
+ self.y = 150
+
+ #collect key press events
+ self.connect("key-press-event", self.key_pressed)
+
+ def key_pressed(self, widget, event, data=None):
+ if event.keyval == 65361:
+ self.x -= 5
+ elif event.keyval == 65362:
+ self.y -= 5
+ elif event.keyval == 65363:
+ self.x += 5
+ elif event.keyval == 65364:
+ self.y += 5
+
+ def show_dialog(self, widget, data=None):
+ #prompts.info("A Pygtk Dialog", "See it works easy")
+ title = "PyGame embedded in Gtk Example"
+ dialog = Gtk.Dialog(title, None, Gtk.DialogFlags.MODAL,(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
+ content_area = dialog.get_content_area()
+ label = Gtk.Label("See, it still works")
+ label.show()
+ content_area.add(label)
+ response = dialog.run()
+ dialog.destroy()
+
+ def quit(self, widget, data=None):
+ self.destroy()
+
+ def draw(self):
+ self.screen.blit(self.background,[0,0])
+
+ rect = self.image.get_rect()
+ rect.x = self.x
+ rect.y = self.y
+ self.screen.blit(self.image, rect)
+ pygame.display.flip()
+
+ return True
+
+ def _realized(self, widget, data=None):
+ os.putenv('SDL_WINDOWID', str(widget.get_window().get_xid()))
+ pygame.init()
+ pygame.display.set_mode((300, 300), 0, 0)
+ self.screen = pygame.display.get_surface()
+ GObject.timeout_add(200, self.draw)
+
+if __name__ == "__main__":
+ window = GameWindow()
+ window.connect("destroy",Gtk.main_quit)
+ window.show()
+ Gtk.main()
diff --git a/testPygameKeyEvents.py b/testPygameKeyEvents.py
new file mode 100644
index 0000000..044811b
--- /dev/null
+++ b/testPygameKeyEvents.py
@@ -0,0 +1,18 @@
+import time
+import pygame
+
+pygame.init()
+pygame.key.set_repeat(2000, 1000 // 30)
+
+pygame.display.set_mode((240, 180))
+
+lastTime = time.time_ns()
+
+if __name__ == "__main__":
+ while True:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ exit()
+ if event.type == pygame.KEYDOWN:
+ print(event.key, "+" + str((time.time_ns() - lastTime) // 1000000))
+ lastTime = time.time_ns()
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..d6c00f0
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,63 @@
+import pygame as pg
+from pygame.math import Vector2
+
+
+class Entity(pg.sprite.Sprite):
+
+ def __init__(self, pos):
+ super().__init__()
+ self.image = pg.Surface((122, 70), pg.SRCALPHA)
+ pg.draw.polygon(self.image, pg.Color('dodgerblue1'),
+ ((1, 0), (120, 35), (1, 70)))
+ # A reference to the original image to preserve the quality.
+ self.orig_image = self.image
+ self.rect = self.image.get_rect(center=pos)
+ self.pos = Vector2(pos) # The original center position/pivot point.
+ self.offset = Vector2(50, 0) # We shift the sprite 50 px to the right.
+ self.angle = 0
+
+ def update(self):
+ self.angle += 2
+ self.rotate()
+
+ def rotate(self):
+ """Rotate the image of the sprite around a pivot point."""
+ # Rotate the image.
+ self.image = pg.transform.rotozoom(self.orig_image, -self.angle, 1)
+ # Rotate the offset vector.
+ offset_rotated = self.offset.rotate(self.angle)
+ # Create a new rect with the center of the sprite + the offset.
+ self.rect = self.image.get_rect(center=self.pos+offset_rotated)
+
+
+def main():
+ screen = pg.display.set_mode((640, 480))
+ clock = pg.time.Clock()
+ entity = Entity((320, 240))
+ all_sprites = pg.sprite.Group(entity)
+
+ while True:
+ for event in pg.event.get():
+ if event.type == pg.QUIT:
+ return
+
+ keys = pg.key.get_pressed()
+ if keys[pg.K_d]:
+ entity.pos.x += 5
+ elif keys[pg.K_a]:
+ entity.pos.x -= 5
+
+ all_sprites.update()
+ screen.fill((30, 30, 30))
+ all_sprites.draw(screen)
+ pg.draw.circle(screen, (255, 128, 0), [int(i) for i in entity.pos], 3)
+ pg.draw.rect(screen, (255, 128, 0), entity.rect, 2)
+ pg.draw.line(screen, (100, 200, 255), (0, 240), (640, 240), 1)
+ pg.display.flip()
+ clock.tick(30)
+
+
+if __name__ == '__main__':
+ pg.init()
+ main()
+ pg.quit()
diff --git a/variable.py b/variable.py
deleted file mode 100644
index 3ada38f..0000000
--- a/variable.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""
-Variable class
-
-======= CLASS INFO =======
-The various files with classes are used by s2p_unpacker and the correct data is
-set. Those are then used to build the project in main.py.
-"""
-
-
-class Variable:
- def __init__(self):
- self.id = None
- self.name = ""
- self.value = 0