From 626f4b81d2f3aae6e57e98f020c4c16f2bc9c6b9 Mon Sep 17 00:00:00 2001 From: "Sr.Gambiarra" Date: Tue, 22 Feb 2022 23:16:10 -0300 Subject: [PATCH] first commit --- .github/ISSUE_TEMPLATE/unmaintained.md | 12 + .gitignore | 28 + .travis.yml | 37 + LICENSE | 19 + Makefile | 31 + README.md | 97 + designer/__init__.py | 1 + designer/__main__.py | 17 + designer/app.py | 1807 +++++++++++++++ designer/components/__init__.py | 0 designer/components/buildozer_spec_editor.py | 224 ++ designer/components/designer_content.py | 445 ++++ designer/components/dialogs/__init__.py | 0 designer/components/dialogs/about.py | 14 + designer/components/dialogs/add_file.py | 110 + designer/components/dialogs/help.py | 20 + designer/components/dialogs/new_project.py | 150 ++ designer/components/dialogs/recent.py | 92 + designer/components/edit_contextual_view.py | 96 + designer/components/event_viewer.py | 305 +++ designer/components/kivy_console.py | 904 ++++++++ designer/components/kv_lang_area.py | 828 +++++++ designer/components/playground.py | 1253 +++++++++++ .../components/playground_size_selector.py | 232 ++ designer/components/property_viewer.py | 322 +++ designer/components/run_contextual_view.py | 135 ++ designer/components/start_page.py | 98 + designer/components/statusbar.py | 232 ++ designer/components/toolbox.py | 130 ++ designer/components/ui_creator.py | 127 ++ designer/components/widgets_tree.py | 154 ++ designer/config.ini | 58 + designer/core/__init__.py | 0 designer/core/builder.py | 603 +++++ designer/core/profile_settings.py | 256 +++ designer/core/project_manager.py | 539 +++++ designer/core/project_settings.py | 66 + designer/core/recent_manager.py | 82 + designer/core/settings.py | 115 + designer/core/shortcuts.py | 248 +++ designer/core/undo_manager.py | 171 ++ designer/data/icons/error.png | Bin 0 -> 496 bytes designer/data/icons/info.png | Bin 0 -> 1599 bytes designer/data/icons/loading.gif | Bin 0 -> 2890 bytes designer/data/new_templates/default.spec | 233 ++ .../data/new_templates/images/actionbar.png | Bin 0 -> 11019 bytes .../data/new_templates/images/boxlayout.png | Bin 0 -> 3917 bytes .../images/carousel_actionbar.png | Bin 0 -> 8050 bytes .../data/new_templates/images/floatlayout.png | Bin 0 -> 3664 bytes .../new_templates/images/screenmanager.png | Bin 0 -> 4201 bytes .../images/screenmanager_actionbar.png | Bin 0 -> 7428 bytes .../data/new_templates/images/tabbedpanel.png | Bin 0 -> 5488 bytes .../images/textinput_scrollview.png | Bin 0 -> 3614 bytes .../template_actionbar_carousel_kv | 37 + .../template_actionbar_carousel_py | 84 + .../data/new_templates/template_actionbar_kv | 26 + .../data/new_templates/template_actionbar_py | 38 + .../data/new_templates/template_boxlayout_kv | 5 + .../data/new_templates/template_boxlayout_py | 48 + .../new_templates/template_floatlayout_kv | 5 + .../new_templates/template_floatlayout_py | 49 + .../template_screen_manager_actionbar_kv | 26 + .../template_screen_manager_actionbar_py | 45 + .../new_templates/template_screen_manager_kv | 12 + .../new_templates/template_screen_manager_py | 44 + .../new_templates/template_tabbed_panel_kv | 15 + .../new_templates/template_tabbed_panel_py | 15 + .../template_textinput_scrollview_kv | 6 + .../template_textinput_scrollview_py | 14 + designer/data/profiles/android_buildozer.ini | 9 + designer/data/profiles/desktop.ini | 8 + designer/data/profiles/ios_buildozer.ini | 9 + designer/data/settings/build_profile.json | 54 + .../data/settings/buildozer_settings.json | 8 + .../data/settings/buildozer_spec_android.json | 267 +++ .../data/settings/buildozer_spec_app.json | 173 ++ .../settings/buildozer_spec_buildozer.json | 28 + .../data/settings/buildozer_spec_ios.json | 27 + designer/data/settings/designer_settings.json | 50 + designer/data/settings/hanga_settings.json | 8 + .../settings/proj_settings_proj_prop.json | 8 + .../settings/proj_settings_shell_env.json | 14 + designer/data/settings/shortcuts.json | 172 ++ designer/designer.kv | 1976 +++++++++++++++++ designer/files.py | 1 + designer/help.rst | 269 +++ designer/tools/__init__.py | 0 designer/tools/bug_reporter.py | 202 ++ designer/tools/git_integration.py | 533 +++++ designer/tools/ssh-agent/ssh.bat | 1 + designer/tools/ssh-agent/ssh.sh | 1 + designer/tools/ssh-agent/ssh_cmd.bat | 58 + designer/tools/ssh-agent/ssh_status.txt | 0 designer/tools/tools.py | 226 ++ designer/uix/__init__.py | 0 designer/uix/action_items.py | 184 ++ designer/uix/code_find.py | 57 + designer/uix/code_input.py | 189 ++ designer/uix/completion_bubble.py | 296 +++ designer/uix/confirmation_dialog.py | 56 + designer/uix/contextual.py | 667 ++++++ designer/uix/info_bubble.py | 54 + designer/uix/input_dialog.py | 72 + designer/uix/py_code_input.py | 134 ++ designer/uix/py_console.py | 354 +++ designer/uix/sandbox.py | 53 + designer/uix/settings.py | 710 ++++++ designer/uix/xpopup/README.md | 119 + designer/uix/xpopup/__init__.py | 54 + designer/uix/xpopup/android.txt | 3 + designer/uix/xpopup/demo_app.py | 216 ++ designer/uix/xpopup/file.py | 397 ++++ designer/uix/xpopup/form.py | 498 +++++ designer/uix/xpopup/main.py | 8 + designer/uix/xpopup/notification.py | 377 ++++ designer/uix/xpopup/screenshot.png | Bin 0 -> 10728 bytes designer/uix/xpopup/tools.py | 118 + designer/uix/xpopup/xbase.py | 161 ++ designer/uix/xpopup/xpopup.pot | 138 ++ designer/uix/xpopup/xpopup.py | 122 + designer/uix/xpopup/xpopup_ru.mo | Bin 0 -> 1962 bytes designer/utils/__init__.py | 0 designer/utils/constants.py | 8 + designer/utils/toolbox_widgets.py | 48 + designer/utils/utils.py | 243 ++ docs/Makefile | 192 ++ docs/doc-requirements.txt | 1 + docs/source/buildozer.rst | 44 + docs/source/conf.py | 292 +++ docs/source/contribute.rst | 37 + docs/source/img/kd_bug_reporter.png | Bin 0 -> 99172 bytes docs/source/img/kd_build_profiles.png | Bin 0 -> 43070 bytes docs/source/img/kd_buildozer_editor.png | Bin 0 -> 126186 bytes docs/source/img/kd_interface.png | Bin 0 -> 103510 bytes docs/source/img/kd_new_project.png | Bin 0 -> 39037 bytes docs/source/img/kd_playground_settings.png | Bin 0 -> 24024 bytes docs/source/img/kd_setup_y.png | Bin 0 -> 16375 bytes docs/source/img/logo.png | Bin 0 -> 7435 bytes docs/source/index.rst | 39 + docs/source/installation.rst | 50 + docs/source/quickstart.rst | 180 ++ docs/source/tools.rst | 78 + kivy_designer.png | Bin 0 -> 99850 bytes requirements.txt | 8 + setup.cfg | 3 + setup.py | 61 + tests/__init__.py | 0 tests/buildozer.spec | 193 ++ tests/test_apps.py | 23 + tests/test_buildozer_spec_editor.py | 22 + tests/test_kv_lang_area.py | 60 + tests/test_playground.py | 25 + tests/test_uix.py | 66 + tools/pep8checker/pep8.py | 1956 ++++++++++++++++ tools/pep8checker/pep8base.html | 70 + tools/pep8checker/pep8kivy.py | 111 + tools/pep8checker/pre-commit.githook | 78 + workspace.code-workspace | 7 + 158 files changed, 23794 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/unmaintained.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 designer/__init__.py create mode 100644 designer/__main__.py create mode 100644 designer/app.py create mode 100644 designer/components/__init__.py create mode 100644 designer/components/buildozer_spec_editor.py create mode 100644 designer/components/designer_content.py create mode 100644 designer/components/dialogs/__init__.py create mode 100644 designer/components/dialogs/about.py create mode 100644 designer/components/dialogs/add_file.py create mode 100644 designer/components/dialogs/help.py create mode 100644 designer/components/dialogs/new_project.py create mode 100644 designer/components/dialogs/recent.py create mode 100644 designer/components/edit_contextual_view.py create mode 100644 designer/components/event_viewer.py create mode 100644 designer/components/kivy_console.py create mode 100644 designer/components/kv_lang_area.py create mode 100644 designer/components/playground.py create mode 100644 designer/components/playground_size_selector.py create mode 100644 designer/components/property_viewer.py create mode 100644 designer/components/run_contextual_view.py create mode 100644 designer/components/start_page.py create mode 100644 designer/components/statusbar.py create mode 100644 designer/components/toolbox.py create mode 100644 designer/components/ui_creator.py create mode 100644 designer/components/widgets_tree.py create mode 100644 designer/config.ini create mode 100644 designer/core/__init__.py create mode 100644 designer/core/builder.py create mode 100644 designer/core/profile_settings.py create mode 100644 designer/core/project_manager.py create mode 100644 designer/core/project_settings.py create mode 100644 designer/core/recent_manager.py create mode 100644 designer/core/settings.py create mode 100644 designer/core/shortcuts.py create mode 100644 designer/core/undo_manager.py create mode 100644 designer/data/icons/error.png create mode 100644 designer/data/icons/info.png create mode 100644 designer/data/icons/loading.gif create mode 100644 designer/data/new_templates/default.spec create mode 100644 designer/data/new_templates/images/actionbar.png create mode 100644 designer/data/new_templates/images/boxlayout.png create mode 100644 designer/data/new_templates/images/carousel_actionbar.png create mode 100644 designer/data/new_templates/images/floatlayout.png create mode 100644 designer/data/new_templates/images/screenmanager.png create mode 100644 designer/data/new_templates/images/screenmanager_actionbar.png create mode 100644 designer/data/new_templates/images/tabbedpanel.png create mode 100644 designer/data/new_templates/images/textinput_scrollview.png create mode 100644 designer/data/new_templates/template_actionbar_carousel_kv create mode 100644 designer/data/new_templates/template_actionbar_carousel_py create mode 100644 designer/data/new_templates/template_actionbar_kv create mode 100644 designer/data/new_templates/template_actionbar_py create mode 100644 designer/data/new_templates/template_boxlayout_kv create mode 100644 designer/data/new_templates/template_boxlayout_py create mode 100644 designer/data/new_templates/template_floatlayout_kv create mode 100644 designer/data/new_templates/template_floatlayout_py create mode 100644 designer/data/new_templates/template_screen_manager_actionbar_kv create mode 100644 designer/data/new_templates/template_screen_manager_actionbar_py create mode 100644 designer/data/new_templates/template_screen_manager_kv create mode 100644 designer/data/new_templates/template_screen_manager_py create mode 100644 designer/data/new_templates/template_tabbed_panel_kv create mode 100644 designer/data/new_templates/template_tabbed_panel_py create mode 100644 designer/data/new_templates/template_textinput_scrollview_kv create mode 100644 designer/data/new_templates/template_textinput_scrollview_py create mode 100644 designer/data/profiles/android_buildozer.ini create mode 100644 designer/data/profiles/desktop.ini create mode 100644 designer/data/profiles/ios_buildozer.ini create mode 100644 designer/data/settings/build_profile.json create mode 100644 designer/data/settings/buildozer_settings.json create mode 100644 designer/data/settings/buildozer_spec_android.json create mode 100644 designer/data/settings/buildozer_spec_app.json create mode 100644 designer/data/settings/buildozer_spec_buildozer.json create mode 100644 designer/data/settings/buildozer_spec_ios.json create mode 100644 designer/data/settings/designer_settings.json create mode 100644 designer/data/settings/hanga_settings.json create mode 100644 designer/data/settings/proj_settings_proj_prop.json create mode 100644 designer/data/settings/proj_settings_shell_env.json create mode 100644 designer/data/settings/shortcuts.json create mode 100644 designer/designer.kv create mode 100644 designer/files.py create mode 100644 designer/help.rst create mode 100644 designer/tools/__init__.py create mode 100644 designer/tools/bug_reporter.py create mode 100644 designer/tools/git_integration.py create mode 100644 designer/tools/ssh-agent/ssh.bat create mode 100644 designer/tools/ssh-agent/ssh.sh create mode 100644 designer/tools/ssh-agent/ssh_cmd.bat create mode 100644 designer/tools/ssh-agent/ssh_status.txt create mode 100644 designer/tools/tools.py create mode 100644 designer/uix/__init__.py create mode 100644 designer/uix/action_items.py create mode 100644 designer/uix/code_find.py create mode 100644 designer/uix/code_input.py create mode 100644 designer/uix/completion_bubble.py create mode 100644 designer/uix/confirmation_dialog.py create mode 100644 designer/uix/contextual.py create mode 100644 designer/uix/info_bubble.py create mode 100644 designer/uix/input_dialog.py create mode 100644 designer/uix/py_code_input.py create mode 100644 designer/uix/py_console.py create mode 100644 designer/uix/sandbox.py create mode 100644 designer/uix/settings.py create mode 100644 designer/uix/xpopup/README.md create mode 100644 designer/uix/xpopup/__init__.py create mode 100644 designer/uix/xpopup/android.txt create mode 100644 designer/uix/xpopup/demo_app.py create mode 100644 designer/uix/xpopup/file.py create mode 100644 designer/uix/xpopup/form.py create mode 100644 designer/uix/xpopup/main.py create mode 100644 designer/uix/xpopup/notification.py create mode 100644 designer/uix/xpopup/screenshot.png create mode 100644 designer/uix/xpopup/tools.py create mode 100644 designer/uix/xpopup/xbase.py create mode 100644 designer/uix/xpopup/xpopup.pot create mode 100644 designer/uix/xpopup/xpopup.py create mode 100644 designer/uix/xpopup/xpopup_ru.mo create mode 100644 designer/utils/__init__.py create mode 100644 designer/utils/constants.py create mode 100644 designer/utils/toolbox_widgets.py create mode 100644 designer/utils/utils.py create mode 100644 docs/Makefile create mode 100644 docs/doc-requirements.txt create mode 100644 docs/source/buildozer.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/contribute.rst create mode 100644 docs/source/img/kd_bug_reporter.png create mode 100644 docs/source/img/kd_build_profiles.png create mode 100644 docs/source/img/kd_buildozer_editor.png create mode 100644 docs/source/img/kd_interface.png create mode 100644 docs/source/img/kd_new_project.png create mode 100644 docs/source/img/kd_playground_settings.png create mode 100644 docs/source/img/kd_setup_y.png create mode 100644 docs/source/img/logo.png create mode 100644 docs/source/index.rst create mode 100644 docs/source/installation.rst create mode 100644 docs/source/quickstart.rst create mode 100644 docs/source/tools.rst create mode 100644 kivy_designer.png create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/buildozer.spec create mode 100644 tests/test_apps.py create mode 100644 tests/test_buildozer_spec_editor.py create mode 100644 tests/test_kv_lang_area.py create mode 100644 tests/test_playground.py create mode 100644 tests/test_uix.py create mode 100644 tools/pep8checker/pep8.py create mode 100644 tools/pep8checker/pep8base.html create mode 100644 tools/pep8checker/pep8kivy.py create mode 100644 tools/pep8checker/pre-commit.githook create mode 100644 workspace.code-workspace diff --git a/.github/ISSUE_TEMPLATE/unmaintained.md b/.github/ISSUE_TEMPLATE/unmaintained.md new file mode 100644 index 0000000..8b896c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/unmaintained.md @@ -0,0 +1,12 @@ +--- +name: UNMAINTAINED +about: Create a report to help us improve + +--- + +**warning** + +Please not that this project is currently unmaintained, as stated in the README, it's unlikely that bug report will trigger any quick fix. If you are interested in maintaining the project, please contact the kivy team using another mean (IRC, mail, kivy-dev user group…). + +**Describe the bug** +A clear and concise description of what the bug is. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1811f84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +*.pyo +*.pyc +.*.swp +.coverage +.noseids +*.so +*.py# +*~ +*.swp +*.DS_Store +*.kpf +.designer/ +docs/source/_static +docs/source/_templates +docs/build/ +build/ +.idea + +# PyDev +.project +.pydevproject +.settings/ + +# Virtualenv +venv + +# Emacs +.projectile diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6327ef8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,37 @@ +language: python + +python: + - 2.7 + - 3.5 + +install: + yes | sudo add-apt-repository ppa:zoogie/sdl2-snapshots; + yes | sudo add-apt-repository ppa:gstreamer-developers/ppa; + sudo apt-get update; + sudo apt-get install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev libsdl2-mixer-dev; + sudo apt-get install libgstreamer1.0-dev gstreamer1.0-alsa gstreamer1.0-plugins-base; + sudo apt-get install python-dev libsmpeg-dev libswscale-dev libavformat-dev libavcodec-dev libjpeg-dev libtiff4-dev libX11-dev libmtdev-dev; + sudo apt-get install python-setuptools build-essential libgl1-mesa-dev libgles2-mesa-dev; + sudo apt-get install xvfb pulseaudio; + pip install --upgrade Cython==0.25.2 pillow nose coveralls; + pip install -r requirements.txt; + garden install xpopup; + +before_script: + export DISPLAY=:99.0; + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1280x720x24 -ac +extension GLX; + export PYTHONPATH=$PYTHONPATH:$(pwd); + mkdir -p ~/.config; + +script: + - make style + - make test + +notifications: + webhooks: + urls: + - https://kivy.org:5000/travisevent + on_success: always + on_failure: always + on_start: always + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d5d6b13 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2017 Kivy Team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe62ded --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +PYTHON = python +CHECKSCRIPT = tools/pep8checker/pep8kivy.py +KIVY_DIR = +KIVY_USE_DEFAULTCONFIG = 1 +HOSTPYTHON = $(KIVYIOSROOT)/tmp/Python-$(PYTHON_VERSION)/hostpython +IOSPATH := $(PATH):/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin +NOSETESTS = $(PYTHON) -m nose.core + +hook: + # Install pre-commit git hook to check your changes for styleguide + # consistency. + cp tools/pep8checker/pre-commit.githook .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +style: + $(PYTHON) $(CHECKSCRIPT) . + +stylereport: + $(PYTHON) $(CHECKSCRIPT) -html . + + +test: + -rm -rf kivy/tests/build + $(NOSETESTS) tests + +help: + @echo "Please use \`make ' where is one of" + @echo " hook add Pep-8 checking as a git precommit hook" + @echo " style to check Python code for style hints." + @echo " style-report make html version of style hints" + @echo " testing make unittest (nosetests)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..3900c0c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +Kivy Designer +============= + +**WARNING:** This project is at an unstable alpha stage and is not yet +suitable for general use. There is no current plan for continuing the +development of Kivy Designer. **The repository has been archived, +please contact us if you intend to maintain the project.** + +Kivy Designer is Kivy's tool for designing graphical user interfaces +(GUIs) from Kivy Widgets. You can compose and customize widgets, and +test them. It is completely written in Python using Kivy. + +[![Build Status](https://travis-ci.org/kivy/kivy-designer.svg?branch=master)](https://travis-ci.org/kivy/kivy-designer) + +Prerequisites +------------- + +- [Kivy >= 1.9.1](http://kivy.org/#download) +- The following Python modules (available via pip): + - [watchdog](https://pythonhosted.org/watchdog/) + - [pygments](http://pygments.org/) + - [docutils](http://docutils.sourceforge.net/) + - [jedi](http://jedi.jedidjah.ch/en/latest/) + - [gitpython](http://gitpython.readthedocs.org) + - [six](https://pythonhosted.org/six/) + - [kivy-garden](http://kivy.org/docs/api-kivy.garden.html) +- The XPopup widget from the [Kivy garden](https://github.com/kivy-garden/garden.xpopup) + +Installation +------------ + +To install the prerequisites, enter a console (on Windows use kivy.bat in the kivy folder): + + pip install -U watchdog pygments docutils jedi gitpython six kivy-garden + +or simple run: + + pip install -Ur requirements.txt + +To install the XPopup enter a console (on Windows use kivy.bat in the kivy folder): + + garden install xpopup + +With the prerequisites installed, you can use the designer: + + git clone http://github.com/kivy/kivy-designer/ + +or download it manually from https://github.com/kivy/kivy-designer/archive/master.zip and extract to +`kivy-designer`, and then run: + + cd kivy-designer + python -m designer + +On OS X you might need to use the `kivy` command instead of `python` if you are using our portable package. + +If you're successful, you'll see something like this: + +![ScreenShot](https://raw.github.com/kivy/kivy-designer/master/kivy_designer.png) + +Support +------- + +If you need assistance, you can ask for help on our mailing list: + +* User Group : https://groups.google.com/group/kivy-users +* Email : kivy-users@googlegroups.com + +We also have an IRC channel: + +* Server : irc.freenode.net +* Port : 6667, 6697 (SSL only) +* Channel : #kivy + +Contributing +------------ + +We love pull requests and discussing novel ideas. Check out our +[contribution guide](http://kivy.org/docs/contribute.html) and +feel free to improve Kivy Designer. + +The following mailing list and IRC channel are used exclusively for +discussions about developing the Kivy framework and its sister projects: + +* Dev Group : https://groups.google.com/group/kivy-dev +* Email : kivy-dev@googlegroups.com + +IRC channel: + +* Server : irc.freenode.net +* Port : 6667, 6697 (SSL only) +* Channel : #kivy-dev + +License +------- + +Kivy Designer is released under the terms of the MIT License. Please refer to the +LICENSE file. diff --git a/designer/__init__.py b/designer/__init__.py new file mode 100644 index 0000000..e92dbbb --- /dev/null +++ b/designer/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1dev' diff --git a/designer/__main__.py b/designer/__main__.py new file mode 100644 index 0000000..c3b3ad5 --- /dev/null +++ b/designer/__main__.py @@ -0,0 +1,17 @@ +import os.path + +from app import DesignerApp +from utils.utils import get_fs_encoding +from kivy.resources import resource_add_path + + +def main(): + data = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data') + if isinstance(data, bytes): + data = data.decode(get_fs_encoding()) + resource_add_path(data) + DesignerApp().run() + + +if __name__ == '__main__': + main() diff --git a/designer/app.py b/designer/app.py new file mode 100644 index 0000000..4bb8625 --- /dev/null +++ b/designer/app.py @@ -0,0 +1,1807 @@ +__all__ = ['DesignerApp', ] + +from components.dialogs.new_project import (NEW_PROJECTS, NewProjectDialog) +from components.buildozer_spec_editor import BuildozerSpecEditor +from components.run_contextual_view import ModulesContView +from components.edit_contextual_view import EditContView +from components.designer_content import DesignerContent +from components.playground import PlaygroundDragElement +from components.dialogs.add_file import AddFileDialog +from components.dialogs.recent import RecentDialog +from components.dialogs.about import AboutDialog +from components.dialogs.help import HelpDialog + +from core.project_manager import ProjectManager, ProjectWatcher +from core.profile_settings import ProfileSettings +from core.project_settings import ProjectSettings +from core.recent_manager import RecentManager +from core.settings import DesignerSettings +from core.undo_manager import UndoManager +from core.shortcuts import Shortcuts +from core.builder import Profiler + +from tools.bug_reporter import BugReporterApp +from tools.tools import DesignerTools + +from uix.confirmation_dialog import (ConfirmationDialog, ConfirmationDialogSave) +from uix.action_items import DesignerActionProfileCheck +from uix.xpopup.file import XFileSave, XFileOpen +from uix.input_dialog import InputDialog +from uix.sandbox import DesignerSandbox + +from utils.toolbox_widgets import toolbox_widgets +import io, os, shutil, traceback +from utils import constants +import webbrowser + +from utils.utils import ( + get_config_dir, get_fs_encoding, get_kd_data_dir, + get_kd_dir, ignore_proj_watcher, show_alert, + show_error_console, show_message, update_info, +) +from distutils.dir_util import copy_tree +from tempfile import mkdtemp + +from kivy.app import App +from kivy.uix.popup import Popup +from kivy.uix.widget import Widget +from kivy.uix.carousel import Carousel +from kivy.uix.tabbedpanel import TabbedPanel +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.screenmanager import ScreenManager +from kivy.base import ExceptionHandler, ExceptionManager + +from kivy.clock import Clock +from kivy.config import Config +from kivy.factory import Factory +from kivy.core.window import Window +from kivy.graphics import Color, Line + +from kivy.properties import ( + BooleanProperty, ListProperty, + ObjectProperty, StringProperty, + partial, +) + +class Designer(FloatLayout): + '''Designer is the Main Window class of Kivy Designer + :data:`message` is a :class:`~kivy.properties.StringProperty` + ''' + spec_editor = ObjectProperty(None) + '''Instance of + :class:`~designer.components.buildozer_spec_editor.BuildozerSpecEditor` + ''' + designer_tools = ObjectProperty(None) + '''Instance of :class:`~designer.tools.tools.DesignerTools` + ''' + designer_git = ObjectProperty(None) + '''Instance of :class:`~designer.tools.git_integration.DesignerGit` + ''' + statusbar = ObjectProperty(None) + '''Reference to the + :class:`~designer.components.statusbar.StatusBar` instance. + :data:`statusbar` is a :class:`~kivy.properties.ObjectProperty` + ''' + editcontview = ObjectProperty(None) + '''Reference to the + :class:`~designer.components.edit_contextual_view.EditContView` + instance. :data:`editcontview` is a + :class:`~kivy.properties.ObjectProperty` + ''' + modulescontview = ObjectProperty(None) + '''Reference to the + :class:`~designer.components.run_contextual_view.ModulesContView`. + :data:`modulescontview` is a :class:`~kivy.properties.ObjectProperty` + ''' + actionbar = ObjectProperty(None) + '''Reference to the :class:`~kivy.actionbar.ActionBar` instance. + ActionBar is used as a MenuBar to display bunch of menu items. + :data:`actionbar` is a :class:`~kivy.properties.ObjectProperty` + ''' + undo_manager = ObjectProperty(UndoManager()) + '''Reference to the + :class:`~designer.core.undo_manager.UndoManager` instance. + :data:`undo_manager` is a :class:`~kivy.properties.ObjectProperty` + ''' + project_watcher = ObjectProperty(None) + '''Reference to the :class:`~designer.core.project_manager.ProjectWatcher`. + :data:`project_watcher` is a :class:`~kivy.properties.ObjectProperty` + ''' + project_manager = ObjectProperty(None) + '''Reference to the :class:`~designer.core.project_manager.ProjectManager`. + :data:`project_manager` is a :class:`~kivy.properties.ObjectProperty` + ''' + proj_settings = ObjectProperty(None) + '''Reference of :class:`~designer.core.project_settings.ProjectSettings`. + :data:`proj_settings` is a :class:`~kivy.properties.ObjectProperty` + ''' + _proj_modified_outside = BooleanProperty(False) + '''Specifies whether current project has been changed outside Kivy Designer + :data:`_proj_modified_outside` is a + :class:`~kivy.properties.BooleanProperty` + ''' + ui_creator = ObjectProperty(None) + '''Reference to :class:`~designer.components.ui_creator.UICreator` instance. + :data:`ui_creator` is a :class:`~kivy.properties.ObjectProperty` + ''' + designer_content = ObjectProperty(None) + '''Reference to + :class:`~designer.components.designer_content.DesignerContent` instance. + :data:`designer_content` is a :class:`~kivy.properties.ObjectProperty` + ''' + proj_tree_view = ObjectProperty(None) + '''Reference to Project Tree instance + :data:`proj_tree_view` is a :class:`~kivy.properties.ObjectProperty` + ''' + designer_settings = ObjectProperty(None) + '''Reference of :class:`~designer.core.settings.DesignerSettings`. + :data:`designer_settings` is a :class:`~kivy.properties.ObjectProperty` + ''' + start_page = ObjectProperty(None) + '''Reference of :class:`~designer.start_page.DesignerStartPage`. + :data:`start_page` is a :class:`~kivy.properties.ObjectProperty` + ''' + select_profile_cont_menu = ObjectProperty(None) + '''Reference of + :class:`~designer.uix.action_items.DesignerActionSubMenu`. + :data:`select_profile_cont_menu` is a + :class:`~kivy.properties.ObjectProperty` + ''' + selected_profile = StringProperty('') + '''Selected profile settings path + :class:`~kivy.properties.StringProperty` and defaults to ''. + ''' + code_inputs = ListProperty([]) + '''List with all opened code inputs and kv lang area. + This list can be used to fetch unsaved code inputs or to check code content + ''' + + @property + def save_window_size(self): + '''Save window size on exit. + ''' + if Config.getboolean('kivy', 'desktop'): + getdefault = self.designer_settings.config_parser.getdefault + return bool(int(getdefault('desktop', 'save_window_size', 1))) + return False + + def __init__(self, **kwargs): + super(Designer, self).__init__(**kwargs) + self.project_watcher = ProjectWatcher() + self.project_watcher.bind(on_project_modified=self.project_modified) + self.project_manager = ProjectManager() + self.recent_manager = RecentManager() + self.spec_editor = BuildozerSpecEditor() + self.widget_to_paste = None + + self.designer_settings = DesignerSettings() + self.designer_settings.bind(on_config_change=self._config_change) + self.designer_settings.load_settings() + self.designer_settings.bind(on_close=self.close_popup) + + self.shortcuts = Shortcuts() + self.shortcuts.map_shortcuts(self.designer_settings.config_parser) + self.designer_settings.config_parser.add_callback( + self.on_designer_settings) + self.display_shortcuts() + + self.prof_settings = ProfileSettings() + self.prof_settings.bind(on_close=self.close_popup) + self.prof_settings.bind(on_changed=self.on_profiles_changed) + self.prof_settings.bind( + on_use_this_profile=self._perform_use_this_prof) + self.prof_settings.load_profiles() + + self.designer_content = DesignerContent(size_hint=(1, None)) + self.designer_content = self.designer_content.__self__ + + self.designer_git.bind(on_branch=self.on_git_branch) + self.statusbar.bind(on_info_press=self.on_info_press) + + getdefault = self.designer_settings.config_parser.getdefault + Clock.schedule_interval(self.save_project, + (int(getdefault('global', 'auto_save_time', 5)) * 60), + ) + + self.profiler = Profiler() + self.profiler.designer = self + self.profiler.bind(on_error=self.on_profiler_error) + self.profiler.bind(on_message=self.on_profiler_message) + self.profiler.bind(on_run=self.on_profiler_run) + self.profiler.bind(on_stop=self.on_profiler_stop) + + self.designer_tools = DesignerTools(designer=self) + + Window.bind(on_resize=self._write_window_size) + Window.bind(on_request_close=self.on_request_close) + + # variables used in the project + self.popup = None + self.help_dlg = None + self._new_dialog = None + + self.temp_proj_directories = [] + + def _write_window_size(self, *_): + '''Write updated window size to config + ''' + parser_set = self.designer_settings.config_parser.set + parser_set('internal', 'window_width', Window.size[0]) + parser_set('internal', 'window_height', Window.size[1]) + self.designer_settings.config_parser.write() + + def load_view_settings(self, *args): + '''Load "View" menu saved settings + ''' + getdefault = self.designer_settings.config_parser.getdefault + + proj_tree = getdefault('view', 'actn_chk_proj_tree', True) + if proj_tree == 'False': + self.ids.actn_chk_proj_tree.checkbox.active = False + + props_events = getdefault('view', 'actn_chk_prop_event', True) + if props_events == 'False': + self.ids.actn_chk_prop_event.checkbox.active = False + + wid_tree = getdefault('view', 'actn_chk_widget_tree', True) + if wid_tree == 'False': + self.ids.actn_chk_widget_tree.checkbox.active = False + + status_bar = getdefault('view', 'actn_chk_status_bar', True) + if status_bar == 'False': + self.ids.actn_chk_status_bar.checkbox.active = False + + kv_lang_area = getdefault('view', 'actn_chk_kv_lang_area', True) + if kv_lang_area == 'False': + self.ids.actn_chk_kv_lang_area.checkbox.active = False + + def on_designer_settings(self, section, *args): + '''Callback to designer settings modifications + :param section: modified section name + ''' + if section == 'shortcuts': + # update the shortcuts + self.shortcuts.map_shortcuts(self.designer_settings.config_parser) + self.display_shortcuts() + + def display_shortcuts(self, *args): + '''Reads shortcus and update shortcut hints in KD + ''' + m = self.shortcuts.map + + def get_hint(name): + for short in m: + shortcut = m[short] + # if shortcut key is the searched + if shortcut[1] == name: + mod, key = short.split('+') + key = key.strip() + modifier = eval(mod) + short = '+'.join(modifier) + if short: + short += f'+{key}' + else: + short = key + return short.title() + return '' + + self.ids.actn_btn_new_file.hint = get_hint('new_file') + self.ids.actn_btn_new_project.hint = get_hint('new_project') + self.ids.actn_btn_open_project.hint = get_hint('open_project') + self.ids.actn_btn_save.hint = get_hint('save') + self.ids.actn_btn_save_as.hint = get_hint('save_as') + self.ids.actn_btn_close_proj.hint = get_hint('close_project') + self.ids.actn_btn_recent.hint = get_hint('recent') + self.ids.actn_btn_settings.hint = get_hint('settings') + self.ids.actn_btn_quit.hint = get_hint('exit') + self.ids.actn_btn_run_proj.hint = get_hint('run') + self.ids.actn_btn_stop_proj.hint = get_hint('stop') + self.ids.actn_btn_clean_proj.hint = get_hint('clean') + self.ids.actn_btn_build_proj.hint = get_hint('build') + self.ids.actn_btn_rebuild_proj.hint = get_hint('rebuild') + self.ids.actn_btn_buildozer_init.hint = get_hint('buildozer_init') + self.ids.actn_btn_export_png.hint = get_hint('export_png') + self.ids.actn_btn_check_pep8.hint = get_hint('check_pep8') + self.ids.actn_btn_create_setup_py.hint = get_hint('create_setup_py') + self.ids.actn_btn_create_gitignore.hint = get_hint('create_gitignore') + self.ids.actn_btn_help.hint = get_hint('help') + self.ids.actn_btn_wiki.hint = get_hint('kivy_docs') + self.ids.actn_btn_doc.hint = get_hint('kd_docs') + self.ids.actn_btn_page.hint = get_hint('kd_repo') + self.ids.actn_btn_about.hint = get_hint('about') + + def toggle_fullscreen(self, check, **kwargs): + ''' + Event Handler when ActionCheckButton "Fullscreen" is changed. + ''' + if check.checkbox.active is True: + Window.fullscreen = "auto" + else: + Window.fullscreen = False + + def restore_window_size(self, *args): + '''Restore window size from previous application run + ''' + getdefault = self.designer_settings.config_parser.getdefault + width = int(getdefault('internal', 'window_width', 800)) + height = int(getdefault('internal', 'window_height', 600)) + Window.size = max(width, 800), max(height, 600) + + def on_git_branch(self, instance, branch_name, *args): + '''Bind git changes + :param branch_name: name of the selected branch + ''' + update_info('Git', branch_name) + + def on_info_press(self, *args): + '''Callback to git statusbar info press + ''' + # open switch branch if git repo + if self.designer_git.is_repo: + self.designer_git.do_branches() + + def show_help(self, *args): + '''Event handler for 'on_help' event of self.start_page + ''' + if self.popup: + return False + if self.help_dlg is None: + self.help_dlg = HelpDialog() + self.help_dlg.rst.source = os.path.join(get_kd_dir(), 'help.rst') + + self.popup = Popup( + title='Kivy Designer Help', content=self.help_dlg, + size_hint=(0.95, 0.95), auto_dismiss=False, + ) + self.popup.open() + self.help_dlg.bind(on_cancel=self.close_popup) + + def set_escape_exit(self): + getdefault = self.designer_settings.config_parser.getdefault + Config.set( + 'kivy', 'exit_on_escape', + int(getdefault('desktop', 'exit_on_escape', 0)) + ) + + def _config_change(self, *args): + '''Event Handler for 'on_config_change' + event of self.designer_settings. + ''' + Clock.unschedule(self.save_project) + getdefault = self.designer_settings.config_parser.getdefault + + Clock.schedule_interval(self.save_project, + (int(getdefault('global', 'auto_save_time', 5)) * 60), + ) + + max_lines = int(getdefault('global', 'num_max_kivy_console', 200)) + self.ui_creator.kivy_console.cached_history = max_lines + + recent_files = int(getdefault('global', 'num_recent_files', 10)) + self.recent_manager.max_recent_files = recent_files + + if self.save_window_size: + self._write_window_size() + + self.set_escape_exit() + + def on_profiler_error(self, instance, message): + '''Display an alert if get an error + ''' + show_alert('Profile error', message) + + def on_profiler_message(self, instance, message, duration=0): + '''Display a message in the status bar + ''' + show_message(message, duration, 'info') + + def on_profiler_run(self, *args): + '''When a new process starts + ''' + self.ids.actn_btn_stop_proj.disabled = False + + def on_profiler_stop(self, *args): + '''When a process is stopped or finished + ''' + self.ids.actn_btn_stop_proj.disabled = True + + def _add_designer_content(self): + '''Add designer_content to Designer, when a project is loaded + ''' + for _child in self.children[:]: + if _child == self.designer_content: + return None + + self.remove_widget(self.start_page) + self.start_page.parent = None + self.add_widget(self.designer_content, 1) + + self.ids['actn_btn_new_file'].disabled = False + self.ids['actn_btn_save'].disabled = False + self.ids['actn_btn_save_as'].disabled = False + self.ids['actn_btn_close_proj'].disabled = False + self.ids['actn_menu_view'].disabled = False + self.ids['actn_menu_proj'].disabled = False + self.ids['actn_menu_run'].disabled = False + self.ids['actn_menu_tools'].disabled = False + + self.proj_settings = ProjectSettings( + project=self.project_manager.current_project) + self.proj_settings.load_proj_settings() + + Clock.schedule_once(self.load_view_settings, 0) + + def on_statusbar_height(self, *args): + '''Callback for statusbar.height + ''' + self.designer_content.y = self.statusbar.height + self.on_height(*args) + + def on_actionbar_height(self, *args): + '''Callback for actionbar.height + ''' + self.on_height(*args) + + def on_height(self, *args): + '''Callback for self.height + ''' + if self.actionbar and self.statusbar: + h = (self.height-self.actionbar.height-self.statusbar.height) + self.designer_content.height = h + + self.designer_content.y = self.statusbar.height + + def project_modified(self, *args): + '''Event Handler called when Project is modified outside Kivy Designer + ''' + # To dispatch modified event only once for all files/folders + # of proj_dir + if self._proj_modified_outside: + return None + + msg = 'Project modified outside Kivy Designer' + Clock.schedule_once(partial(show_message, msg, 5, 'error')) + if self.popup: + return None + + def close(*args): + self._proj_modified_outside = False + self.close_popup() + + confirm_dlg = ConfirmationDialog( + message="Current Project has been modified\n" + "outside the Kivy Designer.\n" + "Do you want to reload project?") + confirm_dlg.bind(on_ok=self._perform_reload, on_cancel=close) + + self.popup = Popup( + title='Kivy Designer', content=confirm_dlg, + size_hint=(None, None), size=('200pt', '150pt'), + auto_dismiss=False) + self.popup.open() + + self._proj_modified_outside = True + + @ignore_proj_watcher + def _perform_reload(self, *args): + '''Perform reload of project after it is modified + ''' + # Perform reload of project after it is modified + self.close_popup() + self._perform_open(self.project_manager.current_project.path) + self._proj_modified_outside = False + + # buildozer may have changed, reload it + proj_path = self.project_manager.current_project.path + + def reload_spec_editor(*args): + self.spec_editor.load_settings(proj_path) + if os.path.exists(os.path.join(proj_path, 'buildozer.spec')): + Clock.schedule_once(reload_spec_editor, 1) + + def on_show_edit(self, *args): + '''Event Handler of 'on_show_edit' event. This will show EditContView + in ActionBar + ''' + if isinstance(self.actionbar.children[0], EditContView): + return None + + if self.editcontview is None: + select_all_trigger = Clock.create_trigger( + self.action_btn_select_all_pressed) + self.editcontview = EditContView( + on_undo=self.action_btn_undo_pressed, + on_redo=self.action_btn_redo_pressed, + on_cut=self.action_btn_cut_pressed, + on_copy=self.action_btn_copy_pressed, + on_paste=self.action_btn_paste_pressed, + on_delete=self.action_btn_delete_pressed, + on_selectall=select_all_trigger, + on_next_screen=self._next_screen, + on_prev_screen=self._prev_screen, + on_touch_up=self.on_editcontview_release, + on_find=partial(self.designer_content.show_findmenu, True)) + + self.actionbar.add_widget(self.editcontview) + + widget = self.ui_creator.propertyviewer.widget + show = isinstance(widget, (Carousel, ScreenManager, TabbedPanel)) + self.editcontview.show_action_btn_screen(show) + + self._edit_selected = 'Py' + if self.ui_creator.kv_code_input.clicked: + self._edit_selected = 'KV' + elif self.ui_creator.playground.clicked: + self._edit_selected = 'Play' + + show_f = self._edit_selected == 'Py' + self.editcontview.show_find(show_f) + + self.ui_creator.playground.clicked = False + self.ui_creator.kv_code_input.clicked = False + + def on_editcontview_release(self, instance, touch): + if self._edit_selected != 'Py': + return self.editcontview.on_touch_up(touch) + + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + clicked = tab_item.content.code_input.clicked + if hasattr(tab_item.content, 'code_input') and clicked: + Clock.schedule_once(tab_item.content.code_input.do_focus) + return True + + def _prev_screen(self, *args): + '''Event handler for 'on_prev_screen' for self.editcontview + ''' + widget = self.ui_creator.propertyviewer.widget + if isinstance(widget, Carousel): + widget.load_previous() + + elif isinstance(widget, ScreenManager): + widget.current = widget.previous() + + elif isinstance(widget, TabbedPanel): + index = widget.tab_list.index(widget.current_tab) + if len(widget.tab_list) <= (index + 1): + return None + + widget.switch_to(widget.tab_list[index + 1]) + + def _next_screen(self, *args): + '''Event handler for 'on_next_screen' for self.editcontview + ''' + widget = self.ui_creator.propertyviewer.widget + if isinstance(widget, Carousel): + widget.load_next() + + elif isinstance(widget, ScreenManager): + widget.current = widget.next() + + elif isinstance(widget, TabbedPanel): + index = widget.tab_list.index(widget.current_tab) + if index == 0: + return None + + widget.switch_to(widget.tab_list[index - 1]) + + def on_touch_down(self, touch): + '''Override of FloatLayout.on_touch_down. Used to determine where + touch is down and to call self.actionbar.on_previous + ''' + instance = isinstance(self.actionbar.children[0], EditContView) + collided = self.actionbar.collide_point(*touch.pos) + + if instance or not collided: + self.actionbar.on_previous(self) + self.ui_creator.playground.clicked = False + + return super(FloatLayout, self).on_touch_down(touch) + + def action_btn_new_file_pressed(self, *args): + '''Event Handler when ActionButton "New Project" is pressed. + ''' + if self.popup: + return False + + input_dialog = InputDialog("File name:") + input_dialog.bind(on_confirm=self._perform_new_file, on_cancel=self.close_popup) + + self.popup = Popup( + title="Add new File", content=input_dialog, + size_hint=(None, None), size=('200pt', '150pt'), + auto_dismiss=False, + ) + self.popup.open() + return True + + @ignore_proj_watcher + def _perform_new_file(self, instance): + ''' + Create a new file in the project folder + ''' + current_project = self.project_manager.current_project + + file_name = instance.get_user_input() + if file_name.find('.') == -1: + file_name += '.py' + + new_file = os.path.join(current_project.path, file_name) + if os.path.exists(new_file): + instance.lbl_error.text = 'File exists' + return None + + open(new_file, 'a').close() + self.designer_content.update_tree_view(current_project) + self.close_popup() + + def action_btn_new_project_pressed(self, *args): + '''Event Handler when ActionButton "New" is pressed. + ''' + if self.popup: + return False + + if self.project_manager.current_project.saved: + self._show_new_dialog() + return True + + _confirm_dlg_save = ConfirmationDialogSave( + 'Your project is not saved.\nWhat would you like to do?' + ) + def save_and_open(*args): + self.action_btn_save_pressed() + self._show_new_dialog() + _confirm_dlg_save.bind( + on_dont_save=self._show_new_dialog, on_save=save_and_open, + on_cancel=self.close_popup) + + self.popup = Popup( + title='New', content=_confirm_dlg_save, + size_hint=(None, None), size=('300pt', '150pt'), + auto_dismiss=False, + ) + self.popup.open() + return True + + def _show_new_dialog(self, *args): + if self.popup: + return False + + if self._new_dialog is None: + self._new_dialog = NewProjectDialog() + self._new_dialog.bind(on_select=self._perform_new, on_cancel=self.close_popup) + + self.popup = Popup( + title='New Project', content=self._new_dialog, + size_hint=(None, None), size=('650pt', '450pt'), + auto_dismiss=False, + ) + self.popup.open() + + @ignore_proj_watcher + def _perform_new(self, *args): + '''To load new project + ''' + self.close_popup() + + new_proj_dir = mkdtemp(prefix=constants.NEW_PROJECT_DIR_NAME_PREFIX) + self.temp_proj_directories.append(new_proj_dir) + + template = self._new_dialog.template_list.text + app_name = self._new_dialog.app_name.text + package_domain, package_name = self._new_dialog.package_name.text\ + .rsplit('.', 1) + package_version = self._new_dialog.package_version.text + + templates_dir = os.path.join(get_kd_data_dir(), constants.DIR_NEW_TEMPLATE) + + kv_file = NEW_PROJECTS[template][0] + py_file = NEW_PROJECTS[template][1] + shutil.copy(os.path.join(templates_dir, py_file), + os.path.join(new_proj_dir, "main.py")) + + shutil.copy(os.path.join(templates_dir, kv_file), + os.path.join(new_proj_dir, "main.kv")) + + buildozer = io.open(os.path.join(new_proj_dir, 'buildozer.spec'), 'w', encoding='utf-8') + + for line in io.open(os.path.join(templates_dir, 'default.spec'), 'r', encoding='utf-8'): + line = line.replace('$app_name', app_name) + line = line.replace('$package_name', package_name) + line = line.replace('$package_domain', package_domain) + line = line.replace('$package_version', package_version) + buildozer.write(line) + buildozer.close() + + self._perform_open(new_proj_dir, True) + self.project_manager.current_project.new_project = True + self.project_manager.current_project.saved = False + show_message('Project created successfully', 5, 'info') + + def cleanup(self): + '''To cleanup everything loaded by the current project before loading + another project. + ''' + self.ui_creator.cleanup() + self.undo_manager.cleanup() + self.designer_content.toolbox.cleanup() + self.designer_content.tab_pannel.cleanup() + + for node in self.proj_tree_view.root.nodes[:]: + self.proj_tree_view.remove_node(node) + + for widget in toolbox_widgets[:]: + if widget[1] == 'custom': + toolbox_widgets.remove(widget) + self.ui_creator.kv_code_input.text = '' + + def action_btn_open_pressed(self, *args): + '''Event Handler when ActionButton "Open" is pressed. + ''' + if self.popup: + return False + + if self.project_manager.current_project.saved: + self._show_open_dialog() + return True + + _confirm_dlg_save = ConfirmationDialogSave( + 'Your project is not saved.\nWhat would you like to do?' + ) + def save_and_open(*args): + self.close_popup() + self.action_btn_save_pressed() + self._show_open_dialog() + + def show_open(*args): + self.close_popup() + self._show_open_dialog() + + _confirm_dlg_save.bind(on_dont_save=show_open, + on_save=save_and_open, + on_cancel=self.close_popup) + self.popup = Popup( + title='Kivy Designer', auto_dismiss=False, + size_hint=(None, None), size=('300pt', '150pt'), + content=_confirm_dlg_save, + ) + self.popup.open() + return True + + def action_btn_close_proj_pressed(self, *args): + ''' + Event Handler when ActionButton "Close Project" is pressed. + ''' + if self.popup: + return False + + if self.project_manager.current_project.saved: + self._perform_close_project() + return True + + _confirm_dlg_save = ConfirmationDialogSave( + 'Your project is not saved.\nWhat would you like to do?' + ) + def save_and_close(*args): + self.close_popup() + self.action_btn_save_pressed() + self._perform_close_project() + + _confirm_dlg_save.bind(on_cancel=self.close_popup, + on_dont_save=self._perform_close_project, + on_save=save_and_close) + self.popup = Popup( + title='Kivy Designer', auto_dismiss=False, + size_hint=(None, None), size=('300pt', '150pt'), + content=_confirm_dlg_save, + ) + self.popup.open() + return True + + def _perform_close_project(self, *args): + ''' + Close the current project and go to the start page + ''' + self.close_popup() + + self.remove_widget(self.designer_content) + self.designer_content.parent = None + self.add_widget(self.start_page, 1) + + self.ids['actn_btn_new_file'].disabled = True + self.ids['actn_btn_save'].disabled = True + self.ids['actn_btn_save_as'].disabled = True + self.ids['actn_btn_close_proj'].disabled = True + self.ids['actn_menu_view'].disabled = True + self.ids['actn_menu_proj'].disabled = True + self.ids['actn_menu_run'].disabled = True + self.ids['actn_menu_tools'].disabled = True + self.project_manager.close_current_project() + self.project_watcher.stop_watching() + + def _show_open_dialog(self, *args): + '''To show FileBrowser to "Open" a project + ''' + if self.popup: + return False + + def_path = os.path.expanduser('~') + current_project = self.project_manager.current_project + if current_project.path and not current_project.new_project: + def_path = self.project_manager.current_project.path + + def open_file_browser(instance): + if instance.is_canceled(): + return None + self._fbrowser_load(instance) + + XFileOpen(title="Open", on_dismiss=open_file_browser, path=def_path) + + def _fbrowser_load(self, instance): + '''Event Handler for 'on_load' event of self._fbrowser + ''' + if not instance.selection or self.popup: + return None + + file_path = instance.selection[0] + file_name, file_extension = os.path.splitext(instance.selection[0]) + + error = None + try: + if file_extension in ('.py', '.kv') \ + or (file_name.endswith('buildozer') + and file_extension == '.spec'): + self._perform_open(file_path) + else: + error = 'Cannot load file type: .%s, Please load a .py file' % \ + file_extension + except: + error = 'Cannot load empty file type' + + if error: + show_message(error, 5, 'error') + + def _perform_open(self, file_path, new_project=False): + '''To open a project given by file_path + ''' + self.project_watcher.stop_watching() + show_message('Project loaded successfully', 5, 'info') + + self.cleanup() + + if os.path.isfile(file_path): + file_path = os.path.dirname(file_path) + + project = self.project_manager.open_project(file_path) + if project is None: + return None + + self.project_watcher.start_watching(file_path) + self.designer_content.update_tree_view(project) + + if not new_project: + self.recent_manager.add_path(project.path) + + for widget in toolbox_widgets[:]: + if widget[1] == 'custom': + toolbox_widgets.remove(widget) + + self._add_designer_content() + app_widgets = self.project_manager.current_project.app_widgets + + if app_widgets: + for name in app_widgets.keys(): + toolbox_widgets.append((name, 'custom')) + + self.designer_content.toolbox.update_app_widgets() + + if len(app_widgets): + first_wdg = app_widgets[list(app_widgets.keys())[-1]] + self.ui_creator.playground.load_widget(first_wdg.name) + else: + self.ui_creator.playground.no_widget() + + Clock.schedule_once(partial(self.ui_creator.kivy_console.run_command, + 'cd %s' % (file_path) + ), 1) + self.designer_git.load_repo(file_path) + + def close_popup(self, *args): + '''EventHandler for all self.popup when self.popup.content + emits 'on_cancel' or equivalent. + ''' + if self.popup: + if self.popup.content: + # remove the content from the popup + self.popup.content.parent = None + self.popup.dismiss() + self.popup = None + return True + return False + + @ignore_proj_watcher + def save_project(self, *args): + '''Saves the current project. + :param path: path to save the project. + ''' + proj = self.project_manager.current_project + saved = proj.save() + if saved: + show_message('Project saved!', 5, 'info') + else: + show_message('Failed to save the project!', 5, 'error') + + def action_btn_save_pressed(self, exit_on_save=False, *args): + '''Event Handler when ActionButton "Save" is pressed. + :param exit_on_save: if True, closes the KD after saving the project + ''' + proj = self.project_manager.current_project + if proj.new_project: + self.action_btn_save_as_pressed(exit_on_save=exit_on_save) + return + else: + self.save_project() + if exit_on_save: + self._perform_quit() + + def action_btn_save_as_pressed(self, exit_on_save=False, *args): + '''Event Handler when ActionButton "Save As" is pressed. + ''' + if self.popup: + return False + proj = self.project_manager.current_project + + def_path = os.path.expanduser('~') + if not proj.new_project and proj.path: + def_path = proj.path + + def save_project(instance): + if instance.is_canceled(): + return + + self._perform_save_as(instance, exit_on_save=exit_on_save) + + XFileSave(title="Enter Folder Name", size_hint=(0.9, 0.9), + on_dismiss=save_project, path=def_path) + + def _perform_save_as(self, instance, exit_on_save=False): + '''Event handler for 'on_success' event of self._save_as_browser + ''' + + proj_dir = instance.path + os.path.sep + instance.filename + + # save the project in the folder and then copy it to a new folder + self.save_project() + copy_tree(self.project_manager.current_project.path, proj_dir) + if exit_on_save: + self._perform_quit() + return + self._perform_open(proj_dir) + + def action_btn_settings_pressed(self, *args): + '''Event handler for 'on_release' event of + DesignerActionButton "Settings" + ''' + if self.popup: + return False + + self.designer_settings.parent = None + self.popup = Popup(title="Kivy Designer Settings", + content=self.designer_settings, + size_hint=(None, None), + size=(720, 480), auto_dismiss=False) + + self.popup.open() + return True + + def action_btn_select_prof_project_pressed(self, *args): + '''Event handler for "Select Profile" menu + ''' + pass + + def on_profiles_changed(self, *args): + '''Callback when there is a modification in the profile settings + ''' + self.prof_settings.load_profiles() + self.fill_select_profile_menu() + + def _perform_use_this_prof(self, instance, *args): + '''Callback to "Use this Profile" button + ''' + _config = instance.selected_config + _config_path = _config.filename + self.designer_settings.config_parser.set('internal', + 'default_profile', + _config_path) + self.designer_settings.config_parser.write() + + def fill_select_profile_menu(self, *args): + '''Fill self.select_profile_cont_menu with available Build Profiles + ''' + prof_menu = self.select_profile_cont_menu + prof_menu.remove_children() + group = 'profile' + for profile in sorted(self.prof_settings.config_parsers.keys()): + config = self.prof_settings.config_parsers[profile] + config_path = config.filename + if isinstance(config_path, bytes): + config_path = config_path.decode(get_fs_encoding()) + + prof_name = config.getdefault('profile', 'name', 'PROFILE') + if not prof_name.strip(): + prof_name = 'PROFILE' + + btn = DesignerActionProfileCheck(group=group, + allow_no_selection=False) + btn.text = prof_name + btn.checkbox.active = False + + btn.config_key = profile + btn.bind(on_active=self._perform_profile_selected) + + if self.designer_settings.config_parser.getdefault('internal', + 'default_profile', '') == config_path: + btn.checkbox.active = True + self.selected_profile = config_path + self._perform_profile_selected(btn, btn.checkbox, True) + + prof_menu.add_widget(btn) + + prof_menu._add_widget() + + def _perform_profile_selected(self, instance, checkbox, value, *args): + '''Event handler to select profile radio button. + Save the selected config_parser path to the config + ''' + if value: + _config = self.prof_settings.config_parsers[instance.config_key] + _config_path = _config.filename + if isinstance(_config_path, bytes): + _config_path.decode(get_fs_encoding()) + self.designer_settings.config_parser.set('internal', + 'default_profile', + _config_path) + self.designer_settings.config_parser.write() + self.selected_profile = _config_path + + target = _config.getdefault('profile', 'target', '') + + if target == 'Desktop': + self.ids.actn_btn_run_module.disabled = False + else: + self.ids.actn_btn_run_module.disabled = True + + def action_btn_recent_files_pressed(self, *args): + '''Event Handler when ActionButton "Recent Projects" is pressed. + ''' + if self.popup: + return False + _recent_dlg = RecentDialog(self.recent_manager.list_projects) + _recent_dlg.bind(on_cancel=self.close_popup, + on_select=self._recent_file_release) + self.popup = Popup(title='Recent Projects', content=_recent_dlg, + size_hint=(0.5, 0.5), auto_dismiss=False) + self.popup.open() + return True + + def _recent_file_release(self, instance, *args): + '''Event Handler for 'on_select' event of RecentDialog. + ''' + self._perform_open(instance.get_selected_project()) + self.close_popup() + + def remove_temp_proj_directories(self): + '''Before KD closes, delete temp new project directories. + ''' + for temp_proj_dir in self.temp_proj_directories: + if os.getcwd() == temp_proj_dir: + os.chdir(get_config_dir()) + shutil.rmtree(temp_proj_dir) + + def check_quit(self, *args): + '''Check if the KD can be closed. + If the project is modified, show an alert. Otherwise closes it. + ''' + if self.popup: + # if there is something open, stops the propagation + show_message( + 'You must close all popups before closing Kivy Designer', + 5, 'error') + return True + proj = self.project_manager.current_project + if proj.new_project or not proj.saved: + _confirm_dlg_save = ConfirmationDialogSave('Your project is ' + 'not saved.\nWhat ' + 'would you like to do?') + + def save(*args): + self.close_popup() + self.action_btn_save_pressed(exit_on_save=True) + + _confirm_dlg_save.bind(on_dont_save=self._perform_quit, + on_save=save, + on_cancel=self.close_popup) + + self.popup = Popup(title='Quit', content=_confirm_dlg_save, + size_hint=(None, None), size=('300pt', '150pt'), + auto_dismiss=False) + self.popup.open() + return True + self._perform_quit() + return False + + def on_request_close(self, *args): + '''Event Handler for 'on_request_close' event of Window. + Check if the project was saved before exit + ''' + return self.check_quit() + + def action_btn_quit_pressed(self, *args): + '''Event Handler when ActionButton "Quit" is pressed. + ''' + return self.check_quit() + + def _perform_quit(self, *args): + '''Perform Application qui.Application + ''' + self.remove_temp_proj_directories() + App.get_running_app().stop() + + def action_btn_undo_pressed(self, *args): + '''Event Handler when ActionButton "Undo" is pressed. + ''' + + if self._edit_selected == 'Play': + self.undo_manager.do_undo() + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.do_undo() + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.do_undo() + break + + def action_btn_redo_pressed(self, *args): + '''Event Handler when ActionButton "Redo" is pressed. + ''' + + if self._edit_selected == 'Play': + self.undo_manager.do_redo() + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.do_redo() + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.do_redo() + break + + def action_btn_cut_pressed(self, *args): + '''Event Handler when ActionButton "Cut" is pressed. + ''' + + if self._edit_selected == 'Play': + self.ui_creator.playground.do_cut() + + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.cut() + + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.cut() + break + + def action_btn_copy_pressed(self, *args): + '''Event Handler when ActionButton "Copy" is pressed. + ''' + + if self._edit_selected == 'Play': + self.ui_creator.playground.do_copy() + + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.copy() + + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.copy() + break + + def action_btn_paste_pressed(self, *args): + '''Event Handler when ActionButton "Paste" is pressed. + ''' + + if self._edit_selected == 'Play': + self.ui_creator.playground.do_paste() + + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.paste() + + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.paste() + break + + def action_btn_delete_pressed(self, *args): + '''Event Handler when ActionButton "Delete" is pressed. + ''' + + if self._edit_selected == 'Play': + self.ui_creator.playground.do_delete() + + elif self._edit_selected == 'KV': + self.ui_creator.kv_code_input.delete_selection() + + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + tab_item.content.code_input.delete_selection() + break + + def action_btn_select_all_pressed(self, *args): + '''Event Handler when ActionButton "Select All" is pressed. + ''' + + if self._edit_selected == 'Play': + self.ui_creator.playground.do_select_all() + + elif self._edit_selected == 'KV': + Clock.schedule_once(self.ui_creator.kv_code_input.do_select_all) + + elif self._edit_selected == 'Py': + tab_list = self.designer_content.tab_pannel.tab_list + for tab_item in tab_list: + if hasattr(tab_item.content, 'code_input') \ + and tab_item.content.code_input.clicked: + Clock.schedule_once( + tab_item.content.code_input.do_select_all) + break + + def action_chk_btn_toolbox_active(self, chk_btn): + '''Event Handler when ActionCheckButton "Toolbox" is activated. + ''' + self.designer_settings.config_parser.set('view', + 'actn_chk_proj_tree', + chk_btn.checkbox.active) + self.designer_settings.config_parser.write() + + if chk_btn.checkbox.active: + self._toolbox_parent.add_widget( + self.designer_content.splitter_tree) + self.designer_content.splitter_tree.width = self._toolbox_width + + else: + self._toolbox_parent = self.designer_content.splitter_tree.parent + self._toolbox_parent.remove_widget( + self.designer_content.splitter_tree) + self._toolbox_width = self.designer_content.splitter_tree.width + self.designer_content.splitter_tree.width = 0 + + def action_chk_btn_property_viewer_active(self, chk_btn): + '''Event Handler when ActionCheckButton "Property Viewer" is activated. + ''' + self.designer_settings.config_parser.set('view', + 'actn_chk_prop_event', + chk_btn.checkbox.active) + self.designer_settings.config_parser.write() + + if chk_btn.checkbox.active: + self._toggle_splitter_widget_tree() + if self.ui_creator.splitter_widget_tree.parent is None: + self._splitter_widget_tree_parent.add_widget( + self.ui_creator.splitter_widget_tree) + self.ui_creator.splitter_widget_tree.width = \ + self._splitter_widget_tree_width + + add_tree = False + if self.ui_creator.grid_widget_tree.parent is not None: + add_tree = True + self.ui_creator.splitter_property.size_hint_y = None + self.ui_creator.splitter_property.height = 300 + + self._splitter_property_parent.clear_widgets() + if add_tree: + self._splitter_property_parent.add_widget( + self.ui_creator.grid_widget_tree) + + self._splitter_property_parent.add_widget( + self.ui_creator.splitter_property) + else: + self._splitter_property_parent = \ + self.ui_creator.splitter_property.parent + self._splitter_property_parent.remove_widget( + self.ui_creator.splitter_property) + self._toggle_splitter_widget_tree() + + def action_chk_btn_widget_tree_active(self, chk_btn): + '''Event Handler when ActionCheckButton "Widget Tree" is activated. + ''' + self.designer_settings.config_parser.set('view', + 'actn_chk_widget_tree', + chk_btn.checkbox.active) + self.designer_settings.config_parser.write() + + if chk_btn.checkbox.active: + self._toggle_splitter_widget_tree() + add_prop = False + if self.ui_creator.splitter_property.parent is not None: + add_prop = True + + self._grid_widget_tree_parent.clear_widgets() + self._grid_widget_tree_parent.add_widget( + self.ui_creator.grid_widget_tree) + if add_prop: + self._grid_widget_tree_parent.add_widget( + self.ui_creator.splitter_property) + self.ui_creator.splitter_property.size_hint_y = None + self.ui_creator.splitter_property.height = 300 + else: + self._grid_widget_tree_parent = \ + self.ui_creator.grid_widget_tree.parent + self._grid_widget_tree_parent.remove_widget( + self.ui_creator.grid_widget_tree) + self.ui_creator.splitter_property.size_hint_y = 1 + self._toggle_splitter_widget_tree() + + def _toggle_splitter_widget_tree(self): + '''To show/hide splitter_widget_tree + ''' + + if self.ui_creator.splitter_widget_tree.parent is not None and\ + self.ui_creator.splitter_property.parent is None and\ + self.ui_creator.grid_widget_tree.parent is None: + + self._splitter_widget_tree_parent = \ + self.ui_creator.splitter_widget_tree.parent + self._splitter_widget_tree_parent.remove_widget( + self.ui_creator.splitter_widget_tree) + self._splitter_widget_tree_width = \ + self.ui_creator.splitter_widget_tree.width + self.ui_creator.splitter_widget_tree.width = 0 + + elif self.ui_creator.splitter_widget_tree.parent is None: + self._splitter_widget_tree_parent.add_widget( + self.ui_creator.splitter_widget_tree) + self.ui_creator.splitter_widget_tree.width = \ + self._splitter_widget_tree_width + + def action_chk_btn_status_bar_active(self, chk_btn): + '''Event Handler when ActionCheckButton "StatusBar" is activated. + ''' + self.designer_settings.config_parser.set('view', + 'actn_chk_status_bar', + chk_btn.checkbox.active) + self.designer_settings.config_parser.write() + + if chk_btn.checkbox.active: + self._statusbar_parent.add_widget(self.statusbar) + self.statusbar.height = self._statusbar_height + else: + self._statusbar_parent = self.statusbar.parent + self._statusbar_height = self.statusbar.height + self._statusbar_parent.remove_widget(self.statusbar) + self.statusbar.height = 0 + + def action_chk_btn_kv_area_active(self, chk_btn): + '''Event Handler when ActionCheckButton "KVLangArea" is activated. + ''' + self.designer_settings.config_parser.set('view', + 'actn_chk_kv_lang_area', + chk_btn.checkbox.active) + self.designer_settings.config_parser.write() + + if chk_btn.checkbox.active: + self.ui_creator.splitter_kv_code_input.height = \ + self._kv_area_height + self._kv_area_parent.add_widget( + self.ui_creator.splitter_kv_code_input) + else: + self._kv_area_parent = \ + self.ui_creator.splitter_kv_code_input.parent + self._kv_area_height = \ + self.ui_creator.splitter_kv_code_input.height + self.ui_creator.splitter_kv_code_input.height = 0 + self._kv_area_parent.remove_widget( + self.ui_creator.splitter_kv_code_input) + + def _error_adding_file(self, *args): + '''Event Handler for 'on_error' event of self._add_file_dlg + ''' + + show_message('Error while adding file to project', 5, 'error') + self.close_popup() + + def _added_file(self, *args): + '''Event Handler for 'on_added' event of self._add_file_dlg + ''' + + show_message('File successfully added to project', 5, 'info') + self.close_popup() + self.designer_content.update_tree_view( + self.project_manager.current_project) + + def action_btn_add_file_pressed(self, *args): + '''Event Handler when ActionButton "Add File" is pressed. + ''' + if self.popup: + return False + add_file_dlg = AddFileDialog( + self.project_manager.current_project) + add_file_dlg.bind(on_added=self._added_file, + on_error=self._error_adding_file, + on_cancel=self.close_popup) + + self.popup = Popup(title="Add File", + content=add_file_dlg, + size_hint=(None, None), + size=(480, 350), auto_dismiss=False) + + self.popup.open() + return True + + def action_btn_run_module_pressed(self, *args): + + if self.modulescontview is None: + self.modulescontview = ModulesContView() + self.modulescontview.bind( + on_module=self.action_btn_run_project_pressed) + + self.actionbar.add_widget(self.modulescontview) + + def action_btn_project_settings_pressed(self, *args): + '''Event Handler when ActionButton "Project Settings" is pressed. + ''' + if self.popup: + return False + self.proj_settings = ProjectSettings( + project=self.project_manager.current_project) + self.proj_settings.load_proj_settings() + self.proj_settings.bind(on_close=self.close_popup) + self.popup = Popup(title="Project Settings", + content=self.proj_settings, + size_hint=(None, None), + size=(720, 480), auto_dismiss=False) + + self.popup.open() + return True + + def action_btn_edit_prof_project_pressed(self, *args): + '''Event Handler when ActionButton "Edit Profiles" is pressed. + ''' + if self.popup: + return False + self.prof_settings.load_profiles() + self.popup = Popup(title="Build Profiles", + content=self.prof_settings, + size_hint=(None, None), + size=(720, 480), + auto_dismiss=False) + self.popup.open() + return True + + def check_selected_prof(self, *args): + '''Check if there is a selected build profile. + :return: True if ok. Show an alert and returns false if not. + ''' + if self.selected_profile == '' or not \ + os.path.isfile(self.selected_profile): + show_alert('Profiler error', "Please, select a build profile on" + "'Run' -> 'Select Profile' menu") + return False + + self.profiler.load_profile(self.selected_profile, + self.project_manager.current_project.path) + return True + + def action_btn_stop_project_pressed(self, *args): + '''Event handler when ActionButton "Stop" is pressed. + ''' + if not self.check_selected_prof(): + return + self.profiler.stop() + + def action_btn_clean_project_pressed(self, *args): + '''Event handler when ActionButton "Clean" is pressed + ''' + if not self.check_selected_prof(): + return + self.profiler.clean() + + def action_btn_build_project_pressed(self, *args): + '''Event handler when ActionButton "Build" is pressed + ''' + if not self.check_selected_prof(): + return + self.profiler.build() + + def action_btn_rebuild_project_pressed(self, *args): + '''Event handler when ActionButton "Build" is pressed + ''' + if not self.check_selected_prof(): + return + self.profiler.rebuild() + + def action_btn_run_project_pressed(self, *args, **kwargs): + '''Event Handler when ActionButton "Run" is pressed. + ''' + if not self.check_selected_prof(): + return + + self.profiler.run(*args, **kwargs) + + def on_sandbox_getting_exception(self, *args): + '''Event Handler for + :class:`~designer.uix.sandbox.DesignerSandbox` + on_getting_exception event. This function will add exception + string in error_console. + ''' + show_error_console(traceback.format_exc(), append=False) + if self.ui_creator.playground.sandbox.error_active: + self.ui_creator.tab_pannel.switch_to( + self.ui_creator.tab_pannel.tab_list[0]) + + self.ui_creator.playground.sandbox.error_active = False + + def action_btn_about_pressed(self, *args): + '''Event handler for 'on_release' event of DesignerActionButton + "About Kivy Designer" + ''' + if self.popup: + return False + about_dlg = AboutDialog() + self.popup = Popup(title='About Kivy Designer', + content=about_dlg, + size_hint=(None, None), size=(600, 400), + auto_dismiss=False) + self.popup.open() + about_dlg.bind(on_close=self.close_popup) + return True + + #### GET SITES #### + def open_repo(self, *args): + ''' + Open the Kivy Designer repository + ''' + webbrowser.open("https://github.com/kivy/kivy-designer") + + def open_docs(self, *args): + ''' + Open the Kivy docs + ''' + webbrowser.open("http://kivy.org/docs/") + + def open_kd_docs(self, *args): + ''' + Open the Kivy Designer documentation + ''' + webbrowser.open("http://kivy-designer.readthedocs.org") + +class DesignerException(ExceptionHandler): + + raised_exception = False + '''Indicates if the BugReporter has already raised some exception + ''' + + def handle_exception(self, inst): + if self.raised_exception: + return ExceptionManager.PASS + App.get_running_app().stop() + if isinstance(inst, KeyboardInterrupt): + return ExceptionManager.PASS + else: + for child in Window.children: + Window.remove_widget(child) + self.raised_exception = True + Window.fullscreen = False + BugReporterApp(traceback=traceback.format_exc()).run() + return ExceptionManager.PASS + + +class DesignerApp(App): + + widget_focused = ObjectProperty(allownone=True) + '''Currently focused widget + ''' + + started = BooleanProperty(False) + '''Indicates if has finished the build() + ''' + + title = 'Kivy Designer' + + def on_stop(self, *args): + if hasattr(self.root, 'ui_creator'): + if hasattr(self.root.ui_creator, 'py_console'): + self.root.ui_creator.py_console.exit() + + def build(self): + ExceptionManager.add_handler(DesignerException()) + Factory.register('Playground', module= 'components.playground') + Factory.register('Toolbox', module= 'components.toolbox') + Factory.register('StatusBar', module= 'components.statusbar') + Factory.register('PropertyViewer', + module= 'components.property_viewer') + Factory.register('EventViewer', + module= 'components.event_viewer') + Factory.register('WidgetsTree', + module= 'components.widgets_tree') + Factory.register('UICreator', module= 'components.ui_creator') + Factory.register('DesignerGit', module= 'tools.git_integration') + Factory.register('DesignerContent', + module= 'components.designer_content') + Factory.register('KivyConsole', + module= 'components.kivy_console') + Factory.register('KVLangAreaScroll', + module= 'components.kv_lang_area') + Factory.register('PythonConsole', module= 'uix.py_console') + Factory.register('DesignerContent', + module= 'components.designer_content') + Factory.register('EventDropDown', + module= 'components.event_viewer') + Factory.register('DesignerActionGroup', + module= 'uix.action_items') + Factory.register('DesignerActionButton', + module= 'uix.action_items') + Factory.register('DesignerActionSubMenu', + module= 'uix.action_items') + Factory.register('DesignerStartPage', + module= 'components.start_page') + Factory.register('DesignerLinkLabel', + module= 'components.start_page') + Factory.register('RecentFilesBox', + module= 'components.start_page') + Factory.register('ContextMenu', + module= 'components.edit_contextual_view') + Factory.register('PlaygroundSizeSelector', + module= 'components.playground_size_selector') + Factory.register('CodeInputFind', + module= 'uix.code_find') + + self._widget_focused = None + # self.root = Designer() + Clock.schedule_once(self._setup) + return Designer() + + def _setup(self, *args): + '''To setup the properties of different classes + ''' + + if self.root.save_window_size: + Clock.schedule_once(self.root.restore_window_size, 0) + self.root.set_escape_exit() + + self.root.proj_tree_view = self.root.designer_content.tree_view + self.root.ui_creator = self.root.designer_content.ui_creator + self.root.statusbar.playground = self.root.ui_creator.playground + self.root.ui_creator.playground.undo_manager = self.root.undo_manager + self.root.ui_creator.eventviewer.designer_tabbed_panel = \ + self.root.designer_content.tab_pannel + self.root.statusbar.bind(height=self.root.on_statusbar_height) + self.root.actionbar.bind(height=self.root.on_actionbar_height) + self.root.ui_creator.playground.sandbox = DesignerSandbox() + self.root.ui_creator.playground.add_widget( + self.root.ui_creator.playground.sandbox) + self.root.ui_creator.playground.sandbox.pos = \ + self.root.ui_creator.playground.pos + self.root.ui_creator.playground.sandbox.size = \ + self.root.ui_creator.playground.size + + max_lines = int(self.root.designer_settings.config_parser.getdefault( + 'global', 'num_max_kivy_console', 200 + )) + self.root.ui_creator.kivy_console.cached_history = max_lines + + self.root.ui_creator.playground.sandbox.bind( + on_getting_exception=self.root.on_sandbox_getting_exception) + + self.bind( + widget_focused=self.root.ui_creator.propertyviewer.setter('widget') + ) + self.bind( + widget_focused=self.root.ui_creator.eventviewer.setter('widget') + ) + + self.focus_widget(self.root.ui_creator.playground.root) + + self.create_kivy_designer_dir() + self.root.start_page.recent_files_box.add_recent( + self.root.recent_manager.list_projects) + + self.root.fill_select_profile_menu() + self.started = True + + def create_kivy_designer_dir(self): + '''To create the ~/.kivy-designer dir + ''' + + if not os.path.exists(get_config_dir()): + os.mkdir(get_config_dir()) + + def create_draggable_element(self, instance, widget_name, touch, + widget=None): + '''Create PlagroundDragElement and make it draggable + until the touch is released also search default args if exist + :param widget: instance of widget. + If set, widget_name will be ignored + :param touch: instance of the current touch + :param instance: if from toolbox, ToolboxButton instance. + None otherwise + :param widget_name: name of the widget that will be dragged + ''' + container = None + if widget: + container = PlaygroundDragElement( + playground=self.root.ui_creator.playground, + child=Widget(), + widget=widget) + touch.grab(container) + touch.grab_current = container + container.on_touch_move(touch) + container.center_x = touch.x + container.y = touch.y + 20 + else: + default_args = {} + extra_args = {} + for options in toolbox_widgets: + if widget_name == options[0]: + if len(options) > 2: + default_args = options[2].copy() + if len(options) > 3: + extra_args = options[3].copy() + break + container = self.root.ui_creator.playground.\ + get_playground_drag_element(instance, widget_name, + touch, default_args, extra_args) + if container: + self.root.add_widget(container) + else: + show_message("Cannot create %s" % widget_name, 5, 'error') + + container.widgettree = self.root.ui_creator.widgettree + return container + + def focus_widget(self, widget, *args): + '''Called when a widget is select in Playground. It will also draw + lines around focussed widget. + :param widget: widget to receive focus + ''' + + if self._widget_focused and (widget is None or + self._widget_focused[0] != widget): + fwidget = self._widget_focused[0] + for instr in self._widget_focused[1:]: + fwidget.canvas.after.remove(instr) + self._widget_focused = [] + + self.widget_focused = widget + + if not widget: + return + + x, y = widget.pos + right, top = widget.right, widget.top + points = [x, y, right, y, right, top, x, top] + if self._widget_focused: + line = self._widget_focused[2] + line.points = points + else: + with widget.canvas.after: + color = Color(.42, .62, .65) + line = Line(points=points, close=True, width=2.) + self._widget_focused = [widget, color, line] + + self.root.ui_creator.playground.clicked = True + self.root.on_show_edit() diff --git a/designer/components/__init__.py b/designer/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/components/buildozer_spec_editor.py b/designer/components/buildozer_spec_editor.py new file mode 100644 index 0000000..08fe895 --- /dev/null +++ b/designer/components/buildozer_spec_editor.py @@ -0,0 +1,224 @@ +import json +import os +import tempfile +import webbrowser +from io import open + +from uix.settings import SettingDict, SettingList +from utils.utils import get_kd_data_dir, ignore_proj_watcher +from kivy.properties import ConfigParser, ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.settings import ( + ContentPanel, + InterfaceWithSidebar, + MenuSidebar, + Settings, + SettingsPanel, +) +from pygments.lexers.configs import IniLexer + + +class SpecContentPanel(ContentPanel): + + def on_current_uid(self, *args): + result = super(SpecContentPanel, self).on_current_uid(*args) + if isinstance(self.current_panel, SpecCodeInput): + self.current_panel.load_spec() + return result + + +class SpecMenuSidebar(MenuSidebar): + + def on_selected_uid(self, *args): + '''(internal) unselects any currently selected menu buttons, unless + they represent the current panel. + + ''' + for button in self.buttons_layout.children: + button.selected = button.uid == self.selected_uid + + +class SpecEditorInterface(InterfaceWithSidebar): + + def open_buildozer_docs(self, *args): + webbrowser.open('http://buildozer.readthedocs.org') + + +class SpecSettingsPanel(SettingsPanel): + + def get_value(self, section, key): + '''Return the value of the section/key from the :attr:`config` + ConfigParser instance. This function is used by :class:`SettingItem` to + get the value for a given section/key. + + If you don't want to use a ConfigParser instance, you might want to + override this function. + ''' + config = self.config + if not config: + return + if config.has_option(section, key): + return config.get(section, key) + else: + return '' + + def set_value(self, section, key, value): + # some keys are not enabled by default on .spec. If the value is empty + # and this key is not on .spec, so we don't need to save it + if not value and not self.config.has_option(section, key): + return False + super(SpecSettingsPanel, self).set_value(section, key, value) + + +class SpecCodeInput(BoxLayout): + + text_input = ObjectProperty(None) + '''CodeInput with buildozer.spec text. + Instance of :class:`kivy.config.ObjectProperty` and defaults to None + ''' + + lbl_error = ObjectProperty(None) + '''(internal) Label to display errors. + Instance of :class:`kivy.config.ObjectProperty` and defaults to None + ''' + + spec_path = StringProperty('') + '''buildozer.spec path. + Instance of :class:`kivy.config.StringProperty` and defaults to '' + ''' + + __events__ = ('on_change', ) + + def __init__(self, **kwargs): + super(SpecCodeInput, self).__init__(**kwargs) + self.text_input.lexer = IniLexer() + + def load_spec(self, *args): + '''Read the buildozer.spec and update the CodeInput + ''' + self.lbl_error.color = [0, 0, 0, 0] + self.text_input.text = open(self.spec_path, 'r', + encoding='utf-8').read() + + @ignore_proj_watcher + def _save_spec(self, *args): + '''Try to save the spec file. If there is a error, show the label. + If not, save the file and dispatch on_change + ''' + f = tempfile.NamedTemporaryFile() + f.write(self.text_input.text) + try: + cfg = ConfigParser() + cfg.read(f.name) + except Exception: + self.lbl_error.color = [1, 0, 0, 1] + else: + spec = open(self.spec_path, 'w') + spec.write(self.text_input.text) + spec.close() + self.dispatch('on_change') + f.close() + + def on_change(self, *args): + '''Event handler to dispatch a .spec modification + ''' + pass + + +class BuildozerSpecEditor(Settings): + '''Subclass of :class:`kivy.uix.settings.Settings` responsible for + the UI editor of buildozer spec + ''' + + config_parser = ObjectProperty(None) + '''Config Parser for this class. Instance + of :class:`kivy.config.ConfigParser` + ''' + + def __init__(self, **kwargs): + super(BuildozerSpecEditor, self).__init__(**kwargs) + self.register_type('dict', SettingDict) + self.register_type('list', SettingList) + self.SPEC_PATH = '' + self.proj_dir = '' + self.config_parser = ConfigParser.get_configparser("buildozer_spec") + if self.config_parser is None: + self.config_parser = ConfigParser(name="buildozer_spec") + + def load_settings(self, proj_dir): + '''This function loads project settings + :param proj_dir: project directory with buildozer.spec + ''' + self.interface.menu.buttons_layout.clear_widgets() + self.proj_dir = proj_dir + self.SPEC_PATH = os.path.join(proj_dir, 'buildozer.spec') + + self.config_parser.read(self.SPEC_PATH) + self.add_json_panel('Application', self.config_parser, + os.path.join(get_kd_data_dir(), + 'settings', 'buildozer_spec_app.json')) + self.add_json_panel('Android', self.config_parser, + os.path.join(get_kd_data_dir(), + 'settings', 'buildozer_spec_android.json')) + self.add_json_panel('iOS', self.config_parser, + os.path.join(get_kd_data_dir(), + 'settings', 'buildozer_spec_ios.json')) + self.add_json_panel('Buildozer', self.config_parser, + os.path.join(get_kd_data_dir(), + 'settings', 'buildozer_spec_buildozer.json')) + + raw_spec = SpecCodeInput(spec_path=self.SPEC_PATH) + raw_spec.bind(on_change=self.on_spec_changed) + self.interface.add_panel(raw_spec, "buildozer.spec", raw_spec.uid) + + menu = self.interface.menu + menu.selected_uid = menu.buttons_layout.children[-1].uid + + def on_spec_changed(self, *args): + self.load_settings(self.proj_dir) + + # force to show the last panel + menu = self.interface.menu + menu.selected_uid = menu.buttons_layout.children[0].uid + + def create_json_panel(self, title, config, filename=None, data=None): + '''Override the original method to use the custom SpecSettingsPanel + ''' + if filename is None and data is None: + raise Exception('You must specify either the filename or data') + if filename is not None: + with open(filename, 'r', encoding='utf-8') as fd: + data = json.loads(fd.read()) + else: + data = json.loads(data) + if type(data) != list: + raise ValueError('The first element must be a list') + panel = SpecSettingsPanel(title=title, settings=self, config=config) + + for setting in data: + # determine the type and the class to use + if not 'type' in setting: + raise ValueError('One setting are missing the "type" element') + ttype = setting['type'] + cls = self._types.get(ttype) + if cls is None: + raise ValueError( + 'No class registered to handle the <%s> type' % + setting['type']) + + # create a instance of the class, without the type attribute + del setting['type'] + str_settings = {} + for key, item in setting.items(): + str_settings[str(key)] = item + + instance = cls(panel=panel, **str_settings) + + # instance created, add to the panel + panel.add_widget(instance) + + return panel + + @ignore_proj_watcher + def on_config_change(self, *args): + super(BuildozerSpecEditor, self).on_config_change(*args) diff --git a/designer/components/designer_content.py b/designer/components/designer_content.py new file mode 100644 index 0000000..0776b2f --- /dev/null +++ b/designer/components/designer_content.py @@ -0,0 +1,445 @@ +import os +from io import open +from components.buildozer_spec_editor import BuildozerSpecEditor +from uix.confirmation_dialog import ConfirmationDialog +from uix.py_code_input import PyScrollView +from utils.utils import get_designer, show_message +from kivy.app import App +from kivy.properties import ( + BooleanProperty, + Clock, + ObjectProperty, + OptionProperty, + StringProperty, + partial, +) +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.popup import Popup +from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelHeader, TabbedPanelItem +from kivy.uix.treeview import TreeViewLabel + + +SUPPORTED_EXT = ('.py', '.py2', '.kv', '.py3', '.txt', '.diff', ) + + +class DesignerContent(FloatLayout): + '''This class contains the body of the Kivy Designer. It contains, + Project Tree and TabbedPanel. + ''' + + ui_creator = ObjectProperty(None) + '''This property refers to the + :class:`~designer.components.ui_creator.UICreator` + instance. As there can only be one + :data:`ui_creator` is a :class:`~kivy.properties.ObjectProperty` + ''' + + tree_toolbox_tab_panel = ObjectProperty(None) + '''TabbedPanel containing Toolbox and Project Tree. Instance of + :class:`~designer.components.designer_content.DesignerTabbedPanel` + ''' + + splitter_tree = ObjectProperty(None) + '''Reference to the splitter parent of tree_toolbox_tab_panel. + :data:`splitter_toolbox` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + toolbox = ObjectProperty(None) + '''Reference to the :class:`~designer.components.toolbox.Toolbox` instance. + :data:`toolbox` is an :class:`~kivy.properties.ObjectProperty` + ''' + + tree_view = ObjectProperty(None) + '''This property refers to Project Tree. Project Tree displays project's + py files under its parent directories. Clicking on any of the file will + open it up for editing. + :data:`tree_view` is a :class:`~kivy.properties.ObjectProperty` + ''' + + tab_pannel = ObjectProperty(None) + '''This property refers to the instance of + :class:`~designer.components.designer_content.DesignerTabbedPanel`. + :data:`tab_pannel` is a :class:`~kivy.properties.ObjectProperty` + ''' + + in_find = BooleanProperty(False) + '''This property indicates if the find menu is activated + :data:`in_find` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False + ''' + + current_codeinput = ObjectProperty(None, allownone=True) + '''Instance of the current PythonCodeInput + :data:`current_codeinput` is a :class:`~kivy.properties.ObjectProperty` + and defaults to None + ''' + + find_tool = ObjectProperty(None) + '''Instance of :class:`~designer.uix.code_find.CodeInputFind`. + :data:`find_tool` is a :class:`~kivy.properties.ObjectProperty` + and defaults to None + ''' + + project = ObjectProperty(None) + '''Instance of + :class:`~designer.core.project_manager.Project` + with the current opened project. + :data:`project` is a :class:`~kivy.properties.ObjectProperty` + and defaults to None + ''' + + def __init__(self, **kwargs): + super(DesignerContent, self).__init__(**kwargs) + self.find_tool.bind(on_close=partial(self.show_findmenu, False)) + self.find_tool.bind(on_next=self.find_tool_next) + self.find_tool.bind(on_prev=self.find_tool_prev) + self.focus_code_input = Clock.create_trigger(self._focus_input) + + def update_tree_view(self, project): + '''This function is used to insert all the py files detected. + as a node in the Project Tree. + :param project: instance of the current project + ''' + self.project = project + + # Fill nodes with file and directories + self._root_node = self.tree_view.root + self.clear_tree_view() + + for _file in sorted(project.get_files()): + self.add_file_to_tree_view(_file) + + self.tree_view.root_options = dict( + text=os.path.basename(self.project.path)) + + def clear_tree_view(self): + ''' + Clear the TreeView + ''' + temp = list(self.tree_view.iterate_all_nodes()) + for node in temp: + self.tree_view.remove_node(node) + + def add_file_to_tree_view(self, _file): + '''This function is used to insert project files given by it's path + argument _file. It will also insert any directory node if not present. + :param _file: path of the file to be inserted + ''' + + self.tree_view.root_options = dict(text='') + dirname = os.path.dirname(_file) + dirname = dirname.replace(self.project.path, '') + # The way os.path.dirname works, there will never be '/' at the end + # of a directory. So, there will always be '/' at the starting + # of 'dirname' variable after removing proj_dir + + # This algorithm first breaks path into its components + # and creates a list of these components. + _dirname = dirname + _basename = 'a' + list_path_components = [] + while _basename != '': + _split = os.path.split(_dirname) + _dirname = _split[0] + _basename = _split[1] + list_path_components.insert(0, _split[1]) + + if list_path_components[0] == '': + del list_path_components[0] + + # Then it traverses from root_node to its children searching from + # each component in the path. If it doesn't find any component + # related with node then it creates it. + node = self._root_node + while list_path_components != []: + found = False + for _node in node.nodes: + if _node.text == list_path_components[0]: + node = _node + found = True + break + + if not found: + for component in list_path_components: + _node = TreeViewLabel(text=component) + self.tree_view.add_node(_node, node) + node = _node + list_path_components = [] + else: + del list_path_components[0] + + # Finally add file_node with node as parent. + file_node = TreeViewLabel(text=os.path.basename(_file)) + file_node.bind(on_touch_down=self._file_node_clicked) + self.tree_view.add_node(file_node, node) + + def _file_node_clicked(self, instance, touch): + '''This is emmited whenever any file node of Project Tree is + clicked. This will open up a tab in DesignerTabbedPanel, for + editing that py file. + ''' + + # Travel upwards and find the path of instance clicked + path = instance.text + parent = instance.parent_node + while parent != self._root_node: + _path = parent.text + path = os.path.join(_path, path) + parent = parent.parent_node + + full_path = os.path.join(self.project.path, path) + if os.path.basename(full_path) == 'buildozer.spec': + self.tab_pannel.show_buildozer_spec_editor(self.project) + else: + ext = path[path.rfind('.'):] + if ext not in SUPPORTED_EXT: + show_message('This extension is not yet supported', 5, 'error') + return + if ext == '.kv': + self.ui_creator.playground.load_widget_from_file(full_path) + self.tab_pannel.switch_to(self.tab_pannel.tab_list[-1]) + else: + self.tab_pannel.open_file(full_path, path) + + def show_findmenu(self, visible, *args): + '''Makes find menu visible/invisible + ''' + self.in_find = visible + if visible: + Clock.schedule_once(self._focus_find) + + def _focus_find(self, *args): + '''Focus on the find tool + ''' + self.find_tool.txt_query.focus = True + + def on_current_tab(self, tabbed_panel, *args): + '''Event handler to tab selection changes + ''' + self.show_findmenu(False) + Clock.schedule_once(partial(self._selected_content, tabbed_panel)) + + def _selected_content(self, tabbed_panel, *args): + '''Called after updating tab content + ''' + if not tabbed_panel.content.children: + return + content = tabbed_panel.content.children[0] + + if isinstance(content, PyScrollView): + self.current_codeinput = content.code_input + else: + self.current_codeinput = None + + def find_tool_prev(self, instance, *args): + if self.current_codeinput: + self.current_codeinput.focus = True + self.current_codeinput.find_prev(instance.query, + instance.use_regex, + instance.case_sensitive) + + def find_tool_next(self, instance, *args): + if self.current_codeinput: + self.current_codeinput.focus = True + self.current_codeinput.find_next(instance.query, + instance.use_regex, + instance.case_sensitive) + + def _focus_input(self, *args): + self.current_codeinput.focus = True + + +class DesignerTabbedPanel(TabbedPanel): + '''DesignerTabbedPanel is used to display files opened up in tabs with + :class:`~designer.components.ui_creator.UICreator` + Tab as a special one containing all features to edit the UI. + ''' + + def open_file(self, path, rel_path, switch_to=True): + '''This will open file for editing in the DesignerTabbedPanel. + :param switch_to: if should switch to the new tab + :param rel_path: relative file path + :param path: absolute file path to open + ''' + for i, tab_item in enumerate(self.tab_list): + if hasattr(tab_item, 'rel_path') and tab_item.rel_path == rel_path: + # self.switch_to(self.tab_list[len(self.tab_list) - i - 2]) + self.switch_to(tab_item) + return + + panel_item = DesignerCloseableTab(title=os.path.basename(path)) + panel_item.bind(on_close=self.on_close_tab) + f = open(path, 'r', encoding='utf-8') + scroll = PyScrollView() + _py_code_input = scroll.code_input + _py_code_input.text = f.read() + _py_code_input.path = path + _py_code_input.bind( + on_show_edit=App.get_running_app().root.on_show_edit) + _py_code_input.bind(saved=panel_item.on_tab_content_saved) + _py_code_input.bind(error=panel_item.on_tab_content_error) + f.close() + + d = get_designer() + if _py_code_input not in d.code_inputs: + d.code_inputs.append(_py_code_input) + + panel_item.content = scroll + panel_item.rel_path = rel_path + self.add_widget(panel_item) + if switch_to: + self.switch_to(self.tab_list[0]) + + def show_buildozer_spec_editor(self, project): + '''Loads the buildozer.spec file and adds a new tab with the + Buildozer Spec Editor + :param project: instance of the current project + ''' + for i, child in enumerate(self.tab_list): + if isinstance(child.content, BuildozerSpecEditor): + self.switch_to(child) + return child + + spec_editor = get_designer().spec_editor + if spec_editor.SPEC_PATH != \ + os.path.join(project.path, 'buildozer.spec'): + spec_editor.load_settings(project.path) + + panel_spec_item = DesignerCloseableTab(title="Spec Editor") + panel_spec_item.bind(on_close=self.on_close_tab) + panel_spec_item.content = spec_editor + panel_spec_item.rel_path = 'buildozer.spec' + self.add_widget(panel_spec_item) + self.switch_to(self.tab_list[0]) + + def on_close_tab(self, instance, *args): + '''Event handler to close icon + :param instance: tab instance + ''' + d = get_designer() + if d.popup: + return False + + self.switch_to(instance) + if instance.has_modification: + # show a dialog to ask if can close + confirm_dlg = ConfirmationDialog( + 'All unsaved changes will be lost.\n' + 'Do you want to continue?') + popup = Popup( + title='New', + content=confirm_dlg, + size_hint=(None, None), + size=('200pt', '150pt'), + auto_dismiss=False) + + def close_tab(*args): + d.close_popup() + self._perform_close_tab(instance) + + confirm_dlg.bind( + on_ok=close_tab, + on_cancel=d.close_popup) + popup.open() + d.popup = popup + else: + Clock.schedule_once(partial(self._perform_close_tab, instance)) + + def _perform_close_tab(self, tab, *args): + # remove code_input from list + if hasattr(tab.content, 'code_input'): + code = tab.content.code_input + d = get_designer() + if code in d.code_inputs: + d.code_inputs.remove(code) + # remove tab + self.remove_widget(tab) + if self.tab_list: + self.switch_to(self.tab_list[0]) + + def cleanup(self): + '''Remove all open tabs + ''' + for child in self.tab_list[:-1]: + self.remove_widget(child) + self.switch_to(self.tab_list[0]) + + +class DesignerTabbedPanelItem(TabbedPanelItem): + pass + + +class DesignerCloseableTab(TabbedPanelHeader): + '''Custom TabbedPanelHeader with close button + ''' + + has_modification = BooleanProperty(False) + '''Indicates if this tab has unsaved content + :data:`has_modification` is a :class:`~kivy.properties.BooleanProperty` + ''' + + has_error = BooleanProperty(False) + '''Indicates if this tab has error + :data:`has_error` is a :class:`~kivy.properties.BooleanProperty` + ''' + + style = OptionProperty('default', + options=['default', 'unsaved', 'error']) + '''Available tab custom styles + :data:`style` is a :class:`~kivy.properties.OptionProperty` + ''' + + title = StringProperty('') + '''Tab header title + :data:`title` is a :class:`~kivy.properties.StringProperty` + ''' + + rel_path = StringProperty('') + '''Relative path of file + :data:`rel_path` is a :class:`~kivy.properties.StringProperty` + ''' + + __events__ = ('on_close', ) + + def on_close(self, *args): + pass + + def on_style(self, instance, style, *args): + '''Update the tab style + ''' + if style == 'default': + self.text = self.title + elif style == 'unsaved': + self.text = '[i]%s[i]' % self.title + elif style == 'error': + self.text = '[color=#e51919]%s[/color]' % self.title + + def on_tab_content_saved(self, instance, value, *args): + '''Callback to the tab content saved modifications. + value is True or False, indicating if status of the file + ''' + self.has_modification = not value + + # if the file contains an error, keep showing it + if self.style == 'error': + return + + self.style = 'default' if value else 'unsaved' + + def on_tab_content_error(self, has_error, *args): + '''Callback to the tab content error status + has_error is True or False, indicating the error + ''' + + if has_error: + self.style = 'error' + elif self.style == 'error': + self.style = 'default' + + def on_text(self, *args): + '''updates the tab width when it has a new title + ''' + def update_width(*args): + self.width = min(self.texture_size[0] + 40, 200) + Clock.schedule_once(update_width) diff --git a/designer/components/dialogs/__init__.py b/designer/components/dialogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/components/dialogs/about.py b/designer/components/dialogs/about.py new file mode 100644 index 0000000..b268e09 --- /dev/null +++ b/designer/components/dialogs/about.py @@ -0,0 +1,14 @@ +from kivy.uix.floatlayout import FloatLayout + + +class AboutDialog(FloatLayout): + '''AboutDialog, to display about information. + It emits 'on_cancel' event when 'Cancel' button is released. + ''' + + __events__ = ('on_close',) + + def on_close(self, *args): + '''Default handler for 'on_close' event + ''' + pass diff --git a/designer/components/dialogs/add_file.py b/designer/components/dialogs/add_file.py new file mode 100644 index 0000000..b3a3dd8 --- /dev/null +++ b/designer/components/dialogs/add_file.py @@ -0,0 +1,110 @@ +import os +import shutil + +from utils.utils import ignore_proj_watcher +from uix.xpopup.file import XFileOpen, XFolder +from kivy.properties import ObjectProperty +from kivy.uix.boxlayout import BoxLayout + + +class AddFileDialog(BoxLayout): + '''AddFileDialog is a dialog for adding files to current project. It emits + 'on_added' event if file has been added successfully, 'on_error' if + there has been some error in adding file and 'on_cancel' when user + wishes to cancel the operation. + ''' + + text_file = ObjectProperty() + '''An instance to TextInput showing file path to be added. + :data:`text_file` is a :class:`~kivy.properties.ObjectProperty` + ''' + + text_folder = ObjectProperty() + '''An instance to TextInput showing folder where file has to be added. + :data:`text_folder` is a :class:`~kivy.properties.ObjectProperty` + ''' + + lbl_error = ObjectProperty() + '''An instance to Label to display errors. + :data:`lbl_error` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_cancel', 'on_added', 'on_error') + + def __init__(self, project, **kwargs): + super(AddFileDialog, self).__init__(**kwargs) + self.project = project + self._popup = None + self._fbrowser = None + + def on_cancel(self): + pass + + def on_added(self): + pass + + def on_error(self): + pass + + @ignore_proj_watcher + def _perform_add_file(self): + '''To copy file from its original path to new path + ''' + + if self.text_file.text == '': + self.lbl_error.text = "Select the File" + return + + target_folder = self.text_folder.text + + folder = os.path.join(self.project.path, target_folder) + if not os.path.exists(folder): + os.mkdir(folder) + + if os.path.exists(os.path.join(folder, + os.path.basename(self.text_file.text))): + self.lbl_error.text = "There is a file with the same name!" + return + + try: + shutil.copy(self.text_file.text, + os.path.join(folder, + os.path.basename(self.text_file.text))) + + self.dispatch('on_added') + + except (OSError, IOError): + self.dispatch('on_error') + + def _file_load(self, instance): + '''To set the text of text_file, to the file selected. + ''' + if instance.is_canceled(): + return + + self.text_file.text = instance.selection[0] + + def open_file_btn_pressed(self, *args): + '''To load File Browser for selected file when 'Open File' is clicked + ''' + + def_path = os.path.expanduser('~') + XFileOpen(title="Open File", on_dismiss=self._file_load, path=def_path) + + def _folder_load(self, instance): + '''To set the text of text_folder, to the folder selected. + ''' + + if instance.is_canceled(): + return + + target_dir = os.path.relpath(instance.path, self.project.path) + self.text_folder.text = target_dir + + def open_folder_btn_pressed(self, *args): + '''To load File Browser for selected folder when 'Open Folder' + is clicked + ''' + + XFolder(title="Open Folder", on_dismiss=self._folder_load, + path=self.project.path, dirselect=True) diff --git a/designer/components/dialogs/help.py b/designer/components/dialogs/help.py new file mode 100644 index 0000000..9266a42 --- /dev/null +++ b/designer/components/dialogs/help.py @@ -0,0 +1,20 @@ +from kivy.properties import ObjectProperty +from kivy.uix.boxlayout import BoxLayout + + +class HelpDialog(BoxLayout): + '''HelpDialog, in which help will be displayed from help.rst. + It emits 'on_cancel' event when 'Cancel' button is released. + ''' + + rst = ObjectProperty(None) + '''rst is reference to `kivy.uix.rst.RstDocument` to display help from + help.rst + ''' + + __events__ = ('on_cancel',) + + def on_cancel(self, *args): + '''Default handler for 'on_cancel' event + ''' + pass diff --git a/designer/components/dialogs/new_project.py b/designer/components/dialogs/new_project.py new file mode 100644 index 0000000..2acc096 --- /dev/null +++ b/designer/components/dialogs/new_project.py @@ -0,0 +1,150 @@ +from functools import partial +from os.path import join + +from kivy.uix.scrollview import ScrollView + +from utils import constants +from utils.utils import get_kd_data_dir +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.uix.boxlayout import BoxLayout + + +NEW_PROJECTS = { + 'FloatLayout': ('template_floatlayout_kv', + 'template_floatlayout_py'), + 'BoxLayout': ('template_boxlayout_kv', + 'template_boxlayout_py'), + 'ScreenManager': ('template_screen_manager_kv', + 'template_screen_manager_py'), + 'ActionBar': ('template_actionbar_kv', + 'template_actionbar_py'), + 'Carousel and ActionBar': ('template_actionbar_carousel_kv', + 'template_actionbar_carousel_py'), + 'ScreenManager and ActionBar': ('template_screen_manager_actionbar_kv', + 'template_screen_manager_actionbar_py'), + 'TabbedPanel': ('template_tabbed_panel_kv', + 'template_tabbed_panel_py'), + 'TextInput and ScrollView': ('template_textinput_scrollview_kv', + 'template_textinput_scrollview_py')} + + +class ProjectTemplateBox(ScrollView): + '''Container consistings of buttons, with their names specifying + the recent files. + ''' + + grid = ObjectProperty(None) + '''The grid layout consisting of all buttons. + This property is an instance of :class:`~kivy.uix.gridlayout` + :data:`grid` is a :class:`~kivy.properties.ObjectProperty` + ''' + + text = ObjectProperty(None) + '''The grid layout consisting of all buttons. + This property is an instance of :class:`~kivy.uix.gridlayout` + :data:`grid` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(ProjectTemplateBox, self).__init__(**kwargs) + + def add_template(self): + '''To add buttons representing Recent Files. + :param list_files: array of paths + ''' + item_strings = list(NEW_PROJECTS.keys()) + item_strings.sort() + for p in item_strings: + recent_item = Factory.DesignerListItemButton(text=p) + self.grid.add_widget(recent_item) + recent_item.bind(on_press=self.btn_release) + self.grid.height += recent_item.height + + self.grid.height = max(self.grid.height, self.height) + self.grid.children[-1].trigger_action() + + def btn_release(self, instance): + '''Event Handler for 'on_release' of an event. + ''' + self.text = instance.text + self.parent.update_template_preview(instance) + + +class NewProjectDialog(BoxLayout): + select_button = ObjectProperty(None) + ''':class:`~kivy.uix.button.Button` used to select the list item. + :data:`select_button` is a :class:`~kivy.properties.ObjectProperty` + ''' + + cancel_button = ObjectProperty(None) + ''':class:`~kivy.uix.button.Button` to cancel the dialog. + :data:`cancel_button` is a :class:`~kivy.properties.ObjectProperty` + ''' + + template_preview = ObjectProperty(None) + '''Type of :class:`~kivy.uix.image.Image` to display preview of selected + new template. + :data:`template_preview` is a :class:`~kivy.properties.ObjectProperty` + ''' + + template_list = ObjectProperty(None) + '''Type of :class:`ProjectTemplateBox` used for showing template available. + :data:`template_list` is a :class:`~kivy.properties.ObjectProperty` + ''' + + app_name = ObjectProperty(None) + '''Type of :class:`ProjectTemplateBox` used for showing template available. + :data:`template_list` is a :class:`~kivy.properties.ObjectProperty` + ''' + + package_name = ObjectProperty(None) + '''Type of :class:`ProjectTemplateBox` used for showing template available. + :data:`template_list` is a :class:`~kivy.properties.ObjectProperty` + ''' + + package_version = ObjectProperty(None) + '''Type of :class:`ProjectTemplateBox` used for showing template available. + :data:`template_list` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_select', 'on_cancel') + + def __init__(self, **kwargs): + super(NewProjectDialog, self).__init__(**kwargs) + self.template_list.add_template() + self.app_name.bind(text=self.on_app_name_text) + self.app_name.text = "My Application" + self.package_version.text = "0.1.dev0" + + def update_template_preview(self, instance): + '''Event handler for 'on_selection_change' event of adapter. + ''' + name = instance.text.lower() + '.png' + name = name.replace(' and ', '_') + image_source = join(get_kd_data_dir(), + constants.NEW_TEMPLATE_IMAGE_PATH, name) + self.template_preview.source = image_source + + def on_app_name_text(self, instance, value): + self.package_name.text = 'org.test.' + value.lower().replace(' ', '_') + + def on_select(self, *args): + '''Default Event Handler for 'on_select' event + ''' + pass + + def on_cancel(self, *args): + '''Default Event Handler for 'on_cancel' event + ''' + pass + + def on_select_button(self, *args): + '''Event Handler for 'on_release' of select button. + ''' + self.select_button.bind(on_press=partial(self.dispatch, 'on_select')) + + def on_cancel_button(self, *args): + '''Event Handler for 'on_release' of cancel button. + ''' + self.cancel_button.bind(on_press=partial(self.dispatch, 'on_cancel')) diff --git a/designer/components/dialogs/recent.py b/designer/components/dialogs/recent.py new file mode 100644 index 0000000..7c3d104 --- /dev/null +++ b/designer/components/dialogs/recent.py @@ -0,0 +1,92 @@ +from utils.utils import get_fs_encoding +# from kivy.adapters.listadapter import ListAdapter +from kivy.properties import ObjectProperty, partial +from kivy.uix.boxlayout import BoxLayout +# from kivy.uix.listview import ListItemButton +from kivy.uix.button import Button + +class RecentItemButton(Button): + pass + + +class ListAdapter(object): + def __init__(self, cls, data, selection_mode, allow_empty_selection, args_converter, *args, **kwwargs): + pass + +class RecentDialog(BoxLayout): + '''RecentDialog shows the list of recent files retrieved from RecentManager + It emits, 'on_select' event when a file is selected and select_button is + clicked and 'on_cancel' when cancel_button is pressed. + ''' + + listview = ObjectProperty(None) + ''':class:`~kivy.uix.listview.ListView` used for showing file paths. + :data:`listview` is a :class:`~kivy.properties.ObjectProperty` + ''' + + select_button = ObjectProperty(None) + ''':class:`~kivy.uix.button.Button` used to select the list item. + :data:`select_button` is a :class:`~kivy.properties.ObjectProperty` + ''' + + cancel_button = ObjectProperty(None) + ''':class:`~kivy.uix.button.Button` to cancel the dialog. + :data:`cancel_button` is a :class:`~kivy.properties.ObjectProperty` + ''' + + adapter = ObjectProperty(None) + ''':class:`~kivy.uix.listview.ListAdapter` used for selecting files. + :data:`adapter` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_select', 'on_cancel') + + def __init__(self, file_list, **kwargs): + super(RecentDialog, self).__init__(**kwargs) + self.item_strings = [] + for item in file_list: + if isinstance(item, bytes): + item = item.decode(get_fs_encoding()) + self.item_strings.append(item) + + self.list_items = RecentItemButton + + self.adapter = ListAdapter( + cls=self.list_items, + data=self.item_strings, + selection_mode='single', + allow_empty_selection=False, + args_converter=self._args_converter) + + self.listview.adapter = self.adapter + + def _args_converter(self, index, path): + '''Convert the item to listview + ''' + return {'text': path, 'size_hint_y': None, 'height': 40} + + def get_selected_project(self, *args): + ''' + Get the path of the selected project + ''' + return self.adapter.selection[0].text + + def on_select_button(self, *args): + '''Event handler for 'on_release' event of select_button. + ''' + self.select_button.bind(on_press=partial(self.dispatch, 'on_select')) + + def on_cancel_button(self, *args): + '''Event handler for 'on_release' event of cancel_button. + ''' + self.cancel_button.bind(on_press=partial(self.dispatch, 'on_cancel')) + + def on_select(self, *args): + '''Default event handler for 'on_select' event. + ''' + pass + + def on_cancel(self, *args): + '''Default event handler for 'on_cancel' event. + ''' + pass diff --git a/designer/components/edit_contextual_view.py b/designer/components/edit_contextual_view.py new file mode 100644 index 0000000..a2447f4 --- /dev/null +++ b/designer/components/edit_contextual_view.py @@ -0,0 +1,96 @@ +from functools import partial + +from kivy.properties import ObjectProperty +from kivy.uix.actionbar import ActionButton, ContextualActionView + + +class EditContView(ContextualActionView): + '''EditContView is a ContextualActionView, used to display Edit items: + Copy, Cut, Paste, Undo, Redo, Select All, Add Custom Widget. It has + events: + on_undo, emitted when Undo ActionButton is clicked. + on_redo, emitted when Redo ActionButton is clicked. + on_cut, emitted when Cut ActionButton is clicked. + on_copy, emitted when Copy ActionButton is clicked. + on_paste, emitted when Paste ActionButton is clicked. + on_delete, emitted when Delete ActionButton is clicked. + on_selectall, emitted when Select All ActionButton is clicked. + on_add_custom, emitted when Add Custom ActionButton is clicked. + on_find, emitted when Find ActionButton is clicked. + ''' + + __events__ = ('on_undo', 'on_redo', 'on_cut', 'on_copy', + 'on_paste', 'on_delete', 'on_selectall', + 'on_next_screen', 'on_prev_screen', 'on_find') + + action_btn_next_screen = ObjectProperty(None, allownone=True) + action_btn_prev_screen = ObjectProperty(None, allownone=True) + action_btn_find = ObjectProperty(None, allownone=True) + + def show_action_btn_screen(self, show): + '''To add action_btn_next_screen and action_btn_prev_screen + if show is True. Otherwise not. + ''' + if self.action_btn_next_screen: + self.remove_widget(self.action_btn_next_screen) + if self.action_btn_prev_screen: + self.remove_widget(self.action_btn_prev_screen) + + self.action_btn_next_screen = None + self.action_btn_prev_screen = None + + if show: + self.action_btn_next_screen = ActionButton(text="Next Screen") + self.action_btn_next_screen.bind( + on_press=partial(self.dispatch, 'on_next_screen')) + self.action_btn_prev_screen = ActionButton(text="Previous Screen") + self.action_btn_prev_screen.bind( + on_press=partial(self.dispatch, 'on_prev_screen')) + + self.add_widget(self.action_btn_next_screen) + self.add_widget(self.action_btn_prev_screen) + + def show_find(self, show): + '''Adds the find button + ''' + if self.action_btn_find is None: + find = ActionButton(text='Find') + find.bind(on_release=partial(self.dispatch, 'on_find')) + self.action_btn_find = find + + if show: + if not self.action_btn_find in self.children: + self.add_widget(self.action_btn_find) + else: + if self.action_btn_find in self.children: + self.remove_widget(self.action_btn_find) + + def on_undo(self, *args): + pass + + def on_redo(self, *args): + pass + + def on_cut(self, *args): + pass + + def on_copy(self, *args): + pass + + def on_paste(self, *args): + pass + + def on_delete(self, *args): + pass + + def on_selectall(self, *args): + pass + + def on_next_screen(self, *args): + pass + + def on_prev_screen(self, *args): + pass + + def on_find(self, *args): + pass diff --git a/designer/components/event_viewer.py b/designer/components/event_viewer.py new file mode 100644 index 0000000..3d2081f --- /dev/null +++ b/designer/components/event_viewer.py @@ -0,0 +1,305 @@ +import re + +from components.property_viewer import PropertyLabel, PropertyViewer +from uix.info_bubble import InfoBubble +from utils.utils import get_current_project, get_designer, show_message +from kivy.clock import Clock +from kivy.properties import BooleanProperty, ObjectProperty, StringProperty +from kivy.uix.button import Button +from kivy.uix.dropdown import DropDown +from kivy.uix.textinput import TextInput + + +class EventHandlerTextInput(TextInput): + '''EventHandlerTextInput is used to display/change/remove EventHandler + for an event + ''' + + eventwidget = ObjectProperty(None) + '''Current selected widget + :data:`eventwidget` is a :class:`~kivy.properties.ObjectProperty` + ''' + + eventname = StringProperty('') + '''Name of current event + :data:`eventname` is a :class:`~kivy.properties.StringProperty` + ''' + + kv_code_input = ObjectProperty() + '''Reference to KVLangArea + :data:`kv_code_input` is a :class:`~kivy.properties.ObjectProperty` + ''' + + text_inserted = BooleanProperty(None) + '''Specifies whether text has been inserted or not + :data:`text_inserted` is a :class:`~kivy.properties.BooleanProperty` + ''' + + info_message = StringProperty(None) + '''Message to be displayed by InfoBubble + :data:`info_message` is a :class:`~kivy.properties.StringProperty` + ''' + + dropdown = ObjectProperty(None) + '''DropDown which will be displayed to show possible + functions for that event + :data:`dropdown` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def on_touch_down(self, touch): + '''Default handler for 'on_touch_down' event + ''' + if self.collide_point(*touch.pos): + self.info_bubble = InfoBubble(message=self.info_message) + bubble_pos = list(self.to_window(*self.pos)) + bubble_pos[1] += self.height + self.info_bubble.show(bubble_pos, 1.5) + + return super(EventHandlerTextInput, self).on_touch_down(touch) + + def show_drop_down_for_widget(self, widget): + '''Show all functions for a widget in a Dropdown. + ''' + self.dropdown = DropDown() + list_funcs = dir(widget) + for func in list_funcs: + if '__' not in func and hasattr(getattr(widget, func), '__call__'): + btn = Button(text=func, size_hint=(None, None), + size=(100, 30), shorten=True) + self.dropdown.add_widget(btn) + btn.bind(on_release=lambda btn: self.dropdown.select(btn.text)) + btn.text_size = [btn.size[0] - 4, btn.size[1]] + btn.valign = 'middle' + + self.dropdown.open(self) + self.dropdown.pos = (self.x, self.y) + self.dropdown.bind(on_select=self._dropdown_select) + + def _dropdown_select(self, instance, value): + '''Event handler for 'on_select' event of self.dropdown + ''' + self.text += value + + def on_text(self, instance, value): + '''Default event handler for 'on_text' + ''' + if not self.kv_code_input: + return + + d = get_designer() + playground = d.ui_creator.playground + self.kv_code_input.set_event_handler(self.eventwidget, + self.eventname, + self.text) + if self.text and self.text[-1] == '.': + if self.text == 'self.': + self.show_drop_down_for_widget(self.eventwidget) + ## TODO recursively call eventwidget.parent to get the root widget + elif self.text == 'root.': + self.show_drop_down_for_widget(playground.root) + + else: + _id = self.text.replace('.', '') + root = playground.root + widget = None + + if _id in root.ids: + widget = root.ids[_id] + + if widget: + self.show_drop_down_for_widget(widget) + + elif self.dropdown: + self.dropdown.dismiss() + + +class NewEventTextInput(TextInput): + '''NewEventTextInput is TextInput which is used to create a new event + for a widget. When event is created then on_create_event is emitted + ''' + + __events__ = ('on_create_event',) + + info_message = StringProperty(None) + '''Message which will be displayed in the InfoBubble + :data:`info_message` is a :class:`~kivy.properties.StringProperty` + ''' + + def on_create_event(self, *args): + '''Default event handler for 'on_create_event' + ''' + pass + + def on_text_validate(self): + '''Create a new event to a CustomWidget + ''' + if self.text[:3] == 'on_': + self.dispatch('on_create_event') + + def on_touch_down(self, touch): + '''Default handler for 'on_touch_down' event. + ''' + if self.collide_point(*touch.pos): + self.info_bubble = InfoBubble(message=self.info_message) + bubble_pos = list(self.to_window(*self.pos)) + bubble_pos[1] += self.height + self.info_bubble.show(bubble_pos, 1.5) + + return super(NewEventTextInput, self).on_touch_down(touch) + + +class EventLabel(PropertyLabel): + pass + + +class EventViewer(PropertyViewer): + '''EventViewer, to display all the events associated with the widget and + event handler. + ''' + + designer_tabbed_panel = ObjectProperty(None) + '''Reference to DesignerTabbedPanel + :data:`designer_tabbed_panel` is a + :class:`~kivy.properties.ObjectProperty` + ''' + + def on_widget(self, instance, value): + '''Default handler for change of 'widget' property + ''' + self.clear() + if value is not None: + self.discover(value) + + def clear(self): + '''To clear :data:`prop_list`. + ''' + self.prop_list.clear_widgets() + + def discover(self, value): + '''To discover all properties and add their + :class:`~designer.components.property_viewer.PropertyLabel` and + :class:`~designer.components.property_viewer.PropertyBoolean`/ + :class:`~designer.components.property_viewer.PropertyTextInput` + to :data:`prop_list`. + ''' + + add = self.prop_list.add_widget + events = value.events() + for event in events: + ip = self.build_for(event) + if not ip: + continue + add(EventLabel(text=event)) + add(ip) + + # check if widget has a class to add custom events + is_custom_widget = False + app_widgets = get_current_project().app_widgets + wdg_name = type(self.widget).__name__ + for wdg in app_widgets: + if wdg == wdg_name: + widget = app_widgets[wdg] + # if has a python file + if widget.py_path: + is_custom_widget = True + break + + if is_custom_widget: + # Allow adding a new event only if current widget is a custom rule + add(EventLabel(text='Type and press enter to \n' + 'create a new event')) + txt = NewEventTextInput( + multiline=False, + info_message='Type and press enter to create a new event') + txt.bind(on_create_event=self.create_event) + add(txt) + + def create_event(self, txt): + '''This function will create a new event given by 'txt' to the widget. + ''' + # Find the python file of widget + py_file = None + app_widgets = get_current_project().app_widgets + for rule_name in app_widgets: + if self.widget.__class__.__name__ == rule_name: + py_file = app_widgets[rule_name].py_path + break + + # Open it in DesignerTabbedPannel + rel_path = py_file.replace(get_current_project().path, '') + if rel_path[0] == '/' or rel_path[0] == '\\': + rel_path = rel_path[1:] + + self.designer_tabbed_panel.open_file(py_file, rel_path, + switch_to=True) + self.rel_path = rel_path + self.txt = txt + Clock.schedule_once(self._add_event) + + def _add_event(self, *args): + '''This function will create a new event given by 'txt' to the widget. + ''' + # Find the class definition + py_code_input = None + txt = self.txt + rel_path = self.rel_path + for tab_item in self.designer_tabbed_panel.tab_list: + if not hasattr(tab_item, 'rel_path'): + continue + if tab_item.rel_path == rel_path: + if hasattr(tab_item.content, 'code_input'): + py_code_input = tab_item.content.code_input + break + + if py_code_input is None: + show_message('Failed to create a custom event', 5, 'error') + return + pos = -1 + for searchiter in re.finditer(r'class\s+%s\(.+\):' % + type(self.widget).__name__, + py_code_input.text): + pos = searchiter.end() + + if pos != -1: + col, row = py_code_input.get_cursor_from_index(pos) + row += 1 + lines = py_code_input.text.splitlines() + found_events = False + events_row = row + for i in range(row, len(lines)): + if re.match(r'\s+__events__\s*=\s*\(.+\)', lines[i]): + found_events = True + events_row = i + break + + elif re.match('class\s+[\w\d\_]+\(.+\):', lines[i]): + break + + elif re.match('def\s+[\w\d\_]+\(.+\):', lines[i]): + break + + if found_events: + events_col = lines[events_row].rfind(')') - 1 + py_code_input.cursor = events_col, events_row + py_code_input.insert_text(', "%s" ' % txt.text) + + py_code_input.cursor = 0, events_row + 1 + py_code_input.insert_text( + '\n def %s(self, *args):\n pass\n' % txt.text + ) + else: + py_code_input.text = py_code_input.text[:pos] + \ + '\n\n __events__=("%s",)\n\n'\ + ' def %s(self, *args):\n pass\n' % \ + (txt.text, txt.text) +\ + py_code_input.text[pos:] + show_message('New event created!', 5, 'info') + + def build_for(self, name): + '''Creates a EventHandlerTextInput for each property given its name + ''' + text = self.kv_code_input.get_property_value(self.widget, name) + return EventHandlerTextInput( + kv_code_input=self.kv_code_input, eventname=name, + eventwidget=self.widget, multiline=False, text=text, + info_message="Set event handler for event %s" % (name)) diff --git a/designer/components/kivy_console.py b/designer/components/kivy_console.py new file mode 100644 index 0000000..97c67bf --- /dev/null +++ b/designer/components/kivy_console.py @@ -0,0 +1,904 @@ +# -*- coding: utf-8 -*- +''' +KivyConsole +=========== + +.. image:: images/KivyConsole.jpg + :align: right + +:class:`KivyConsole` is a :class:`~kivy.uix.widget.Widget` +Purpose: Providing a system console for debugging kivy by running another +instance of kivy in this console and displaying it's output. +To configure, you can use + +cached_history : +cached_commands : +font_size : +shell : + +''Versionadded:: 1.0.?TODO + +''Usage: + from kivy.uix.kivyconsole import KivyConsole + + parent.add_widget(KivyConsole()) + +or + + console = KivyConsole() + +To run a command: + + console.stdin.write('ls -l') + +or + subprocess.Popen(('echo','ls'), stdout = console.stdin) + +To display something on stdout write to stdout + + console.stdout.write('this will be written to the stdout\n') + +or + subprocess.Popen('ps', stdout = console.stdout, shell = True) + +Warning: To read from stdout remember that the process is run in a thread, give +it time to complete otherwise you might get a empty or partial string; +returning whatever has been written to the stdout pipe till the time +read() was called. + + text = console.stdout.read() or read(no_of_bytes) or readline() + +TODO: create a stdin and stdout pipe for + this console like in logger.[==== ]%done +TODO: move everything that is non-specific to + a generic console in a different Project.[ ]%done +TODO: Fix Prompt, make it smaller plus give it more info + +''Shortcuts: +Inside the console you can use the following shortcuts: +Shortcut Function +_________________________________________________________ +PGup Search for previous command inside command history + starting with the text before current cursor position + +PGdn Search for Next command inside command history + starting with the text before current cursor position + +UpArrow Replace command_line with previous command + +DnArrow Replace command_line with next command + (only works if one is not at last command) + +Tab If there is nothing before the cursur when tab is pressed + contents of current directory will be displayed. + '.' before cursur will be converted to './' + '..' to '../' + If there is a path before cursur position + contents of the path will be displayed. + else contents of the path before cursor containing + the commands matching the text before cursur will + be displayed +''' +__all__ = ('KivyConsole', ) + +import os +import re +import shlex +import subprocess +import sys +from functools import partial + +from utils.utils import get_fs_encoding +from kivy.app import runTouchApp +from kivy.clock import Clock +from kivy.compat import PY2 +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.properties import ( + BooleanProperty, + DictProperty, + ListProperty, + NumericProperty, + ObjectProperty, +) +from kivy.uix.button import Button +from kivy.uix.gridlayout import GridLayout +from kivy.uix.textinput import TextInput +from kivy.utils import platform +from pygments.lexers.shell import BashSessionLexer + + +try: + import thread +except ImportError: + import _thread as thread + + +Builder.load_string(''' +: + cols:1 + txtinput_history_box: history_box.__self__ + txtinput_command_line: command_line.__self__ + ScrollView: + bar_width: 10 + scroll_type: ['bars', 'content'] + CodeInput: + id: history_box + size_hint: (1, None) + height: 801 + font_size: root.font_size + readonly: True + foreground_color: root.foreground_color + background_color: root.background_color + TextInput: + id: command_line + multiline: False + size_hint: (1, None) + font_size: root.font_size + readonly: root.readonly + foreground_color: root.foreground_color + background_color: root.background_color + height: 36 + on_text_validate: root.on_enter(*args) + on_touch_up: + self.collide_point(*args[1].pos)\\ + and root._move_cursor_to_end(self) +''') + + +class KivyConsole(GridLayout): + '''This is a Console widget used for debugging and running external + commands + + ''' + + readonly = BooleanProperty(False) + '''This defines whether a person can enter commands in the console + + :data:`readonly` is an :class:`~kivy.properties.BooleanProperty`, + Default to 'False' + ''' + + foreground_color = ListProperty((1, 1, 1, 1)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(1, 1, 1, 1)' + ''' + + background_color = ListProperty((0, 0, 0, 1)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(0, 0, 0, 1)' + ''' + + cached_history = NumericProperty(200) + '''Indicates the No. of lines to cache. Defaults to 200 + + :data:`cached_history` is an :class:`~kivy.properties.NumericProperty`, + Default to '200' + ''' + + cached_commands = NumericProperty(90) + '''Indicates the no of commands to cache. Defaults to 90 + + :data:`cached_commands` is a :class:`~kivy.properties.NumericProperty`, + Default to '90' + ''' + + environment = DictProperty(os.environ.copy()) + '''Indicates the environment the commands are run in. Set your PATH or + other environment variables here. like so:: + + kivy_console.environment['PATH']='path' + + environment is :class:`~kivy.properties.DictProperty`, defaults to + the environment for the process running Kivy console + ''' + + font_size = NumericProperty(14) + '''Indicates the size of the font used for the console + + :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, + Default to '9' + ''' + + textcache = ListProperty(['', ]) + '''Indicates the cache of the commands and their output + + :data:`textcache` is a :class:`~kivy.properties.ListProperty`, + Default to '' + ''' + + shell = BooleanProperty(False) + '''Indicates the whether system shell is used to run the commands + + :data:`shell` is a :class:`~kivy.properties.BooleanProperty`, + Default to 'False' + + WARNING: Shell = True is a security risk and therefore = False by default, + As a result with shell = False some shell specific commands and + redirections + like 'ls |grep lte' or dir >output.txt will not work. + If for some reason you need to run such commands, try running the platform + shell first + eg: /bin/sh ...etc on nix platforms and cmd.exe on windows. + As the ability to interact with the running command is built in, + you should be able to interact with the native shell. + + Shell = True, should be set only if absolutely necessary. + ''' + + txtinput_command_line = ObjectProperty(None) + + def __init__(self, **kwargs): + self.register_event_type('on_subprocess_done') + self.register_event_type('on_command_list_done') + super(KivyConsole, self).__init__(**kwargs) + # initialisations + self.txtinput_command_line_refocus = False + self.txtinput_run_command_refocus = False + self.win = None + self.scheduled = False + self.command_history = [] + self.command_history_pos = 0 + self.command_status = 'closed' + if sys.version_info >= (3, 0): + self.cur_dir = os.getcwd() + else: + self.cur_dir = os.getcwdu() + self.command_list = [] # list of cmds to be executed + self.stdout = std_in_out(self, 'stdout') + self.stdin = std_in_out(self, 'stdin') + self.popen_obj = None + # self.stderror = stderror(self) + # delayed initialisation + Clock.schedule_once(self._initialize) + _trig = Clock.create_trigger(self._change_txtcache) + Clock.schedule_once(_trig) + self.bind(textcache=_trig) + self._hostname = 'unknown' + try: + if hasattr(os, 'uname'): + self._hostname = os.uname()[1] + else: + self._hostname = os.environ.get('COMPUTERNAME', 'unknown') + except Exception: + pass + self._username = os.environ.get('USER', '') + if not self._username: + self._username = os.environ.get('USERNAME', 'unknown') + + def run_command(self, command, *args): + '''Run a command using Kivy Console. + The output will be visible in the Kivy console. + Returns False if there is a command running and cancel the execution. + Otherwise, start the execution of the commands and returns True + :param command: + @string with a shell command to be executed + @list array with command strings + ''' + # if there is a command running + if self.popen_obj: + return False + + if isinstance(command, list): + self.command_list = command + else: + self.command_list = [command] + self._run_command_list() + + return True + + def _run_command_list(self, *args): + '''Runs a list of commands + ''' + if self.command_list: + self.stdin.write(self.command_list.pop(0)) + self.bind(on_subprocess_done=self._run_command_list) + else: + self.dispatch('on_command_list_done') + + def clear(self, *args): + '''Clear the Kivy Console area + ''' + self.txtinput_history_box.text = '' + self.textcache = [''] + + def _initialize(self, dt): + '''Set console default variable values + ''' + self.txtinput_history_box.lexer = BashSessionLexer() + self.txtinput_history_box.text = ''.join(self.textcache) + self.txtinput_command_line.text = self.prompt() + self.txtinput_command_line.bind(focus=self.on_focus) + self.txtinput_command_line.bind( + selection_text=self.on_txtinput_selection) + self._focus(self.txtinput_command_line) + + def on_txtinput_selection(self, *args): + '''Callback to command input text selection. + Cannot select the PS1 variable, so it'll handle to select only + input text. + ''' + ticl = self.txtinput_command_line + col = len(self.prompt()) + ticl.select_text(col, len(ticl.text)) + + def _move_cursor_to_end(self, instance): + '''Moves the command input cursor to the end + ''' + def mte(*l): + instance.cursor = instance.get_cursor_from_index(len_prompt) + len_prompt = len(self.prompt()) + if instance.cursor[0] < len_prompt: + Clock.schedule_once(mte, -1) + + def _focus(self, widg, t_f=True): + Clock.schedule_once(partial(self._deffered_focus, widg, t_f)) + + def _deffered_focus(self, widg, t_f, dt): + if widg.get_root_window(): + widg.focus = t_f + + def prompt(self, *args): + '''Returns the PS1 variable + ''' + p = "[%s@%s %s]$ " % ( + self._username, self._hostname, + os.path.basename(self.cur_dir.encode('utf-8'))) + if isinstance(p, bytes): + p = p.decode(get_fs_encoding()) + return p + + def _change_txtcache(self, *args): + '''Update the Kivy Console output area + ''' + try: + # if passing the text cache limit, omit it + self.textcache = self.textcache[-self.cached_history:] + except IndexError: + pass + for i in range(len(self.textcache)): + if PY2 and isinstance(self.textcache[i], unicode): + self.textcache[i] = self.textcache[i].encode('utf-8') + tihb = self.txtinput_history_box + tihb.text = ''.join(self.textcache) + if not self.get_root_window(): + return + tihb.height = max(tihb.minimum_height, tihb.parent.height) + tihb.parent.scroll_y = 0 + + def on_key_down(self, *l): + '''Handle the on_key_down from keyboard + ''' + ticl = self.txtinput_command_line + + def move_cursor_to(col): + '''Update the cursor position + ''' + ticl.cursor =\ + col, ticl.cursor[1] + + def search_history(up_dn): + if up_dn == 'up': + plus_minus = -1 + else: + plus_minus = 1 + l_curdir = len(self.prompt()) + col = ticl.cursor_col + command = ticl.text[l_curdir: col] + max_len = len(self.command_history) - 1 + chp = self.command_history_pos + + while max_len >= 0: + if plus_minus == 1: + if self.command_history_pos > max_len - 1: + self.command_history_pos = max_len + return + else: + if self.command_history_pos <= 0: + self.command_history_pos = max_len + return + self.command_history_pos = self.command_history_pos\ + + plus_minus + cmd = self.command_history[self.command_history_pos] + if cmd[:len(command)] == command: + ticl.text = ''.join(( + self.prompt(), cmd)) + move_cursor_to(col) + return + self.command_history_pos = max_len + 1 + + if ticl.focus: + if l[1] == 273: + # up arrow: display previous command + if self.command_history_pos > 0: + self.command_history_pos -= 1 + ticl.text = ''.join( + (self.prompt(), + self.command_history[self.command_history_pos])) + return + if l[1] == 274: + # dn arrow: display next command + if self.command_history_pos < len(self.command_history) - 1: + self.command_history_pos += 1 + ticl.text = ''.join( + (self.prompt(), + self.command_history[self.command_history_pos])) + else: + self.command_history_pos = len(self.command_history) + ticl.text = self.prompt() + col = len(ticl.text) + move_cursor_to(col) + return + if l[1] == 9: + # tab: autocomplete + def display_dir(cur_dir, starts_with=None): + # display contents of dir from cur_dir variable + try: + dir_list = os.listdir(cur_dir) + except OSError as err: + self.add_to_cache(''.join((err.strerror, '\n'))) + return + if starts_with is not None: + len_starts_with = len(starts_with) + self.add_to_cache( + 'contents of directory: ' + cur_dir + '\n') + txt = '' + no_of_matches = 0 + for _file in dir_list: + if starts_with is not None: + if _file[:len_starts_with] == starts_with: + # if file matches starts with + txt = ''.join((txt, _file, '\t')) + no_of_matches += 1 + else: + self.add_to_cache(''.join((_file, '\t'))) + if no_of_matches == 1: + len_txt = len(txt) - 1 + cmdl_text = ticl.text + len_cmdl = len(cmdl_text) + os_sep = os.sep \ + if col == len_cmdl or (col < len_cmdl and + cmdl_text[col] != + os.sep) else '' + ticl.text = ''.join( + (self.prompt(), text_before_cursor, + txt[len_starts_with:len_txt], os_sep, + cmdl_text[col:])) + move_cursor_to(col + (len_txt - len_starts_with) + 1) + elif no_of_matches > 1: + self.add_to_cache(txt) + self.add_to_cache('\n') + + # send back space to command line -remove the tab + Clock.schedule_once(ticl.do_backspace, 0) + l_curdir = len(self.prompt()) + move_cursor_to(l_curdir + len(ticl.text)) + ntext = os.path.expandvars(ticl.text) + # store text before cursor for comparison + col = ticl.cursor_col + if ntext != ticl.text: + ticl.text = ntext + col = len(ntext) + text_before_cursor = ticl.text[l_curdir: col] + + # if empty or space before: list cur dir + if text_before_cursor == ''\ + or ticl.text[col - 1] == ' ': + display_dir(self.cur_dir) + # if in mid command: + else: + # list commands in PATH starting with text before cursor + # split command into path till the seperator + cmd_start = text_before_cursor.rfind(' ') + cmd_start += 1 + cur_dir = self.cur_dir\ + if text_before_cursor[cmd_start] != os.sep\ + else os.sep + os_sep = os.sep if cur_dir != os.sep else '' + cmd_end = text_before_cursor.rfind(os.sep) + len_txt_bef_cur = len(text_before_cursor) - 1 + if cmd_end == len_txt_bef_cur: + # display files in path + if text_before_cursor[cmd_start] == os.sep: + cmd_start += 1 + display_dir(''.join((cur_dir, os_sep, + text_before_cursor[cmd_start:cmd_end]))) + elif text_before_cursor[len_txt_bef_cur] == '.': + # if / already there return + if len(ticl.text) > col\ + and ticl.text[col] == os.sep: + return + if text_before_cursor[len_txt_bef_cur - 1] == '.': + len_txt_bef_cur -= 1 + if text_before_cursor[len_txt_bef_cur - 1]\ + not in (' ', os.sep): + return + # insert at cursor os.sep: / or \ + ticl.text = ''.join( + (self.prompt(), + text_before_cursor, os_sep, + ticl.text[col:])) + else: + if cmd_end < 0: + cmd_end = cmd_start + else: + cmd_end += 1 + display_dir(''.join(( + cur_dir, + os_sep, + text_before_cursor[cmd_start:cmd_end])), + text_before_cursor[cmd_end:]) + return + if l[1] == 280: + # pgup: search last command starting with... + search_history('up') + return + if l[1] == 281: + # pgdn: search next command starting with... + search_history('dn') + return + if l[1] == 278: + # Home: cursor should not go to the left of cur_dir + col = len(self.prompt()) + move_cursor_to(col) + if len(l[4]) > 0 and l[4][0] == 'shift': + ticl.selection_to = col + return + if l[1] == 276 or l[1] == 8: + # left arrow/bkspc: cursor should not go left of cur_dir + col = len(self.prompt()) + if ticl.cursor_col < col: + if l[1] == 8: + ticl.text = self.prompt() + move_cursor_to(col) + return + + def on_focus(self, instance, value): + '''Handle the focus on the command input + ''' + if value: + # focused + if instance is self.txtinput_command_line: + Window.unbind(on_key_down=self.on_key_down) + Window.bind(on_key_down=self.on_key_down) + else: + # defocused + Window.unbind(on_key_down=self.on_key_down) + if self.txtinput_command_line_refocus: + self.txtinput_command_line_refocus = False + if self.txtinput_command_line.get_root_window(): + self.txtinput_command_line.focus = True + self.txtinput_command_line.scroll_x = 0 + if self.txtinput_run_command_refocus: + self.txtinput_run_command_refocus = False + instance.focus = True + instance.scroll_x = 0 + instance.text = '' + + def add_to_cache(self, _string): + self.textcache.append(_string) + + def kill_process(self, *args): + '''Kill the current process + ''' + if self.popen_obj: + self.popen_obj.kill() + + def on_enter(self, *args): + '''When the user press enter and wants to run a command + ''' + self.unbind(on_subprocess_done=self.on_enter) + # if there is a command running, it's necessary to stop it first + if self.command_status == 'started': + self.kill_process() + # restart this call after killing the running process + self.bind(on_subprocess_done=self.on_enter) + return + + txtinput_command_line = self.txtinput_command_line + add_to_cache = self.add_to_cache + command_history = self.command_history + + def remove_command_interaction_widgets(*args): + '''command finished: remove widget responsible for interaction + ''' + parent.remove_widget(self.interact_layout) + self.interact_layout = None + # enable running a new command + try: + parent.add_widget(self.txtinput_command_line) + except: + self._initialize(0) + + self._focus(txtinput_command_line, True) + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + + def run_cmd(*args): + '''Run the command + ''' + # this is run inside a thread so take care, avoid gui ops + try: + _posix = True + if sys.platform[0] == 'w': + _posix = False + cmd = command + if PY2 and isinstance(cmd, unicode): + cmd = command.encode(get_fs_encoding()) + if not self.shell: + cmd = shlex.split(cmd, posix=_posix) + for i in range(len(cmd)): + cmd[i] = cmd[i].replace('\x01', ' ') + map(lambda s: s.decode(get_fs_encoding()), cmd) + except Exception as err: + cmd = '' + self.add_to_cache(''.join((str(err), ' <', command, ' >\n'))) + if len(cmd) > 0: + prev_stdout = sys.stdout + sys.stdout = self.stdout + try: + # execute command + self.popen_obj = popen = subprocess.Popen( + cmd, + bufsize=0, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, + preexec_fn=None, + close_fds=False, + shell=self.shell, + cwd=self.cur_dir.encode(get_fs_encoding()), + universal_newlines=False, + startupinfo=None, + creationflags=0) + popen_stdout_r = popen.stdout.readline + popen_stdout_flush = popen.stdout.flush + txt = popen_stdout_r() + plat = platform() + while txt: + # skip flush on android + if plat[0] != 'a': + popen_stdout_flush() + + if isinstance(txt, bytes): + txt = txt.decode(get_fs_encoding()) + add_to_cache(txt) + txt = popen_stdout_r() + except (OSError, ValueError) as err: + add_to_cache(''.join( + (str(err), + ' < ', command, ' >\n'))) + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + sys.stdout = prev_stdout + self.popen_obj = None + Clock.schedule_once(remove_command_interaction_widgets, 0) + + # append text to textcache + add_to_cache(self.txtinput_command_line.text + '\n') + command = txtinput_command_line.text[len(self.prompt()):] + + if command == '': + self.txtinput_command_line_refocus = True + return + + # store command in command_history + if self.command_history_pos > 0: + self.command_history_pos = len(command_history) + if command_history[self.command_history_pos - 1] != command: + command_history.append(command) + else: + command_history.append(command) + + len_command_history = len(command_history) + self.command_history_pos = len(command_history) + + # on reaching limit(cached_lines) pop first command + if len_command_history >= self.cached_commands: + self.command_history = command_history[1:] + + # replace $PATH with + command = os.path.expandvars(command) + + if command == 'clear' or command == 'cls': + self.clear() + txtinput_command_line.text = self.prompt() + self.txtinput_command_line_refocus = True + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + return + # if command = cd change directory + if command.startswith('cd ') or command.startswith('export '): + if command[0] == 'e': + e_q = command[7:].find('=') + _exprt = command[7:] + if e_q: + os.environ[_exprt[:e_q]] = _exprt[e_q + 1:] + self.environment = os.environ.copy() + else: + try: + command = re.sub('[ ]+', ' ', command) + if command[3] == os.sep: + os.chdir(command[3:]) + else: + os.chdir(os.path.join(self.cur_dir, command[3:])) + if sys.version_info >= (3, 0): + self.cur_dir = os.getcwd() + else: + self.cur_dir = os.getcwdu() + except OSError as err: + Logger.debug('Shell Console: err:' + err.strerror + + ' directory:' + str(command[3:])) + add_to_cache(''.join((err.strerror, '\n'))) + txtinput_command_line.text = self.prompt() + self.txtinput_command_line_refocus = True + self.command_status = 'closed' + self.dispatch('on_subprocess_done') + return + + txtinput_command_line.text = self.prompt() + # store output in textcache + parent = txtinput_command_line.parent + # disable running a new command while and old one is running + parent.remove_widget(txtinput_command_line) + # add widget for interaction with the running command + txtinput_run_command = TextInput(multiline=False, + font_size=self.font_size) + + def interact_with_command(*l): + '''Text input to interact with the running command + ''' + popen_obj = self.popen_obj + if not popen_obj: + return + txt = l[0].text + '\n' + popen_obj_stdin = popen_obj.stdin + popen_obj_stdin.write(txt) + popen_obj_stdin.flush() + self.txtinput_run_command_refocus = True + + self.txtinput_run_command_refocus = False + txtinput_run_command.bind(on_text_validate=interact_with_command) + txtinput_run_command.bind(focus=self.on_focus) + btn_kill = Button(text="Stop", + width=60, + size_hint=(None, 1)) + + self.interact_layout = il = GridLayout(rows=1, cols=2, height=27, + size_hint=(1, None)) + btn_kill.bind(on_press=self.kill_process) + il.add_widget(txtinput_run_command) + il.add_widget(btn_kill) + parent.add_widget(il) + + txtinput_run_command.focus = True + self.command_status = 'started' + thread.start_new_thread(run_cmd, ()) + + def on_subprocess_done(self, *args): + '''Event handler for when a process was killed + or just finished the execution. + ''' + pass + + def on_command_list_done(self, *args): + '''Event handler for when the whole command list was executed or killed + ''' + pass + + +class std_in_out(object): + ''' class for writing to/reading from this console''' + + def __init__(self, obj, mode='stdout'): + self.obj = obj + self.mode = mode + self.stdin_pipe, self.stdout_pipe = os.pipe() + thread.start_new_thread(self.read_from_in_pipe, ()) + self.textcache = None + + def update_cache(self, text_line, *l): + '''Update the output text area + ''' + self.obj.textcache.append(text_line) + + def read_from_in_pipe(self, *l): + '''Read the output from the command + ''' + txt = '\n' + txt_line = '' + try: + while txt != '': + txt = os.read(self.stdin_pipe, 1) + txt_line += txt + if txt == '\n': + if self.mode == 'stdin': + # run command + self.write(txt_line) + else: + Clock.schedule_once( + partial(self.update_cache, txt_line), 0) + self.flush() + txt_line = '' + except OSError as e: + Logger.exception(e) + + def close(self): + '''Close the pipes + ''' + os.close(self.stdin_pipe) + os.close(self.stdout_pipe) + + def __del__(self): + self.close() + + def fileno(self): + return self.stdout_pipe + + def write(self, s): + '''Write a command to the pipe + ''' + if isinstance(s, bytes): + s = s.decode(get_fs_encoding()) + Logger.debug('write called with command:' + s) + if self.mode == 'stdout': + self.obj.add_to_cache(s) + self.flush() + else: + # process.stdout.write ...run command + if self.mode == 'stdin': + self.obj.txtinput_command_line.text = ''.join(( + self.obj.prompt(), s)) + self.obj.on_enter() + + def read(self, no_of_bytes=0): + if self.mode == 'stdin': + # stdin.read + Logger.exception('KivyConsole: can not read from a stdin pipe') + return + # process.stdout/in.read + txtc = self.textcache + if no_of_bytes == 0: + # return all data + if txtc is None: + self.flush() + while self.obj.command_status != 'closed': + pass + txtc = self.textcache + return txtc + try: + self.textcache = txtc[no_of_bytes:] + except IndexError: + self.textcache = txtc + return txtc[:no_of_bytes] + + def readline(self): + if self.mode == 'stdin': + # stdin.readline + Logger.exception('KivyConsole: can not read from a stdin pipe') + return + else: + # process.stdout.readline + if self.textcache is None: + self.flush() + txt = self.textcache + x = txt.find('\n') + if x < 0: + Logger.Debug('console_shell: no more data') + return + self.textcache = txt[x:] + # ##self. write to ... + return txt[:x] + + def flush(self): + self.textcache = ''.join(self.obj.textcache) + return + + +if __name__ == '__main__': + runTouchApp(KivyConsole()) diff --git a/designer/components/kv_lang_area.py b/designer/components/kv_lang_area.py new file mode 100644 index 0000000..ead6015 --- /dev/null +++ b/designer/components/kv_lang_area.py @@ -0,0 +1,828 @@ +import re + +from uix.code_input import DesignerCodeInput +from utils.utils import ( + get_current_project, + get_indent_str, + get_indentation, + get_line_end_pos, + get_line_start_pos, +) +from kivy.clock import Clock +from kivy.properties import BooleanProperty, ObjectProperty, StringProperty +from kivy.uix.carousel import Carousel +from kivy.uix.screenmanager import ScreenManager +from kivy.uix.scrollview import ScrollView +from kivy.uix.tabbedpanel import ( + TabbedPanel, + TabbedPanelContent, + TabbedPanelHeader, +) + + +class KVLangAreaScroll(ScrollView): + '''KVLangAreaScroll used as a :class:`~kivy.scrollview.ScrollView` + for adding :class:`~designer.components.kv_lang_area.KVLangArea`. + ''' + + kv_lang_area = ObjectProperty(None) + '''(internal) Reference to the + :class:`~designer.uix.kv_lang_area.KVLangArea`. + :data:`kv_lang_area` is a :class:`~kivy.properties.ObjectProperty` + ''' + + line_number = ObjectProperty(None) + '''(internal) Text Input to display line numbers + :data:`line_number` is a :class:`~kivy.properties.ObjectProperty` + ''' + + show_line_number = BooleanProperty(True) + '''Display line number on left + :data:`show_line_number` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True + ''' + + def __init__(self, **kwargs): + super(KVLangAreaScroll, self).__init__(**kwargs) + + # the maximum number of lines achieved in this scroller + self._max_num_of_lines = 0 + # identify if the line number binding is already running + self._line_number_handled = False + + def on_width(self, *args): + # runs on width, when it's added to the uicreator + if not self._line_number_handled: + # just handle it once + if self.show_line_number: + self.kv_lang_area.bind(_lines=self.on_lines_changed) + else: + self.line_number.parent.remove_widget(self.line_number) + self._line_number_handled = True + + def on_lines_changed(self, *args): + '''Event handler that listen the line modifications to update + line_number + ''' + n = len(self.kv_lang_area._lines) + if n > self._max_num_of_lines: + self.update_line_number(self._max_num_of_lines, n) + + def update_line_number(self, old, new): + '''Analyze the difference between old and new number of lines + to update the text input + ''' + self._max_num_of_lines = new + # generate the new line labels + self.line_number.text += '\n'.join( + [str(i) for i in range(old + 1, new + 1)]) + '\n' + self.line_number.width = self.line_number._label_cached.get_extents( + str(self._max_num_of_lines))[0] + (self.line_number.padding[0] * 2) + # not removing lines, as long as extra lines will not be visible + + +class KVLangArea(DesignerCodeInput): + '''KVLangArea is the CodeInput for editing kv lang. It emits on_show_edit + event, when clicked. + ''' + + have_error = BooleanProperty(False) + '''This property specifies whether KVLangArea has encountered an error + in reload in the edited text by user or not. + :data:`can_place` is a :class:`~kivy.properties.BooleanProperty` + ''' + + _reload = BooleanProperty(False) + '''Specifies whether to reload kv or not. + :data:`_reload` is a :class:`~kivy.properties.BooleanProperty` + ''' + + playground = ObjectProperty() + '''Reference to :class:`~designer.components.playground.Playground` + :data:`playground` is a :class:`~kivy.properties.ObjectProperty` + ''' + + project = ObjectProperty() + '''Reference to :class:`~designer.core.project_manager.Project` + :data:`project` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_reload_kv', ) + + def __init__(self, **kwargs): + super(KVLangArea, self).__init__(**kwargs) + self._reload_trigger = Clock.create_trigger(self.func_reload_kv, 1) + self.bind(text=self._reload_trigger) + + def get_widget_path(self, widget): + '''To get path of a widget, path of a widget is a list containing + the index of it in its parent's children list. For example, + Widget1: + Widget2: + Widget3: + Widget4: + + path of Widget4 is [0, 1, 0] + see `tests/test_kv_lang_area` for more examples + :param widget: widget to get its path + ''' + + path_to_widget = [] + _widget = widget + while _widget and _widget != self.playground.sandbox.children[0]: + if not _widget.parent: + break + + if isinstance(_widget.parent.parent, Carousel): + parent = _widget.parent + try: + place = parent.parent.slides.index(_widget) + + except ValueError: + place = 0 + + path_to_widget.append(place) + _widget = _widget.parent.parent + + elif isinstance(_widget.parent, ScreenManager): + parent = _widget.parent + try: + place = parent.screens.index(_widget) + + except ValueError: + place = 0 + + path_to_widget.append(place) + _widget = _widget.parent + + elif isinstance(_widget.parent, TabbedPanelContent): + tab_panel = _widget.parent.parent + path_to_widget.append(0) + place = len(tab_panel.tab_list) - \ + tab_panel.tab_list.index(tab_panel.current_tab) - 1 + + path_to_widget.append(place) + _widget = tab_panel + + elif isinstance(_widget, TabbedPanelHeader): + tab_panel = _widget.parent + while tab_panel and not isinstance(tab_panel, TabbedPanel): + tab_panel = tab_panel.parent + + place = len(tab_panel.tab_list) - \ + tab_panel.tab_list.index(_widget) - 1 + + path_to_widget.append(place) + _widget = tab_panel + + else: + place = len(_widget.parent.children) - \ + _widget.parent.children.index(_widget) - 1 + + path_to_widget.append(place) + _widget = _widget.parent + + return path_to_widget + + def shift_widget(self, widget, from_index): + '''This function will shift widget's kv str from one position + to another. + :param from_index: original index of widget before moving + :param widget: shifted widget + ''' + self._reload = False + + path = self.get_widget_path(widget) + path.reverse() + # copies the original path + prev_path = list(path) + # get the path before shifting the widget + prev_path[-1] = len(widget.parent.children) - from_index - 1 + start_pos, end_pos = self.get_widget_text_pos_from_kv( + widget, widget.parent, path_to_widget=prev_path) + + widget_text = self.text[start_pos:end_pos] + + if widget.parent.children.index(widget) == 0: + self.text = self.text[:start_pos] + self.text[end_pos:] + self.add_widget_to_parent(widget, widget.parent, + kv_str=widget_text) + + else: + self.text = self.text[:start_pos] + self.text[end_pos:] + text = re.sub(r'#.+', '', self.text) + lines = text.splitlines() + total_lines = len(lines) + root_lineno = 0 + root_name = self.playground.root_name + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + next_widget_path = path + lineno = self._find_widget_place(next_widget_path, lines, + total_lines, + root_lineno + 1) + + self.cursor = (0, lineno) + self.insert_text(widget_text + '\n') + + def add_widget_to_parent(self, widget, target, kv_str=''): + '''This function is called when widget is added to target. + It will search for line where parent is defined in text and will add + widget there. + ''' + text = re.sub(r'#.+', '', self.text) + lines = text.splitlines() + total_lines = len(lines) + if total_lines == 0: + return + + self._reload = False + + # If target is not none then widget is not root widget + if target: + path_to_widget = self.get_widget_path(target) + + path_to_widget.reverse() + + root_lineno = 0 + root_name = self.playground.root_name + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + parent_lineno = self._find_widget_place(path_to_widget, lines, + total_lines, + root_lineno + 1) + + if parent_lineno >= total_lines: + return + + # Get text of parents line + parent_line = lines[parent_lineno] + if not parent_line.strip(): + return + + insert_after_line = -1 + + if parent_line.find(':') == -1: + # If parent_line doesn't contain ':' then insert it + # Also insert widget's rule after its properties + insert_after_line = parent_lineno + _line = 0 + _line_pos = -1 + _line_pos = self.text.find('\n', _line_pos + 1) + + while _line <= insert_after_line: + _line_pos = self.text.find('\n', _line_pos + 1) + _line += 1 + + self.text = self.text[:_line_pos] + ':' + self.text[_line_pos:] + indent = len(parent_line) - len(parent_line.lstrip()) + + else: + # If ':' in parent_line then, + # find a place to insert widget's rule + indent = len(parent_line) - len(parent_line.lstrip()) + lineno = parent_lineno + _indent = indent + 1 + line = parent_line + while (line.strip() == '' or _indent > indent): + lineno += 1 + if lineno >= total_lines: + break + line = lines[lineno] + _indent = len(line) - len(line.lstrip()) + + insert_after_line = lineno - 1 + line = lines[insert_after_line] + while line.strip() == '': + insert_after_line -= 1 + line = lines[insert_after_line] + + to_insert = '' + # counts indentation in the beginning of the string + extra_indent = len(kv_str) - len(kv_str.lstrip()) + if kv_str == '': + to_insert = type(widget).__name__ + ':' + else: + to_insert = kv_str + + if insert_after_line == total_lines - 1: + # if inserting at the last line + _line_pos = len(self.text) - 1 + indent = get_indent_str(indent + 4 - extra_indent) + to_add = '' + for line in to_insert.splitlines(): + to_add += '\n' + indent + line + self.text = self.text[:_line_pos + 1] + to_add + else: + # inserting somewhere else + insert_after_line -= 1 + _line = 0 + _line_pos = -1 + _line_pos = self.text.find('\n', _line_pos + 1) + while _line <= insert_after_line: + _line_pos = self.text.find('\n', _line_pos + 1) + _line += 1 + + self.text = self.text[:_line_pos] + '\n' + \ + get_indent_str(indent + 4) + to_insert + \ + self.text[_line_pos:] + + else: + # widget is a root widget + parent_lineno = 0 + self.cursor = (0, 0) + type_name = type(widget).__name__ + is_class = False + app_widgets = get_current_project().app_widgets + for rule_name in app_widgets: + if rule_name == type_name: + is_class = True + break + + if not is_class: + self.insert_text(type_name + ':\n') + + self.playground.load_widget(type_name) + + def get_widget_text_pos_from_kv(self, widget, parent=None, + path_to_widget=None): + '''To get start and end pos of widget's rule in kv text + :param path_to_widget: array with widget path + :param parent: parent of widget + :param widget: widget to find the kv text + ''' + if not path_to_widget: + path_to_widget = self.get_widget_path(widget) + path_to_widget.reverse() + + # Go to widget's rule's line and determines all its rule's + # and it's child if any. Then delete them + text = re.sub(r'#.+', '', self.text) + lines = text.splitlines() + total_lines = len(lines) + root_lineno = 0 + root_name = self.playground.root_name + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + widget_lineno = self._find_widget_place(path_to_widget, lines, + total_lines, root_lineno + 1) + widget_line = lines[widget_lineno] + indent = len(widget_line) - len(widget_line.lstrip()) + lineno = widget_lineno + _indent = indent + 1 + line = widget_line + while line.strip() == '' or _indent > indent: + lineno += 1 + if lineno >= total_lines: + break + line = lines[lineno] + _indent = len(line) - len(line.lstrip()) + + delete_until_line = lineno - 1 + line = lines[delete_until_line] + while line.strip() == '': + delete_until_line -= 1 + line = lines[delete_until_line] + + widget_line_pos = get_line_start_pos(self.text, widget_lineno) + delete_until_line_pos = -1 + if delete_until_line == total_lines - 1: + delete_until_line_pos = len(self.text) + else: + delete_until_line_pos = get_line_end_pos(self.text, + delete_until_line) + + self._reload = False + + return widget_line_pos, delete_until_line_pos + + def get_widget_text_from_kv(self, widget, parent, path=[]): + '''This function will get a widget's text from KVLangArea's text given + its parent. + ''' + + start_pos, end_pos = self.get_widget_text_pos_from_kv( + widget, parent, path_to_widget=path) + text = self.text[start_pos:end_pos] + + return text + + def remove_widget_from_parent(self, widget): + '''This function is called when widget is removed from parent. + It will delete widget's rule from parent's rule + ''' + if self.text == '': + return + + self._reload = False + + start_pos, end_pos = self.get_widget_text_pos_from_kv(widget) + text = self.text[start_pos:end_pos] + self.text = self.text[:start_pos] + self.text[end_pos:] + return text + + def _get_widget_from_path(self, path): + '''This function is used to get widget given its path + ''' + + if not self.playground.root: + return None + + if len(path) == 0: + return None + + root = self.playground.root + path_index = 0 + widget = root + path_length = len(path) + + while widget.children != [] and path_index < path_length: + try: + widget = widget.children[len(widget.children) - + 1 - path[path_index]] + except IndexError: + widget = widget.children[0] + + path_index += 1 + + return widget + + def func_reload_kv(self, force=False, *args): + self.have_error = False + if not self._reload: + self._reload = True + return + + if not isinstance(force, bool): + force = False + self.dispatch('on_reload_kv', self.text, force) + + def on_reload_kv(self, text, force, *args): + '''Dispatches an event with the KV lang area text + ''' + pass + + def _get_widget_path_at_line(self, lineno, root_lineno=0): + '''To get widget path of widget at line + ''' + + if self.text == '': + return [] + + text = self.text + # Remove all comments + text = re.sub(r'#.+', '', text) + + lines = text.splitlines() + line = lines[lineno] + + # Search for the line containing widget's name + _lineno = lineno + + while line.find(':') != -1 and \ + line.strip().find(':') != len(line.strip()) - 1: + lineno -= 1 + line = lines[lineno] + + path = [] + child_count = 0 + # From current line go above and + # fill number of children above widget's rule + while _lineno >= root_lineno and lines[_lineno].strip() != "" and \ + get_indentation(lines[lineno]) != 0: + _lineno = lineno - 1 + diff_indent = get_indentation(lines[lineno]) - \ + get_indentation(lines[_lineno]) + + while _lineno >= root_lineno and (lines[_lineno].strip() == '' + or diff_indent <= 0): + if lines[_lineno].strip() != '' and diff_indent == 0 and \ + 'canvas' not in lines[_lineno] and \ + (lines[_lineno].find(':') == -1 or + lines[_lineno].find(':') == + len(lines[_lineno].rstrip()) - 1): + child_count += 1 + + _lineno -= 1 + diff_indent = get_indentation(lines[lineno]) - \ + get_indentation(lines[_lineno]) + + lineno = _lineno + + if _lineno > root_lineno: + _lineno += 1 + + if 'canvas' not in lines[_lineno] and \ + lines[_lineno].strip().find(':') == \ + len(lines[_lineno].strip()) - 1: + + path.insert(0, child_count) + child_count = 0 + + return path + + def get_property_value(self, widget, prop): + self._reload = False + if prop[:3] != 'on_' and \ + not isinstance(widget.properties()[prop], StringProperty) and\ + value == '': + return + + path_to_widget = self.get_widget_path(widget) + path_to_widget.reverse() + + # Go to the line where widget is declared + lines = re.sub(r'#.+', '', self.text).splitlines() + total_lines = len(lines) + + root_name = self.playground.root_name + total_lines = len(lines) + root_lineno = 0 + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + widget_lineno = self._find_widget_place(path_to_widget, lines, + total_lines, root_lineno + 1) + widget_line = lines[widget_lineno] + indent = get_indentation(widget_line) + prop_found = False + + # Else find if property has already been declared with a value + lineno = widget_lineno + 1 + # But if widget line is the last line in the text + if lineno < total_lines: + line = lines[lineno] + _indent = get_indentation(line) + colon_pos = -1 + while lineno < total_lines and (line.strip() == '' or + _indent > indent): + line = lines[lineno] + _indent = get_indentation(line) + if line.strip() != '': + colon_pos = line.find(':') + if colon_pos == -1: + break + + if colon_pos == len(line.rstrip()) - 1: + break + + if prop == line[:colon_pos].strip(): + prop_found = True + break + + lineno += 1 + + if prop_found: + # if property found then change its value + _pos_prop_value = get_line_start_pos(self.text, lineno) + \ + colon_pos + 2 + if lineno == total_lines - 1: + _line_end_pos = len(self.text) + else: + _line_end_pos = get_line_end_pos(self.text, lineno) + + return self.text[_pos_prop_value:_line_end_pos] + + return '' + + def set_event_handler(self, widget, prop, value): + self._reload = False + + path_to_widget = self.get_widget_path(widget) + path_to_widget.reverse() + + # Go to the line where widget is declared + lines = re.sub(r'#.+', '', self.text).splitlines() + total_lines = len(lines) + + root_name = self.playground.root_name + total_lines = len(lines) + root_lineno = 0 + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + widget_lineno = self._find_widget_place(path_to_widget, lines, + total_lines, root_lineno + 1) + + widget_line = lines[widget_lineno] + indent = get_indentation(widget_line) + prop_found = False + + if not widget_line.strip(): + return + + if ':' not in widget_line: + # If cannot find ':' then insert it + self.cursor = (len(lines[widget_lineno]), widget_lineno) + lines[widget_lineno] += ':' + self.insert_text(':') + + else: + # Else find if property has already been declared with a value + lineno = widget_lineno + 1 + # But if widget line is the last line in the text + if lineno < total_lines: + line = lines[lineno] + _indent = get_indentation(line) + colon_pos = -1 + while lineno < total_lines and (line.strip() == '' or + _indent > indent): + line = lines[lineno] + _indent = get_indentation(line) + if line.strip() != '': + colon_pos = line.find(':') + if colon_pos == -1: + break + + if colon_pos == len(line.rstrip()) - 1: + break + + if prop == line[:colon_pos].strip(): + prop_found = True + break + + lineno += 1 + + if prop_found: + if lineno == total_lines - 1: + _line_end_pos = len(self.text) + else: + _line_end_pos = get_line_end_pos(self.text, lineno) + + if value != '': + # if property found then change its value + _pos_prop_value = get_line_start_pos(self.text, lineno) + \ + colon_pos + 2 + self.text = self.text[:_pos_prop_value] + ' ' + value + \ + self.text[_line_end_pos:] + + self.cursor = (0, lineno) + + else: + _line_start_pos = get_line_start_pos(self.text, widget_lineno) + self.text = \ + self.text[:get_line_start_pos(self.text, lineno)] + \ + self.text[_line_end_pos:] + + elif value != '': + # if not found then add property after the widgets line + _line_end_pos = get_line_end_pos(self.text, widget_lineno) + + indent_str = '\n' + for i in range(indent + 4): + indent_str += ' ' + + self.cursor = (len(lines[widget_lineno]), widget_lineno) + self.insert_text(indent_str + prop + ': ' + str(value)) + + def set_property_value(self, widget, prop, value, proptype): + '''To find and change the value of property of widget rule in text + ''' + + # Do not add property if value is empty and + # property is not a string property + + self._reload = False + if not isinstance(widget.properties()[prop], StringProperty) and\ + value == '': + return + + path_to_widget = self.get_widget_path(widget) + path_to_widget.reverse() + + # Go to the line where widget is declared + lines = re.sub(r'#.+', '', self.text.rstrip()).splitlines() + total_lines = len(lines) + + root_name = self.playground.root_name + total_lines = len(lines) + root_lineno = 0 + for lineno, line in enumerate(lines): + pos = line.find(root_name) + if pos != -1 and get_indentation(line) == 0: + root_lineno = lineno + break + + widget_lineno = self._find_widget_place(path_to_widget, lines, + total_lines, root_lineno + 1) + widget_line = lines[widget_lineno] + if not widget_line.strip(): + return + + indent = get_indentation(widget_line) + prop_found = False + + if ':' not in widget_line: + # If cannot find ':' then insert it + self.cursor = (len(lines[widget_lineno]), widget_lineno) + lines[widget_lineno] += ':' + self.insert_text(':') + + else: + # Else find if property has already been declared with a value + lineno = widget_lineno + 1 + # But if widget line is the last line in the text + if lineno < total_lines: + line = lines[lineno] + _indent = get_indentation(line) + colon_pos = -1 + while lineno < total_lines and (line.strip() == '' or + _indent > indent): + line = lines[lineno] + _indent = get_indentation(line) + if line.strip() != '': + colon_pos = line.find(':') + if colon_pos == -1: + break + + if colon_pos == len(line.rstrip()) - 1: + break + + if prop == line[:colon_pos].strip(): + prop_found = True + break + + lineno += 1 + + if prop_found: + # if property found then change its value + _pos_prop_value = get_line_start_pos(self.text, lineno) + \ + colon_pos + 2 + if lineno == total_lines - 1: + _line_end_pos = len(self.text) + else: + _line_end_pos = get_line_end_pos(self.text, lineno) + + if proptype == 'StringProperty' or \ + (proptype == 'OptionProperty' and + not isinstance(value, list)): + value = "'{}'".format(value.replace("'", "\\'")) + + self.text = self.text[:_pos_prop_value] + ' ' + str(value) + \ + self.text[_line_end_pos:] + + self.cursor = (0, lineno) + + else: + # if not found then add property after the widgets line + _line_start_pos = get_line_start_pos(self.text, widget_lineno) + _line_end_pos = get_line_end_pos(self.text, widget_lineno) + if proptype == 'StringProperty' or \ + (proptype == 'OptionProperty' and + not isinstance(value, list)): + value = "'{}'".format(value.replace("'", "\\'")) + + indent_str = '\n' + for i in range(indent + 4): + indent_str += ' ' + + self.cursor = (len(lines[widget_lineno]), widget_lineno) + self.insert_text(indent_str + prop + ': ' + str(value)) + + def _find_widget_place(self, path, lines, total_lines, lineno, indent=4): + '''To find the line where widget is declared according to path + ''' + + child_count = 0 + path_index = 1 + path_length = len(path) + # From starting line go down to find the widget's rule according to path + while lineno < total_lines and path_index < path_length: + line = lines[lineno] + _indent = get_indentation(line) + colon_pos = line.find(':') + if _indent == indent and line.strip() != '': + if colon_pos != -1: + line = line.rstrip() + if colon_pos == len(line) - 1 and 'canvas' not in line: + line = line[:colon_pos].lstrip() + if child_count == path[path_index]: + path_index += 1 + indent = _indent + 4 + child_count = 0 + else: + child_count += 1 + else: + child_count += 1 + + lineno += 1 + + return lineno - 1 diff --git a/designer/components/playground.py b/designer/components/playground.py new file mode 100644 index 0000000..39aacfa --- /dev/null +++ b/designer/components/playground.py @@ -0,0 +1,1253 @@ +import functools +import os +import re +from io import open +from core.undo_manager import WidgetDragOperation, WidgetOperation +from uix.confirmation_dialog import ConfirmationDialogSave +from uix.settings import SettingListContent +from utils.toolbox_widgets import toolbox_widgets as widgets_common +from utils.utils import ( + FakeSettingList, + get_app_widget, + get_current_project, + get_designer, + ignore_proj_watcher, + show_message, +) +from kivy.app import App +from kivy.base import EventLoop +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.factory import Factory +from kivy.graphics import Color, Line +from kivy.properties import ( + BooleanProperty, + ListProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.carousel import Carousel +from kivy.uix.filechooser import FileChooserIconView, FileChooserListView +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.layout import Layout +from kivy.uix.popup import Popup +from kivy.uix.relativelayout import RelativeLayout +from kivy.uix.scatter import ScatterPlane +from kivy.uix.scatterlayout import ScatterLayout +from kivy.uix.screenmanager import Screen, ScreenManager +from kivy.uix.tabbedpanel import TabbedPanel + + +class PlaygroundDragElement(BoxLayout): + '''An instance of this class is the drag element shown when user tries to + add a widget to :class:`~designer.components.playground.Playground` + by dragging from :class:`~designer.components.toolbox.Toolbox` to + :class:`~designer.components.playground.Playground`. + ''' + + playground = ObjectProperty() + '''Reference to the :class:`~designer.components.playground.Playground` + :data:`playground` is a :class:`~kivy.properties.ObjectProperty` + ''' + + target = ObjectProperty(allownone=True) + '''Widget where widget is to be added. + :data:`target` a :class:`~kivy.properties.ObjectProperty` + ''' + + can_place = BooleanProperty(False) + '''Whether widget can be added or not. + :data:`can_place` is a :class:`~kivy.properties.BooleanProperty` + ''' + + drag_type = OptionProperty('new widget', options=('new widget', + 'dragndrop')) + '''Specifies the type of dragging currently done by PlaygroundDragElement. + If it is 'new widget', then it means a new widget will be added + If it is 'dragndrop', then it means already created widget is + drag-n-drop, from one position to another. + :data:`drag_type` is a :class:`~kivy.properties.OptionProperty` + ''' + + drag_parent = ObjectProperty(None) + '''Parent of currently dragged widget. + Will be none if 'drag_type' is 'new widget' + :data:`drag_parent` is a :class:`~kivy.properties.ObjectProperty` + ''' + + widgettree = ObjectProperty(None) + '''Reference to class:`~designer.nodetree.WidgetsTree`, + the widget_tree of Designer. + :data:`widgettree` is a :class:`~kivy.properties.ObjectProperty` + ''' + + child = ObjectProperty(None) + '''The widget which is currently being dragged. + :data:`child` is a :class:`~kivy.properties.ObjectProperty` + ''' + + widget = ObjectProperty(None) + '''The widget which is currently being dragged and will be added to the UI. + This is similar to child, however does not contains custom style used + to present the dragging widget + :data:`widget` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(PlaygroundDragElement, self).__init__(**kwargs) + if self.child: + self.add_widget(self.child) + + def show_lines_on_child(self, *args): + '''To schedule Clock's callback for _show_lines_on_child. + ''' + Clock.schedule_once(self._show_lines_on_child) + + def on_widget(self, *args): + def update_parent(*largs): + p = self.widget.parent + if p: + self.widget.KD__last_parent = p + self.widget.unbind(parent=update_parent) + self.widget.bind(parent=update_parent) + update_parent() + + def _show_lines_on_child(self, *args): + '''To show boundaries around the child. + ''' + x, y = self.child.pos + right, top = self.child.right, self.child.top + points = [x, y, right, y, right, top, x, top] + if hasattr(self, '_canvas_instr'): + points_equal = True + for i in range(len(points)): + if points[i] != self._canvas_instr[1].points[i]: + points_equal = False + break + + if points_equal: + return + + self.remove_lines_on_child() + with self.child.canvas.after: + color = Color(1, 0.5, 0.8) + line = Line(points=points, close=True, width=2.) + + self._canvas_instr = [color, line] + + def remove_lines_on_child(self, *args): + '''Remove lines from canvas of child. + ''' + if hasattr(self, '_canvas_instr') \ + and self._canvas_instr[1].points[0] != -1: + try: + self.child.canvas.after.remove(self._canvas_instr[0]) + self.child.canvas.after.remove(self._canvas_instr[1]) + except ValueError: + pass + + self._canvas_instr[1].points[0] = -1 + Clock.unschedule(self._show_lines_on_child) + + def is_intersecting_playground(self, x, y): + '''To determine whether x,y is inside playground + ''' + if not self.playground: + return False + + if self.playground.x <= x <= self.playground.right \ + and self.playground.y <= y <= self.playground.top: + return True + + return False + + def is_intersecting_widgettree(self, x, y): + '''To determine whether x,y is inside playground + ''' + if not self.widgettree: + return False + + if self.widgettree.x <= x <= self.widgettree.right \ + and self.widgettree.y <= y <= self.widgettree.top: + return True + + return False + + def on_touch_move(self, touch): + '''This is responsible for moving the drag element and showing where + the widget it contains will be added. + ''' + # if this widget is not being dragged, exit + if touch.grab_current is not self: + return False + + # update dragging position + self.center_x = touch.x + self.y = touch.y + 20 + + # the widget where it will be added + target = None + + # now, getting the target widget + # if is targeting the playground + if self.is_intersecting_playground(touch.x, touch.y): + target = self.playground.try_place_widget( + self.widget, touch.x, touch.y) + + # if is targeting widget tree + elif self.is_intersecting_widgettree(touch.x, touch.y): + pos_in_tree = self.widgettree.tree.to_widget( + touch.x, touch.y) + node = self.widgettree.tree.get_node_at_pos(pos_in_tree) + + if node: + # if the same widget, skip + if node.node == self.widget: + return True + else: + # otherwise, runs recursively until get a valid target + while node and node.node != self.playground.sandbox: + widget = node.node + if self.playground.allowed_target_for( + widget, self.children): + # current target is valid + target = widget + break + # runs each parent to find a valid target + node = node.parent_node + + self.target = target + + # check if its added somewhere, remove it + if self.widget.parent: + if self.target: + # special cases + if isinstance(self.target, ScreenManager): + if isinstance(self.widget, Screen): + self.target.remove_widget(self.widget) + self.target.real_remove_widget(self.widget) + elif not isinstance(self.target, TabbedPanel): + self.target.remove_widget(self.widget) + # inside a usual widget + if self.widget.parent: + self.widget.parent.remove_widget(self.widget) + + # check if it can be placed in the target + # if moving from another place + if self.drag_type == 'dragndrop': + self.can_place = target == self.drag_parent + # if is a new widget + else: + self.can_place = target is not None + + # if cannot add it, go away + if not target or not self.can_place: + return True + + # try to add the widget + + self.playground.sandbox.error_active = True + with self.playground.sandbox: + # adding a new widget + if isinstance(target, ScreenManager): + target.real_add_widget(self.widget) + # usual target + else: + target.add_widget(self.widget) + App.get_running_app().focus_widget(target) + + self.playground.sandbox.error_active = False + + return True + + def on_touch_up(self, touch): + '''This is responsible for adding the widget to the parent + ''' + # if this widget is not being dragged, exit + if touch.grab_current is not self: + return False + + # aborts the dragging + touch.ungrab(self) + + widget_from = None + target = None + + # get info about the dragged widget + if self.is_intersecting_playground(touch.x, touch.y): + # if added by playground + target = self.playground.try_place_widget( + self.widget, touch.x, touch.y) + widget_from = 'playground' + elif self.is_intersecting_widgettree(touch.x, touch.y): + # if added by widgettree + pos_in_tree = self.widgettree.tree.to_widget( + touch.x, touch.y) + node = self.widgettree.tree.get_node_at_pos(pos_in_tree) + if node: + widget = node.node + while widget and widget != self.playground.sandbox: + if self.playground.allowed_target_for( + widget, self.widget): + target = widget + widget_from = 'treeview' + break + + widget = widget.parent + + # check if has parent + parent = self.widget.parent + + # check if it's possible to add it in the target + if self.drag_type == 'dragndrop': + self.can_place = target == self.drag_parent and \ + parent is not None + else: + self.can_place = target is not None and parent is not None + + # try to find the widget on parent(from preview) and remove it + index = -1 + if self.target: + try: + index = self.target.children.index(self.widget) + except ValueError: + pass + + if isinstance(self.target, ScreenManager): + self.target.real_remove_widget(self.widget) + else: + self.target.remove_widget(self.widget) + + # check if we can add this new widget + self.playground.sandbox.error_active = True + with self.playground.sandbox: + if self.can_place or self.playground.root is None: + # if widget already exists, just moving it + if self.drag_type == 'dragndrop': + if parent: + if widget_from == 'playground': + # adding by playground + self.playground.place_widget( + self.widget, touch.x, touch.y, index=index) + else: + # adding by tree + self.playground.place_widget( + self.widget, touch.x, touch.y, + index=index, target=target) + else: + # adding by playground + if widget_from == 'playground': + self.playground.place_widget( + self.widget, touch.x, touch.y) + # adding by widget tree + else: + self.playground.add_widget_to_parent( + self.widget, target) + # if could not add it and from playground, undo the modifications + elif self.drag_type == 'dragndrop': + # just cant add it, undo the last modifications + self.playground.undo_dragging() + # if widget outside of screen is removed + if target is None: + self.playground.remove_widget_from_parent(self.widget) + + self.playground.sandbox.error_active = False + + # remove the dragging widget + self.target = None + if self.parent: + self.parent.remove_widget(self) + self.playground.drag_operation = [] + self.playground.from_drag = False + return True + + def fit_child(self, *args): + '''Updates it's size to display the child correctly + ''' + if self.child: + self.size = self.child.size + + +class Playground(ScatterPlane): + '''Playground represents the actual area where user will add and delete + the widgets. It has event on_show_edit, which is emitted whenever + Playground is clicked. + ''' + + root = ObjectProperty(None, allownone=True) + '''This property represents the root widget. + :data:`root` is a :class:`~kivy.properties.ObjectProperty` + ''' + + root_name = StringProperty('') + '''Specifies the current widget under modification on Playground + :data:`root_name` is a :class:`~kivy.properties.StringProperty` + ''' + + root_app_widget = ObjectProperty(None, allownone=True) + '''This property represents the root widget as a ProjectManager.AppWidget + :data:`root_app_widget` is a :class:`~kivy.properties.ObjectProperty` + ''' + + tree = ObjectProperty() + + clicked = BooleanProperty(False) + '''This property represents whether + :class:`~designer.components.playground.Playground` + has been clicked or not + :data:`clicked` is a :class:`~kivy.properties.BooleanProperty` + ''' + + sandbox = ObjectProperty(None) + '''This property represents the sandbox widget which is added to + :class:`~designer.components.playground.Playground`. + :data:`sandbox` is a :class:`~kivy.properties.ObjectProperty` + ''' + + kv_code_input = ObjectProperty() + '''This property refers to the + :class:`~designer.components.ui_creator.UICreator`'s KVLangArea. + :data:`kv_code_input` is a :class:`~kivy.properties.ObjectProperty` + ''' + + widgettree = ObjectProperty() + '''This property refers to the + :class:`~designer.components.ui_creator.UICreator`'s WidgetTree. + :data:`widgettree` is a :class:`~kivy.properties.ObjectProperty` + ''' + + from_drag = BooleanProperty(False) + '''Specifies whether a widget is dragged or a new widget is added. + :data:`from_drag` is a :class:`~kivy.properties.BooleanProperty` + ''' + + drag_operation = ListProperty((), allownone=True) + '''Stores data of drag_operation in form of a tuple. + drag_operation[0] is the widget which has been dragged. + drag_operation[1] is the parent of above widget. + drag_operation[2] is the index of widget in parent's children property. + :data:`drag_operation` is a :class:`~kivy.properties.ListProperty` + ''' + + _touch_still_down = BooleanProperty(False) + '''Specifies whether touch is still down or not. + :data:`_touch_still_down` is a :class:`~kivy.properties.BooleanProperty` + ''' + + dragging = BooleanProperty(False) + '''Specifies whether currently dragging is performed or not. + :data:`dragging` is a :class:`~kivy.properties.BooleanProperty` + ''' + + __events__ = ('on_show_edit',) + + def __init__(self, **kwargs): + super(Playground, self).__init__(**kwargs) + self.keyboard = None + self.selected_widget = None + self.undo_manager = None + self._widget_x = -1 + self._widget_y = -1 + self.widget_to_paste = None + self._popup = None + self._last_root = None + + def on_root(self, *args): + if self.root: + self._last_root = self.root + + def on_pos(self, *args): + '''Default handler for 'on_pos' + ''' + if self.sandbox: + self.sandbox.pos = self.pos + + def on_size(self, *args): + '''Default handler for 'on_size' + ''' + if self.sandbox: + self.sandbox.size = self.size + + def on_show_edit(self, *args): + '''Default handler for 'on_show_edit' + ''' + pass + + def on_widget_select_pressed(self, *args): + '''Event handler to playground widget selector press + ''' + d = get_designer() + if d.popup: + return False + widgets = get_current_project().app_widgets + app_widgets = [] + for name in widgets.keys(): + widget = widgets[name] + if widget.is_root: + name = 'Root - ' + name + app_widgets.append(name) + + fake_setting = FakeSettingList() + fake_setting.allow_custom = False + fake_setting.items = app_widgets + fake_setting.desc = 'Select the Widget to edit on Playground' + fake_setting.group = 'playground_widget' + + content = SettingListContent(setting=fake_setting) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + d.popup = Popup( + content=content, + title='Playground - Edit Widget', + size_hint=(None, None), + size=(popup_width, popup_height), + auto_dismiss=False + ) + + content.bind(on_apply=self._perform_select_root_widget, + on_cancel=d.close_popup) + + content.selected_items = [self.root_name] + if self.root_app_widget and self.root_app_widget.is_root: + content.selected_items = ['Root - ' + self.root_name] + content.show_items() + + d.popup.open() + + def _perform_select_root_widget(self, instance, selected_item, *args): + '''On Playground edit item selection + :type selected_item: instance of selected array + ''' + get_designer().close_popup() + name = selected_item[0] + # remove Root label from widget name + if name.startswith('Root - '): + name = name.replace('Root - ', '') + self.load_widget(name) + + def no_widget(self, *args): + '''Remove any reamining sandbox content and shows an message + ''' + self.root = None + show_message('No widget found!', 5, 'error') + self.sandbox.clear_widgets() + + def load_widget(self, widget_name, update_kv_lang=True): + '''Load and display and widget given its name. + If widget is not found, shows information on status bar and clear + the playground + :param widget_name name of the widget to display + :param update_kv_lang if True, reloads the kv file. If False, keep the + kv lang text + ''' + d = get_designer() + if d.popup: + # if has a popup, it's not using playground + return False + widgets = get_current_project().app_widgets + # if displaying no widget or this widget is not know + if self.root is None or self.root_app_widget is None or \ + widget_name not in widgets: + self._perform_load_widget(widget_name, update_kv_lang) + return + # if a know widget, continue + target = widgets[widget_name] + + # check if we are switching kv files + if target.kv_path != self.root_app_widget.kv_path and \ + not self.kv_code_input.saved: + + file_name = os.path.basename(self.root_app_widget.kv_path) + _confirm_dlg = ConfirmationDialogSave( + 'The %s was not saved. \n' + 'If you continue, your modifications will be lost.\n' + 'Do you want to save and continue?' % file_name + ) + + @ignore_proj_watcher + def save_and_load(*args): + get_current_project().save() + self._perform_load_widget(widget_name, True) + + def dont_save(*args): + d.close_popup() + self._perform_load_widget(widget_name, True) + + _confirm_dlg.bind( + on_save=save_and_load, + on_dont_save=dont_save, + on_cancel=d.close_popup) + + d.popup = Popup(title='Change Widget', content=_confirm_dlg, + size_hint=(None, None), size=('400pt', '150pt'), + auto_dismiss=False) + d.popup.open() + return + self._perform_load_widget(widget_name, update_kv_lang) + + def _perform_load_widget(self, widget_name, update_kv_lang=True): + '''Loads the widget if everything is ok + :param widget_name name of the widget to display + :param update_kv_lang if True, reloads the kv file. If False, keep the + kv lang text + ''' + self.root_name = widget_name + self.root = None + self.sandbox.clear_widgets() + widgets = get_current_project().app_widgets + try: + target = widgets[widget_name] + if update_kv_lang: + # updates kv lang text with file + kv_path = target.kv_path + if kv_path: + self.kv_code_input.text = open(kv_path, + encoding='utf-8').read() + else: + show_message( + 'Could not found the associated .kv file with %s' + ' widget' % widget_name, 5, 'error' + ) + self.kv_code_input.text = '' + self.root_app_widget = target + wdg = get_app_widget(target) + if wdg is None: + self.kv_code_input.have_error = True + self.add_widget_to_parent(wdg, None, from_undo=True, from_kv=True) + self.kv_code_input.path = target.kv_path + except (KeyError, AttributeError): + show_message( + 'Failed to load %s widget' % widget_name, 5, 'error') + + def on_reload_kv(self, kv_lang_area, text, force): + '''Reloads widgets from kv lang input and update the + visible widget. + if force is True, all widgets must be reloaded before parsing the new kv + :param force: if True, will parse the project again + :param text: kv source + :param kv_lang_area: instance of kivy lang area + ''' + proj = get_current_project() + # copy of initial widgets + widgets = dict(proj.app_widgets) + try: + if force: + proj.parse() + if self.root_name: + kv_path = widgets[self.root_name].kv_path + else: + kv_path = self.kv_code_input.path + proj.parse_kv(text, kv_path) + # if was displaying one widget, but it was removed + if self.root_name and self.root_name not in proj.app_widgets: + self.load_widget_from_file(self.root_app_widget.kv_path) + show_message( + 'The %s is not available. Displaying another widget' + % self.root_name, 5, 'info' + ) + elif not self.root_name and not widgets and proj.app_widgets: + # if was not displaying a widget because there was no widget + # and now a widget is available + first_wdg = proj.app_widgets[list(proj.app_widgets.keys())[-1]] + self.load_widget(first_wdg.name, update_kv_lang=False) + else: + # displaying an usual widget + self.load_widget(self.root_name, update_kv_lang=False) + except KeyError: + show_message( + 'Failed to load %s widget' % self.root_name, 5, 'error') + + def load_widget_from_file(self, kv_path): + '''Loads first widget from a file + :param kv_path: absolute kv path + ''' + self.sandbox.clear_widgets() + proj = get_current_project() + widgets = proj.app_widgets + if not os.path.exists(kv_path): + show_message(kv_path + ' not exists', 5, 'error') + return + self.kv_code_input.text = open(kv_path, 'r', encoding='utf-8').read() + self.kv_code_input.path = kv_path + for key in widgets: + wd = widgets[key] + if wd.kv_path == kv_path: + self.load_widget(wd.name, update_kv_lang=False) + return + # if not found a widget in the path, open the first one + if len(widgets): + first_wdg = widgets[list(widgets.keys())[-1]] + self.load_widget(first_wdg.name, update_kv_lang=False) + return + show_message('No widget was found', 5, 'error') + + def try_place_widget(self, widget, x, y): + '''This function is used to determine where to add the widget + :param y: new widget position + :param x: new widget position + :param widget: widget to be added + ''' + + x, y = self.to_local(x, y) + return self.find_target(x, y, self.root, widget) + + def place_widget(self, widget, x, y, index=0, target=None): + '''This function is used to first determine the target where to add + the widget. Then it add that widget. + :param target: where this widget should be added. + If None, coordinates will be used to locate the target + :param index: index used in add_widget + :param x: widget position x + :param y: widget position y + :param widget: widget to add + ''' + local_x, local_y = self.to_local(x, y) + if not target: + target = self.find_target(local_x, local_y, self.root, widget) + + if not self.from_drag: + self.add_widget_to_parent(widget, target) + else: + extra_args = {'x': x, 'y': y, 'index': index} + self.add_widget_to_parent(widget, target, from_kv=True, + from_undo=True, extra_args=extra_args) + + def drag_wigdet(self, widget, target, extra_args, from_undo=False): + '''This function will drag widget from one place to another inside + target + ''' + extra_args['prev_x'], extra_args['prev_y'] = \ + self.to_parent(self._widget_x, self._widget_y) + + if isinstance(target, FloatLayout) or \ + isinstance(target, ScatterLayout) or \ + isinstance(target, RelativeLayout): + target.add_widget(widget, self.drag_operation[2]) + widget.pos_hint = {} + widget.x, widget.y = self.to_local(extra_args['x'], + extra_args['y']) + self.from_drag = False + added = True + local_x, local_y = widget.x - target.x, widget.y - target.y + self.kv_code_input.set_property_value( + widget, 'pos_hint', "{'x': %f, 'y': %f}" % ( + local_x / target.width, local_y / target.height), + 'ListPropery') + + if not from_undo: + self.undo_manager.push_operation( + WidgetDragOperation(widget, target, + self.drag_operation[1], + self.drag_operation[2], + self, extra_args=extra_args)) + + elif isinstance(target, BoxLayout) or \ + isinstance(target, AnchorLayout) or \ + isinstance(target, GridLayout): + target.add_widget(widget, extra_args['index']) + self.from_drag = False + added = True + if 'prev_index' in extra_args: + self.kv_code_input.shift_widget(widget, + extra_args['prev_index']) + + else: + self.kv_code_input.shift_widget(widget, self.drag_operation[2]) + + if not from_undo: + self.undo_manager.push_operation( + WidgetDragOperation(widget, target, + self.drag_operation[1], + self.drag_operation[2], + self, extra_args=extra_args)) + + def add_widget_to_parent(self, widget, target, from_undo=False, + from_kv=False, kv_str='', extra_args={}): + '''This function is used to add the widget to the target. + :param from_undo: this action is comming from undo + :param target: target will receive the widget + :param widget: widget to be added + ''' + added = False + if widget is None: + return False + + with self.sandbox: + if target is None: + self.root = widget + self.sandbox.add_widget(widget) + widget.size = self.sandbox.size + added = True + else: + if extra_args and self.from_drag: + self.drag_wigdet(widget, target, extra_args=extra_args) + else: + target.add_widget(widget) + added = True + if not added: + return False + + self.widgettree.refresh() + + if not from_kv: + if not kv_str and hasattr(widget, '_KD_KV_STR'): + kv_str = widget._KD_KV_STR + del widget._KD_KV_STR + self.kv_code_input.add_widget_to_parent(widget, target, + kv_str=kv_str) + if not from_undo: + root = App.get_running_app().root + root.undo_manager.push_operation(WidgetOperation('add', + widget, target, + self, '')) + + def get_widget(self, widget_name, **default_args): + '''This function is used to get the instance of class of name, + widgetname. + :param widget_name: name of the widget to be instantiated + ''' + widget = None + for _widget in widgets_common: + if _widget[0] == widget_name and _widget[1] == 'custom': + app_widgets = get_current_project().app_widgets + widget = get_app_widget(app_widgets[widget_name]) + break + if not widget: + try: + widget = Factory.get(widget_name)(**default_args) + except: + pass + return widget + + def generate_kv_from_args(self, widget_name, kv_dict, *args): + '''Converts a dictionary to kv string + :param widget_name: name of the widget + :param kv_dict: dict with widget rules + ''' + kv = widget_name + ':' + indent = '\n' + ' ' * 4 + + try: # check whether python knows about 'basestring' + basestring + except NameError: # no, it doesn't (it's Python3); use 'str' instead + basestring = str + + for key in kv_dict.keys(): + value = kv_dict[key] + if isinstance(value, basestring): + value = "'" + value + "'" + kv += indent + key + ': ' + str(value) + + return kv + + def get_playground_drag_element(self, instance, widget_name, touch, + default_args, extra_args, *args): + '''This function will return the desired playground element + for widget_name. + :param extra_args: extra args used to display the dragging widget + :param default_args: default widget args + :param touch: instance of the current touch + :param instance: if from toolbox, ToolboxButton instance. + None otherwise + :param widget_name: name of the widget that will be dragged + ''' + + # create default widget that will be added and the custom to display + widget = self.get_widget(widget_name, **default_args) + widget._KD_KV_STR = self.generate_kv_from_args(widget_name, + default_args) + values = default_args.copy() + values.update(extra_args) + child = self.get_widget(widget_name, **values) + custom = False + for op in widgets_common: + if op[0] == widget_name: + if op[1] == 'custom': + custom = True + break + container = PlaygroundDragElement( + playground=self, child=child, widget=widget) + if not custom: + container.fit_child() + touch.grab(container) + touch_pos = [touch.x, touch.y] + if instance: + touch_pos = instance.to_window(*touch.pos) + container.center_x = touch_pos[0] + container.y = touch_pos[1] + 20 + return container + + def cleanup(self): + '''This function is used to clean the state of Playground, cleaning + the changes done by currently opened project. + ''' + + # Cleanup is called when project is created or loaded + # so this operation shouldn't be recorded in Undo + if self.root: + self.remove_widget_from_parent(self.root, from_undo=True, + from_kv=True) + + self.selected_widget = None + self._widget_x = -1 + self._widget_y = -1 + self.widget_to_paste = None + + def remove_widget_from_parent(self, widget, from_undo=False, + from_kv=False): + '''This function is used to remove widget its parent. + :param from_undo: is comming from an undo action + :param widget: widget to be removed + ''' + + parent = None + d = get_designer() + if not widget: + return + + removed_str = '' + if not from_kv: + removed_str = self.kv_code_input.remove_widget_from_parent(widget) + if widget != self.root: + parent = widget.parent + if parent is None and hasattr(widget, 'KD__last_parent'): + parent = widget.KD__last_parent + if isinstance(parent.parent, Carousel): + parent.parent.remove_widget(widget) + elif isinstance(parent, ScreenManager): + if isinstance(widget, Screen): + parent.remove_widget(widget) + else: + parent.real_remove_widget(widget) + else: + parent.remove_widget(widget) + else: + self.root.parent.remove_widget(self.root) + self.root = None + + # if is designer + if hasattr(d, 'ui_creator'): + d.ui_creator.widgettree.refresh() + if not from_undo and hasattr(d, 'ui_creator'): + d.undo_manager.push_operation( + WidgetOperation('remove', widget, parent, self, removed_str)) + + def find_target(self, x, y, target, widget=None): + '''This widget is used to find the widget which collides with x,y + :param widget: widget to be added in target + :param target: widget to search over + :param x: position to search + :param y: position to search + ''' + if target is None or not target.collide_point(x, y): + return None + + x, y = target.to_local(x, y) + class_rules = get_current_project().app_widgets + + for child in target.children: + is_child_custom = False + if child == widget: + continue + + for rule_name in class_rules: + if rule_name == type(child).__name__: + is_child_custom = True + break + + is_child_complex = False + for _widget in widgets_common: + if _widget[0] == type(child).__name__ and \ + _widget[1] == 'complex': + is_child_complex = True + break + + # if point lies in custom wigdet's child then return custom widget + if is_child_custom or is_child_complex: + if not widget and self._custom_widget_collides(child, x, y): + return child + + elif widget: + if isinstance(child, TabbedPanel): + if child.current_tab: + _item = self.find_target( + x, y, child.current_tab.content, widget) + return _item + + else: + return target + + elif isinstance(child.parent, Carousel): + t = self.find_target(x, y, child, widget) + return t + + else: + if not child.collide_point(x, y): + continue + + if not self.allowed_target_for(child, widget) and not \ + child.children: + continue + + return self.find_target(x, y, child, widget) + + return target + + def _custom_widget_collides(self, widget, x, y): + '''This widget is used to find which custom widget collides with x,y + ''' + if not widget: + return False + + if widget.collide_point(x, y): + return True + + x, y = widget.to_local(x, y) + for child in widget.children: + if self._custom_widget_collides(child, x, y): + return True + + return False + + def allowed_target_for(self, target, widget): + '''This function is used to determine if widget could be added to + target. + ''' + # stop on complex widget + t = target if widget else target.parent + if isinstance(t, FileChooserListView): + return False + if isinstance(t, FileChooserIconView): + return False + + # if we don't have widget, always return true + if widget is None: + return True + + is_widget_layout = isinstance(widget, Layout) + is_target_layout = isinstance(target, Layout) + if is_widget_layout and is_target_layout: + return True + + if is_target_layout or isinstance(target, Carousel): + return True + + return False + + def _keyboard_released(self, *args): + '''Called when self.keyboard is released + ''' + self.keyboard.unbind(on_key_down=self._on_keyboard_down) + self.keyboard = None + + def _on_keyboard_down(self, keyboard, keycode, text, modifiers): + '''Called when a key on keyboard is pressed + ''' + if modifiers != [] and modifiers[-1] == 'ctrl': + if keycode[1] == 'c': + self.do_copy() + + elif keycode[1] == 'v': + self.do_paste() + + elif keycode[1] == 'x': + self.do_cut() + + elif keycode[1] == 'a': + self.do_select_all() + + elif keycode[1] == 'z': + self.do_undo() + + elif modifiers[0] == 'shift' and keycode[1] == 'z': + self.do_redo() + + elif keycode[1] == 'delete': + self.do_delete() + + def do_undo(self): + '''Undoes the last operation + ''' + self.undo_manager.do_undo() + + def do_redo(self): + '''Undoes the last operation + ''' + self.undo_manager.do_redo() + + def do_copy(self, for_drag=False): + '''Copy the selected widget + ''' + base_widget = self.selected_widget + if base_widget: + self.widget_to_paste = self.get_widget(type(base_widget).__name__) + props = base_widget.properties() + for prop in props: + if prop == 'id' or prop == 'children': + continue + + setattr(self.widget_to_paste, prop, + getattr(base_widget, prop)) + + self.widget_to_paste.parent = None + widget_str = self.kv_code_input. \ + get_widget_text_from_kv(base_widget, None) + + if not for_drag: + widget_str = re.sub(r'\s+id:\s*[\w\d_]+', '', widget_str) + self._widget_str_to_paste = widget_str + + def do_paste(self): + '''Paste the selected widget to the current widget + ''' + parent = self.selected_widget + if parent and self.widget_to_paste: + d = get_current_project() + class_rules = d.app_widgets + root_widget = self.root + is_child_custom = False + for rule_name in class_rules: + if rule_name == type(parent).__name__: + is_child_custom = True + break + + # find appropriate parent to add widget_to_paste + while parent: + if isinstance(parent, Layout) and (not is_child_custom + or root_widget == parent): + break + + parent = parent.parent + is_child_custom = False + for rule_name in class_rules: + if rule_name == type(parent).__name__: + is_child_custom = True + break + + if parent is not None: + self.add_widget_to_parent(self.widget_to_paste, + parent, + kv_str=self._widget_str_to_paste) + self.widget_to_paste = None + + def do_cut(self): + '''Cuts the selected widget + ''' + base_widget = self.selected_widget + + if base_widget and base_widget.parent: + self.widget_to_paste = base_widget + self._widget_str_to_paste = self.kv_code_input. \ + get_widget_text_from_kv(base_widget, None) + + self.remove_widget_from_parent(base_widget) + + def do_select_all(self): + '''Select All widgets which basically means selecting root widget + ''' + self.selected_widget = self.root + App.get_running_app().focus_widget(self.root) + + def do_delete(self): + '''Delete the selected widget + ''' + if self.selected_widget: + self.remove_widget_from_parent(self.selected_widget) + self.selected_widget = None + + def on_touch_move(self, touch): + '''Default handler for 'on_touch_move' + ''' + if self.widgettree.dragging is True: + return True + + super(Playground, self).on_touch_move(touch) + return False + + def on_touch_up(self, touch): + '''Default handler for 'on_touch_up' + ''' + if super(ScatterPlane, self).collide_point(*touch.pos): + self.dragging = False + Clock.unschedule(self.start_widget_dragging) + + self.dragging = False + return super(Playground, self).on_touch_up(touch) + + def undo_dragging(self): + '''To undo the last dragging operation if it has not been completed. + ''' + if not self.drag_operation: + return + + if self.drag_operation[0].parent: + self.drag_operation[0].parent.remove_widget(self.drag_operation[0]) + + try: + self.drag_operation[1].add_widget(self.drag_operation[0], + self.drag_operation[2]) + except TypeError: + # some widgets not allow index + self.drag_operation[1].add_widget(self.drag_operation[0]) + + Clock.schedule_once(functools.partial( + App.get_running_app().focus_widget, + self.drag_operation[0]), 0.01) + self.drag_operation = [] + + def start_widget_dragging(self, *args): + '''This function will create PlaygroundDragElement + which will start dragging currently selected widget. + ''' + if not self.dragging and not self.drag_operation \ + and self.selected_widget and self.selected_widget != self.root: + # x, y = self.to_local(*touch.pos) + # target = self.find_target(x, y, self.root) + drag_widget = self.selected_widget + self._widget_x, self._widget_y = drag_widget.x, drag_widget.y + index = self.selected_widget.parent.children.index(drag_widget) + self.drag_operation = (drag_widget, drag_widget.parent, index) + + self.selected_widget.parent.remove_widget(self.selected_widget) + drag_elem = App.get_running_app().create_draggable_element( + None, '', self.touch, self.selected_widget) + + drag_elem.drag_type = 'dragndrop' + drag_elem.drag_parent = self.drag_operation[1] + self.dragging = True + self.from_drag = True + App.get_running_app().focus_widget(None) + + def on_touch_down(self, touch): + '''An override of ScatterPlane's on_touch_down. + Used to determine the current selected widget and also emits, + on_show_edit event. + ''' + + if super(ScatterPlane, self).collide_point(*touch.pos) and \ + not self.keyboard: + win = EventLoop.window + self.keyboard = win.request_keyboard(self._keyboard_released, self) + self.keyboard.bind(on_key_down=self._on_keyboard_down) + + if super(ScatterPlane, self).collide_point(*touch.pos): + if not self.dragging: + self.touch = touch + Clock.schedule_once(self.start_widget_dragging, 0.5) + + x, y = self.to_local(*touch.pos) + target = self.find_target(x, y, self.root) + self.selected_widget = target + App.get_running_app().focus_widget(target) + self.clicked = True + self.dispatch('on_show_edit', Playground) + return True + + if self.parent.collide_point(*touch.pos): + super(Playground, self).on_touch_down(touch) + + return False diff --git a/designer/components/playground_size_selector.py b/designer/components/playground_size_selector.py new file mode 100644 index 0000000..7a9a5ce --- /dev/null +++ b/designer/components/playground_size_selector.py @@ -0,0 +1,232 @@ +from functools import partial + +from kivy.clock import Clock +from kivy.properties import ObjectProperty, OptionProperty, StringProperty +from kivy.uix.accordion import AccordionItem +from kivy.uix.button import Button +from kivy.uix.gridlayout import GridLayout +from kivy.uix.modalview import ModalView +from kivy.uix.togglebutton import ToggleButton + + +class PlaygroundSizeSelector(Button): + '''Button to open playground size selection view + ''' + + view = ObjectProperty() + '''This property refers to the + :class:`~designer.components..playground_size_selector.PlaygroundSizeView` + instance. + :data:`view` is an :class:`~kivy.properties.ObjectProperty` + ''' + + playground = ObjectProperty() + '''This property holds a reference to the + :class:`~designer.components.playground.Playground` instance. + :data:`playground` is an :class:`~kivy.properties.ObjectProperty` + ''' + + def on_playground(self, *_): + '''Create a + :class: + `~designer.components.playground_size_selector.PlaygroundSizeView` + for the current playground. + ''' + self.view = PlaygroundSizeView(selected_size=self.playground.size) + self.view.bind(selected_size=self._update_playground) + self.view.bind(selected_size_name=self.setter('text')) + self.text = self.view.selected_size_name + + def _update_playground(self, _, size): + '''Callback to update the playground size on :data:`selected_size` + changes + ''' + if self.playground: + self.playground.size = size + if self.playground.root: + self.playground.root.size = size + + def on_press(self): + '''Open the + :class: + `~designer.components.playground_size_selector.PlaygroundSizeView` + ''' + self.view.size_hint = None, None + self.view.width = self.get_root_window().width / 2. + self.view.height = self.get_root_window().height / 2. + self.view.attach_to = self + self.view.open() + + +class PlaygroundSizeView(ModalView): + '''Dialog for playground size selection + ''' + + accordion = ObjectProperty() + '''This property holds a reference to the + :class:`~kivy.uix.accordion.Accordion` inside the dialog. + :data:`accordion` is an :class:`~kivy.properties.ObjectProperty` + ''' + + selected_size = ObjectProperty() + '''This property contains the currently selected playground size. + :data:`selected_size` is an :class:`~kivy.properties.ObjectProperty` + ''' + + selected_size_name = StringProperty('') + '''This property contains the name associated with :data:`selected_size`. + :data:`selected_size_name` is a :class:`~kivy.properties.StringProperty` + ''' + + selected_orientation = OptionProperty( + 'landscape', options=('portrait', 'landscape') + ) + '''This property contains the screen orientation for :data:`selected_size`. + :data:`selected_orientation` is an + :class:`~kivy.properties.OptionProperty` + ''' + + default_sizes = ( + ('Desktop - SD', ( + ('Default', (550, 350)), + ('Small', (800, 600)), + ('Medium', (1024, 768)), + ('Large', (1280, 1024)), + ('XLarge', (1600, 1200)) + )), + ('Desktop - HD', ( + ('720p', (1280, 720)), + ('LVDS', (1366, 768)), + ('1080p', (1920, 1080)), + ('4K', (3840, 2160)), + ('4K Cinema', (4096, 2160)) + )), + ('Generic', ( + ('QVGA', (320, 240)), + ('WQVGA400', (400, 240)), + ('WQVGA432', (432, 240)), + ('HVGA', (480, 320)), + ('WVGA800', (800, 480)), + ('WVGA854', (854, 480)), + ('1024x600', (1024, 600)), + ('1024x768', (1024, 768)), + ('1280x768', (1280, 768)), + ('WXGA', (1280, 800)), + ('640x480', (640, 480)), + ('1536x1152', (1536, 1152)), + ('1920x1152', (1920, 1152)), + ('1920x1200', (1920, 1200)), + ('960x640', (960, 640)), + ('2048x1536', (2048, 1536)), + ('2560x1536', (2560, 1536)), + ('2560x1600', (2560, 1600)), + )), + ('Android', ( + ('HTC One', (1920, 1080)), + ('HTC One X', (1920, 720)), + ('HTC One SV', (800, 480)), + ('Galaxy S3', (1280, 720)), + ('Galaxy Note 2', (1280, 720)), + ('Motorola Droid 2', (854, 480)), + ('Motorola Xoom', (1280, 800)), + ('Xperia E', (480, 320)), + ('Nexus 4', (1280, 768)), + ('Nexus 7 (2012)', (1280, 800)), + ('Nexus 7 (2013)', (1920, 1200)), + )), + ('iOS', ( + ('iPad 1/2', (1024, 768)), + ('iPad 3', (2048, 1536)), + ('iPhone 4', (960, 640)), + ('iPhone 5', (1136, 640)), + )), + ) + '''Ordered map of default selectable sizes. + ''' + + def __init__(self, **kwargs): + self._buttons = {} + super(PlaygroundSizeView, self).__init__(**kwargs) + Clock.schedule_once(self.config) + + def config(self, *args): + for title, values in self.default_sizes: + grid = GridLayout(rows=4) + + def sort_sizes(item): + return item[1][1] * item[1][0] + + values = sorted(values, key=sort_sizes, reverse=True) + for name, size in values: + btn = ToggleButton(text='', markup=True) + btntext = ('%s\n[color=777777][size=%d]%dx%d[/size][/color]' % + (name, btn.font_size * 0.8, size[0], size[1])) + btn.text = btntext + btn.bind(on_press=partial(self.set_size, size)) + grid.add_widget(btn) + self._buttons[name] = btn + + item = AccordionItem(title=title) + item.add_widget(grid) + self.accordion.add_widget(item) + + self.accordion.select(self.accordion.children[-1]) + + self.update_buttons() + + def find_size(self): + '''Find the size name and orientation for the current size. + ''' + orientation = self.check_orientation(self.selected_size) + check_size = tuple(sorted(self.selected_size, reverse=True)).__eq__ + for _, values in self.default_sizes: + for name, size in values: + if check_size(size): + return name, size, orientation + return 'Custom', self.selected_size, orientation + + def check_orientation(self, size): + '''Determine if the provided size is portrait or landscape. + ''' + return 'portrait' if size[1] > size[0] else 'landscape' + + def update_buttons(self, size_name=None): + '''Update the toggle state of the size buttons and open the + appropriate accordion section. + ''' + if not size_name: + size_name = self.find_size()[0] + for name, btn in list(self._buttons.items()): + if name == size_name: + btn.state = 'down' + self.accordion.select(btn.parent.parent.parent.parent.parent) + else: + btn.state = 'normal' + + def on_selected_size(self, *_): + '''Callback to update properties on changes to :data:`selected_size`. + ''' + size_info = self.find_size() + self.selected_size_name = ('%s\n[color=777777](%s, %dx%d)[/color]' % + (size_info[0], size_info[2], + size_info[1][0], size_info[1][1])) + self.selected_orientation = size_info[2] + + self.update_buttons(size_info[0]) + + def update_size(self, size): + '''Set :data:`selected_size` while taking orientation into account. + ''' + size = sorted(size, reverse=self.selected_orientation == 'landscape') + self.selected_size = size + + def set_size(self, size, *_): + '''Set :data:`selected_size` and close the dialog. + ''' + self.update_size(size) + self.dismiss() + + def on_selected_orientation(self, _, value): + '''Callback to update size on changes to :data:`selected_orientation`. + ''' + self.update_size(self.selected_size) diff --git a/designer/components/property_viewer.py b/designer/components/property_viewer.py new file mode 100644 index 0000000..950b390 --- /dev/null +++ b/designer/components/property_viewer.py @@ -0,0 +1,322 @@ +from core.undo_manager import PropOperation +from uix.settings import SettingListContent +from utils.utils import FakeSettingList, get_designer +from kivy.core.window import Window +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.checkbox import CheckBox +from kivy.uix.label import Label +from kivy.uix.popup import Popup +from kivy.uix.scrollview import ScrollView +from kivy.uix.textinput import TextInput + + +class PropertyLabel(Label): + '''This class represents the :class:`~kivy.label.Label` for showing + Property Names in + :class:`~designer.components.property_viewer.PropertyViewer`. + ''' + pass + + +class PropertyBase(object): + '''This class represents Abstract Class for Property showing classes i.e. + PropertyTextInput and PropertyBoolean + ''' + + propwidget = ObjectProperty() + '''It is an instance to the Widget whose property value is displayed. + :data:`propwidget` is a :class:`~kivy.properties.ObjectProperty` + ''' + + propname = StringProperty() + '''It is the name of the property. + :data:`propname` is a :class:`~kivy.properties.StringProperty` + ''' + + propvalue = ObjectProperty(allownone=True) + '''It is the value of the property. + :data:`propvalue` is a :class:`~kivy.properties.ObjectProperty` + ''' + + oldvalue = ObjectProperty(allownone=True) + '''It is the old value of the property + :data:`oldvalue` is a :class:`~kivy.properties.ObjectProperty` + ''' + + have_error = BooleanProperty(False) + '''It specifies whether there have been an error in setting new value + to property + :data:`have_error` is a :class:`~kivy.properties.BooleanProperty` + ''' + + proptype = StringProperty() + '''It is the type of property. + :data:`proptype` is a :class:`~kivy.properties.StringProperty` + ''' + + record_to_undo = BooleanProperty(False) + '''It specifies whether the property change has to be recorded to undo. + It is used when :class:`~designer.core.undo_manager.UndoManager` undoes + or redoes the property change. + :data:`record_to_undo` is a :class:`~kivy.properties.BooleanProperty` + ''' + + kv_code_input = ObjectProperty() + '''It is a reference to the + :class:`~designer.uix.kv_code_input.KVLangArea`. + :data:`kv_code_input` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def set_value(self, value): + '''This function first converts the value of the propwidget, then sets + the new value. If there is some error in setting new value, then it + sets the property value back to oldvalue + ''' + + self.have_error = False + conversion_err = False + oldvalue = getattr(self.propwidget, self.propname) + try: + if isinstance(self.propwidget.property(self.propname), + NumericProperty): + + if value == 'None' or value == '': + value = None + else: + value = float(value) + + except Exception: + conversion_err = True + + designer = get_designer() + if not conversion_err: + try: + setattr(self.propwidget, self.propname, value) + self.kv_code_input.set_property_value(self.propwidget, + self.propname, value, + self.proptype) + if self.record_to_undo: + designer.undo_manager.push_operation( + PropOperation(self, oldvalue, value)) + self.record_to_undo = True + except Exception: + self.have_error = True + setattr(self.propwidget, self.propname, oldvalue) + + +class PropertyOptions(PropertyBase, Label): + '''PropertyOptions to show/set/get options for an OptionProperty + ''' + + def __init__(self, prop, **kwargs): + super(PropertyOptions, self).__init__(**kwargs) + self._chooser = None + self._original_options = prop.options + self._options = prop.options + if self._options and isinstance(self._options[0], list): + # handler to list option properties + opts = [] + for op in self._options: + opts.append(str(op)) + self._options = opts + + def on_propvalue(self, *args): + '''Default handler for 'on_propvalue'. + ''' + + if self.propvalue: + if isinstance(self.propvalue, list): + self.text = str(self.propvalue) + else: + self.text = self.propvalue + else: + self.text = '' + + def on_touch_down(self, touch): + '''Display the option chooser + ''' + d = get_designer() + if d.popup: + return False + if self.collide_point(*touch.pos): + if self._chooser is None: + fake_setting = FakeSettingList() + fake_setting.allow_custom = False + fake_setting.items = self._options + fake_setting.desc = 'Property Options' + fake_setting.group = 'property_options' + content = SettingListContent(setting=fake_setting) + self._chooser = content + + self._chooser.parent = None + self._chooser.selected_items = [self.text] + self._chooser.show_items() + + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + d.popup = Popup( + content=self._chooser, + title='Property Options - ' + self.propname, + size_hint=(None, None), + size=(popup_width, popup_height), + auto_dismiss=False + ) + + self._chooser.bind( + on_apply=self._on_options, + on_cancel=d.close_popup) + + d.popup.open() + return True + + return False + + def _on_options(self, instance, selected_items): + if isinstance(self._original_options[0], list): + new_value = eval(selected_items[0]) + else: + new_value = selected_items[0] + self.propvalue = new_value + self.set_value(new_value) + get_designer().close_popup() + + +class PropertyTextInput(PropertyBase, TextInput): + '''PropertyTextInput is used as widget to display + :class:`~kivy.properties.StringProperty` and + :class:`~kivy.properties.NumericProperty`. + ''' + + def value_changed(self, value, *args): + if value != str(getattr(self.propwidget, self.propname)): + self.set_value(value) + + def insert_text(self, substring, from_undo=False): + '''Override of :class:`~kivy.uix.textinput.TextInput`.insert_text, + it first checks whether the value being entered is valid or not. + If yes, then it enters that value otherwise it doesn't. + For Example, if Property is NumericProperty then it will + first checks if value being entered should be a number + or decimal only. + ''' + if self.proptype == 'NumericProperty' and \ + substring.isdigit() is False and\ + (substring != '.' or '.' in self.text)\ + and substring not in 'None': + return + + super(PropertyTextInput, self).insert_text(substring) + + +class PropertyBoolean(PropertyBase, CheckBox): + '''PropertyBoolean is used as widget to display + :class:`~kivy.properties.BooleanProperty`. + ''' + pass + + +class PropertyViewer(ScrollView): + '''PropertyViewer is used to display property names and their corresponding + value. + ''' + + widget = ObjectProperty(allownone=True) + '''Widget for which properties are displayed. + :data:`widget` is a :class:`~kivy.properties.ObjectProperty` + ''' + + prop_list = ObjectProperty() + '''Widget in which all the properties and their value is added. It is a + :class:`~kivy.gridlayout.GridLayout. + :data:`prop_list` is a :class:`~kivy.properties.ObjectProperty` + ''' + + kv_code_input = ObjectProperty() + '''It is a reference to the KVLangArea. + :data:`kv_code_input` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(PropertyViewer, self).__init__(**kwargs) + self._label_cache = {} + + def on_widget(self, instance, new_widget): + '''Default handler for 'on_widget'. + ''' + self.clear() + if new_widget is not None: + self.discover(new_widget) + + def clear(self): + '''To clear :data:`prop_list`. + ''' + self.prop_list.clear_widgets() + + def discover(self, value): + '''To discover all properties and add their + :class:`~designer.components.property_viewer.PropertyLabel` and + :class:`~designer.components.property_viewer.PropertyBoolean`/ + :class:`~designer.components.property_viewer.PropertyTextInput` + to :data:`prop_list`. + ''' + + add = self.prop_list.add_widget + get_label = self._get_label + props = list(value.properties().keys()) + props.sort() + + for prop in props: + ip = self.build_for(prop) + if not ip: + continue + add(get_label(prop)) + add(ip) + + def _get_label(self, prop): + try: + return self._label_cache[prop] + except KeyError: + lbl = self._label_cache[prop] = PropertyLabel(text=prop) + return lbl + + def build_for(self, name): + '''Creates a EventHandlerTextInput for each property given its name + ''' + + prop = self.widget.property(name) + if isinstance(prop, NumericProperty): + return PropertyTextInput(propwidget=self.widget, propname=name, + proptype='NumericProperty', + kv_code_input=self.kv_code_input) + + elif isinstance(prop, StringProperty): + return PropertyTextInput(propwidget=self.widget, propname=name, + proptype='StringProperty', + kv_code_input=self.kv_code_input) + + elif isinstance(prop, ListProperty): + return PropertyTextInput(propwidget=self.widget, propname=name, + proptype='ListProperty', + kv_code_input=self.kv_code_input) + + elif isinstance(prop, BooleanProperty): + ip = PropertyBoolean(propwidget=self.widget, propname=name, + proptype='BooleanProperty', + kv_code_input=self.kv_code_input) + ip.record_to_undo = True + return ip + + elif isinstance(prop, OptionProperty): + ip = PropertyOptions(prop, propwidget=self.widget, propname=name, + proptype='OptionProperty', + kv_code_input=self.kv_code_input) + return ip + + return None diff --git a/designer/components/run_contextual_view.py b/designer/components/run_contextual_view.py new file mode 100644 index 0000000..5e94cbf --- /dev/null +++ b/designer/components/run_contextual_view.py @@ -0,0 +1,135 @@ +import webbrowser + +from uix.action_items import DesignerActionProfileCheck +from kivy.app import App +from kivy.modules import screen +from kivy.properties import Clock, ObjectProperty, partial +from kivy.uix.actionbar import ContextualActionView + + +class ModulesContView(ContextualActionView): + + mod_screen = ObjectProperty(None) + + __events__ = ('on_module', ) + + def on_module(self, *args, **kwargs): + '''Dispatch the selected module + ''' + self.parent.on_previous(self) + + def on_screen(self, *args): + '''Screen module selected, shows ModScreenContView menu + ''' + if self.mod_screen is None: + self.mod_screen = ModScreenContView() + self.mod_screen.bind(on_run=self.on_screen_module) + self.parent.add_widget(self.mod_screen) + + def on_screen_module(self, *args, **kwargs): + '''when running from screen module + ''' + self.mod_screen.parent.on_previous(self.mod_screen) + self.dispatch('on_module', *args, **kwargs) + + def on_webdebugger(self, *args): + '''when running from webdebugger''' + self.dispatch('on_module', mod='webdebugger', data=[]) + Clock.schedule_once(partial(webbrowser.open, + 'http://localhost:5000/'), 5) + + +class ModScreenContView(ContextualActionView): + + __events__ = ('on_run', ) + + designer = ObjectProperty(None) + '''Instance of Desiger + ''' + + def __init__(self, **kwargs): + super(ModScreenContView, self).__init__(**kwargs) + + # populate emulation devices + devices = self.ids.module_screen_device + + self.designer = App.get_running_app().root + config = self.designer.designer_settings.config_parser + + # load the default values + saved_device = config.getdefault('internal', 'mod_screen_device', '') + saved_orientation = config.getdefault('internal', + 'mod_screen_orientation', '') + saved_scale = config.getdefault('internal', 'mod_screen_scale', '') + + first = True + first_btn = None + for device in sorted(screen.devices): + btn = DesignerActionProfileCheck(group='mod_screen_device', + allow_no_selection=False, config_key=device) + btn.text = screen.devices[device][0] + btn.bind(on_active=self.on_module_settings) + + if first: + btn.checkbox_active = True + first_btn = btn + first = False + else: + if device == saved_device: + first_btn.checkbox.active = False + btn.checkbox_active = True + else: + btn.checkbox_active = False + + devices.add_widget(btn) + for orientation in self.ids.module_screen_orientation.list_action_item: + if orientation.config_key == saved_orientation: + orientation.checkbox_active = True + + for scale in self.ids.module_screen_scale.list_action_item: + if scale.config_key == saved_scale: + scale.checkbox_active = True + + def on_run_press(self, *args): + '''Run button pressed. Analyze settings and dispatch ModulesContView + on run + ''' + device = None + orientation = None + scale = None + + for d in self.ids.module_screen_device.list_action_item: + if d.checkbox.active: + device = d.config_key + break + + for o in self.ids.module_screen_orientation.list_action_item: + if o.checkbox.active: + orientation = o.config_key + break + + for s in self.ids.module_screen_scale.list_action_item: + if s.checkbox.active: + scale = s.config_key + break + + parameter = '%s,%s,scale=%s' % (device, orientation, scale) + + self.dispatch('on_run', mod='screen', data=parameter) + + def on_run(self, *args, **kwargs): + '''Event handler for on_run + ''' + pass + + def on_module_settings(self, instance, *args): + '''Event handle to save Screen Module settings when a different + option is selected + ''' + if instance.checkbox.active: + self.designer.designer_settings.config_parser.set( + 'internal', + instance.group, + instance.config_key + ) + self.designer.designer_settings.config_parser.write() diff --git a/designer/components/start_page.py b/designer/components/start_page.py new file mode 100644 index 0000000..9bb804e --- /dev/null +++ b/designer/components/start_page.py @@ -0,0 +1,98 @@ +import webbrowser + +from utils.utils import get_designer, get_fs_encoding +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.scrollview import ScrollView + + +class DesignerLinkLabel(Button): + '''DesignerLinkLabel displays a http link and opens it in a browser window + when clicked. + ''' + + link = StringProperty(None) + '''Contains the http link to be opened. + :data:`link` is a :class:`~kivy.properties.StringProperty` + ''' + + def on_release(self, *args): + '''Default event handler for 'on_release' event. + ''' + if self.link: + webbrowser.open(self.link) + + +class RecentItem(BoxLayout): + path = StringProperty('') + '''Contains the application path + :data:`path` is a :class:`~kivy.properties.StringProperty` + ''' + + __events__ = ('on_press', ) + + def on_press(self, *args): + '''Item pressed + ''' + + +class RecentFilesBox(ScrollView): + '''Container consistings of buttons, with their names specifying + the recent files. + ''' + + grid = ObjectProperty(None) + '''The grid layout consisting of all buttons. + This property is an instance of :class:`~kivy.uix.gridlayout` + :data:`grid` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(RecentFilesBox, self).__init__(**kwargs) + + def add_recent(self, list_files): + '''To add buttons representing Recent Files. + :param list_files: array of paths + ''' + for p in list_files: + if isinstance(p, bytes): + p = p.decode(get_fs_encoding()) + recent_item = RecentItem(path=p) + self.grid.add_widget(recent_item) + recent_item.bind(on_press=self.btn_release) + self.grid.height += recent_item.height + + self.grid.height = max(self.grid.height, self.height) + + def btn_release(self, instance): + '''Event Handler for 'on_release' of an event. + ''' + d = get_designer() + d._perform_open(instance.path) + + +class DesignerStartPage(BoxLayout): + + recent_files_box = ObjectProperty(None) + '''This property is an instance + of :class:`~designer.components.start_page.RecentFilesBox` + :data:`recent_files_box` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_open_down', 'on_new_down', 'on_help') + + def on_open_down(self, *args): + '''Default Event Handler for 'on_open_down' + ''' + pass + + def on_new_down(self, *args): + '''Default Event Handler for 'on_new_down' + ''' + pass + + def on_help(self, *args): + '''Default Event Handler for 'on_help' + ''' + pass diff --git a/designer/components/statusbar.py b/designer/components/statusbar.py new file mode 100644 index 0000000..36b14ae --- /dev/null +++ b/designer/components/statusbar.py @@ -0,0 +1,232 @@ +from kivy.clock import Clock +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.label import Label +from kivy.uix.tabbedpanel import ( + TabbedPanel, + TabbedPanelContent, + TabbedPanelHeader, +) + + +class StatusNavBarButton(Button): + '''StatusNavBarButton is a :class:`~kivy.uix.button` representing + the Widgets in the Widget hierarchy of currently selected widget. + ''' + + node = ObjectProperty() + + +class StatusNavBarSeparator(Label): + '''StatusNavBarSeparator :class:`~kivy.uix.label.Label` + Used to separate two Widgets by '>' + ''' + + pass + + +class StatusNavbar(BoxLayout): + pass + + +class StatusMessage(BoxLayout): + + message = StringProperty('') + '''Message visible on the status bar + :data:`message` is an + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + icon = StringProperty('') + '''Message icon path + :data:`icon` is an + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + img = ObjectProperty(None) + '''Instance of notification type icon + :data:`img` is an + :class:`~kivy.properties.ObjectProperty` and defaults to None + ''' + + def show_message(self, message, duration=5, notification_type=None): + self.message = message + icon = '' + if notification_type == 'info': + icon = 'icons/info.png' + elif notification_type == 'error': + icon = 'icons/error.png' + elif notification_type == 'loading': + icon = 'icons/loading.gif' + + if icon: + self.img.opacity = 1 + self.img.source = icon + else: + self.img.opacity = 0 + if duration > 0: + Clock.schedule_once(self.clear_message, duration) + + def clear_message(self, *args): + self.img.opacity = 0 + self.message = '' + + +class StatusInfo(BoxLayout): + + message = StringProperty('') + '''Message visible on the status bar + :data:`message` is an + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + info = StringProperty('') + '''Info visible on the status bar + :data:`info` is an + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + branch = StringProperty('') + '''Branch name visible on the status bar + :data:`branch` is an + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + def update_info(self, info, branch_name=None): + template = info + if branch_name is not None: + self.branch = branch_name + + if self.branch: + template += ' | ' + self.branch + + self.message = template + + +class StatusBar(BoxLayout): + '''StatusBar used to display Widget hierarchy of currently selected + widget and to display messages. + ''' + + app = ObjectProperty() + '''Reference to current app instance. + :data:`app` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + navbar = ObjectProperty() + '''To be used as parent of + :class:`~designer.components.statusbar.StatusNavBarButton` + and :class:`~designer.components.statusbar.StatusNavBarSeparator`. + :data:`navbar` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + status_message = ObjectProperty() + '''Instance of :class:`~designer.components.statusbar.StatusMessage` + :class:`~kivy.properties.ObjectProperty` + ''' + + status_info = ObjectProperty() + '''Instance of :class:`~designer.components.statusbar.StatusInfo` + :class:`~kivy.properties.ObjectProperty` + ''' + + playground = ObjectProperty() + '''Instance of + :data:`playground` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_message_press', 'on_info_press', ) + + def __init__(self, **kwargs): + super(StatusBar, self).__init__(**kwargs) + self.update_navbar = Clock.create_trigger(self._update_navbar) + self.update_nav_size = Clock.create_trigger(self._update_content_width) + + def _update_navbar(self, *args): + '''To update navbar with the parents of currently selected Widget. + ''' + self.navbar.clear_widgets() + wid = self.app.widget_focused + if not wid: + self.update_nav_size() + return + + # get parent list, until app.root.playground.root + children = [] + while wid: + if wid == self.playground.sandbox or\ + wid == self.playground.sandbox.children[0]: + break + + if isinstance(wid, TabbedPanelContent): + _wid = wid + wid = wid.parent.current_tab + children.append(StatusNavBarButton(node=wid)) + wid = _wid.parent + + elif isinstance(wid, TabbedPanelHeader): + children.append(StatusNavBarButton(node=wid)) + _wid = wid + while _wid and not isinstance(_wid, TabbedPanel): + _wid = _wid.parent + wid = _wid + + children.append(StatusNavBarButton(node=wid)) + wid = wid.parent + + count = len(children) + for index, child in enumerate(reversed(children)): + self.navbar.add_widget(child) + if index < count - 1: + self.navbar.add_widget(StatusNavBarSeparator()) + else: + child.state = 'down' + + def on_app(self, instance, app, *args): + app.bind(widget_focused=self._update_navbar) + + def _update_content_width(self, *args): + '''Updates the statusbar's children sizes to save space + ''' + nav = self.navbar.parent + nav_c = self.navbar.children + mes = self.status_message + if nav_c == 0: + mes.size_hint_x = 0.9 + nav.size_hint_x = None + nav.width = 0 + elif mes.message: + mes.size_hint_x = 0.4 + nav.size_hint_x = 0.5 + elif not mes.message: + mes.size_hint_x = None + mes.width = 0 + nav.size_hint_x = 0.9 + + def show_message(self, message, duration=5, notification_type=None, *args): + '''Shows a message. Use type to change the icon and the duration + in seconds. Set duration = -1 to undefined time + :param notification_type: types: info, error, loading + :param duration: notification duration in seconds + :param message: message to display + ''' + self.status_message.show_message(message, duration, notification_type) + + def update_info(self, info, branch_name=None): + '''Updates the info message + ''' + self.status_info.update_info(info, branch_name) + + def on_message_press(self, *args): + '''Event handler to message widget touch down + ''' + pass + + def on_info_press(self, *args): + '''Event handler to info widget touch down + ''' + pass diff --git a/designer/components/toolbox.py b/designer/components/toolbox.py new file mode 100644 index 0000000..0ba9895 --- /dev/null +++ b/designer/components/toolbox.py @@ -0,0 +1,130 @@ +from utils.toolbox_widgets import toolbox_widgets +from kivy.clock import Clock +from kivy.factory import Factory +from kivy.metrics import pt +from kivy.properties import ObjectProperty +from kivy.uix.accordion import AccordionItem +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button + + +class ToolboxCategory(AccordionItem): + '''ToolboxCategory is responsible for grouping and showing + :class:`~designer.components.toolbox.ToolboxButton` + of same class into one category. + ''' + + gridlayout = ObjectProperty(None) + '''An instance of :class:`~kivy.uix.gridlayout.GridLayout`. + :data:`gridlayout` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + +class ToolboxButton(Button): + '''ToolboxButton is a subclass of :class:`~kivy.uix.button.Button`, + to display class of Widgets in + :class:`~designer.components.toolbox.ToolboxCategory`. + ''' + + def __init__(self, **kwargs): + self.register_event_type('on_press_and_touch') + super(ToolboxButton, self).__init__(**kwargs) + + def on_touch_down(self, touch): + '''Default handler for 'on_touch_down' + ''' + if self.collide_point(*touch.pos): + self.dispatch('on_press_and_touch', touch) + return super(ToolboxButton, self).on_touch_down(touch) + + def on_press_and_touch(self, touch): + '''Default handler for 'on_press_and_touch' event + ''' + pass + + +class Toolbox(BoxLayout): + '''Toolbox is used to display all the widgets in designer.common.widgets + in their respective classes. + ''' + + accordion = ObjectProperty() + '''An instance to :class:`~kivy.uix.accordion.Accordion`, + used to show Widgets in their groups. + :data:`accordion` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + app = ObjectProperty() + '''An instance to the current running app. + :data:`app` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(Toolbox, self).__init__(**kwargs) + Clock.schedule_once(self.discover_widgets) + self.custom_category = None + self._list = [] + + def discover_widgets(self, *largs): + '''To create and add ToolboxCategory and ToolboxButton for widgets in + designer.common.widgets + ''' + # for now, don't do auto detection of widgets. + # just do manual discovery, and tagging. + + categories = list(set([x[1] for x in toolbox_widgets])) + categories.sort() + for category in categories: + toolbox_category = ToolboxCategory(title=category) + self.accordion.add_widget(toolbox_category) + + cat_widgets = [] + for widget in toolbox_widgets: + if widget[1] == category: + cat_widgets.append(widget) + + cat_widgets.sort() + for widget in cat_widgets: + toolbox_category.gridlayout.add_widget( + ToolboxButton(text=widget[0])) + + self.accordion.children[-1].collapse = False + + def cleanup(self): + '''To clean all the children in self.custom_category. + ''' + if self.custom_category: + self.accordion.remove_widget(self.custom_category) + Factory.register('BoxLayout', module='kivy.uix.boxlayout') + self.custom_category = ToolboxCategory(title='App Widgets') + self._list.append(self.custom_category) + + def update_app_widgets(self): + '''To add/update self.custom_category with new custom classes loaded + by project. + ''' + if self.custom_category: + self.accordion.remove_widget(self.custom_category) + self._list = [] + self.custom_category = ToolboxCategory(title='App Widgets') + self._list.append(self.custom_category) + + self.accordion.add_widget(self.custom_category) + + custom_widgets = [] + for widget in toolbox_widgets: + if widget[1] == 'custom': + custom_widgets.append(widget) + + custom_widgets.sort() + for widget in custom_widgets: + self.custom_category.gridlayout.add_widget( + ToolboxButton(text=widget[0])) + + # Setting appropriate height to gridlayout to enable scrolling + self.custom_category.gridlayout.size_hint_y = None + self.custom_category.gridlayout.height = \ + (len(self.custom_category.gridlayout.children) + 5) * pt(22) diff --git a/designer/components/ui_creator.py b/designer/components/ui_creator.py new file mode 100644 index 0000000..18a421b --- /dev/null +++ b/designer/components/ui_creator.py @@ -0,0 +1,127 @@ +from utils.utils import get_designer +from kivy.app import App +from kivy.clock import Clock +from kivy.properties import ObjectProperty +from kivy.uix.floatlayout import FloatLayout + + +class UICreator(FloatLayout): + '''UICreator is the Wigdet responsible for editing/creating UI of project + ''' + + toolbox = ObjectProperty(None) + '''Reference to the :class:`~designer.components.toolbox.Toolbox` instance. + :data:`toolbox` is an :class:`~kivy.properties.ObjectProperty` + ''' + + propertyviewer = ObjectProperty(None) + '''Reference to the + :class:`~designer.components.property_viewer.PropertyViewer` + instance. :data:`propertyviewer` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + playground = ObjectProperty(None) + '''Reference to the :class:`~designer.components.playground.Playground` + instance.:data:`playground` is an :class:`~kivy.properties.ObjectProperty` + ''' + + widgettree = ObjectProperty(None) + '''Reference to the :class:`~designer.components.widgets_tree.WidgetsTree` + instance.:data:`widgettree` is an :class:`~kivy.properties.ObjectProperty` + ''' + + kv_code_input = ObjectProperty(None) + '''Reference to the :class:`~designer.uix.KVLangArea` instance. + :data:`kv_code_input` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + splitter_kv_code_input = ObjectProperty(None) + '''Reference to the splitter parent of kv_code_input. + :data:`splitter_kv_code_input` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + grid_widget_tree = ObjectProperty(None) + '''Reference to the grid parent of widgettree. + :data:`grid_widget_tree` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + splitter_property = ObjectProperty(None) + '''Reference to the splitter parent of propertyviewer. + :data:`splitter_property` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + splitter_widget_tree = ObjectProperty(None) + '''Reference to the splitter parent of widgettree. + :data:`splitter_widget_tree` is an + :class:`~kivy.properties.ObjectProperty` + ''' + + error_console = ObjectProperty(None) + '''Instance of :class:`~kivy.uix.codeinput.CodeInput` used for displaying + exceptions. + ''' + + kivy_console = ObjectProperty(None) + '''Instance of :class:`~designer.components.kivy_console.KivyConsole`. + ''' + + python_console = ObjectProperty(None) + '''Instance of :class:`~designer.uix.py_console.PythonConsole` + ''' + + tab_pannel = ObjectProperty(None) + '''Instance of + :class:`~designer.components.designer_content.DesignerTabbedPanel` + containing error_console, kivy_console and kv_lang_area + ''' + + eventviewer = ObjectProperty(None) + + def __init__(self, **kwargs): + super(UICreator, self).__init__(**kwargs) + Clock.schedule_once(self._setup_everything) + + def reload_btn_pressed(self, *args): + '''Default handler for 'on_release' event of "Reload" button. + ''' + self.kv_code_input.func_reload_kv(force=True) + + def on_touch_down(self, *args): + '''Default handler for 'on_touch_down' event. + ''' + if self.playground and self.playground.keyboard: + self.playground.keyboard.release() + + return super(UICreator, self).on_touch_down(*args) + + def on_show_edit(self, *args): + '''Event handler for 'on_show_edit' event. + ''' + App.get_running_app().root.on_show_edit(*args) + + def cleanup(self): + '''To clean up everything before loading new project. + ''' + self.playground.cleanup() + self.kv_code_input.text = '' + + def _setup_everything(self, *args): + '''To setup all the references in between widget + ''' + + self.kv_code_input.playground = self.playground + self.playground.kv_code_input = self.kv_code_input + self.playground.kv_code_input.bind( + on_reload_kv=self.playground.on_reload_kv) + self.playground.widgettree = self.widgettree + self.propertyviewer.kv_code_input = self.kv_code_input + self.eventviewer.kv_code_input = self.kv_code_input + self.py_console.remove_widget(self.py_console.children[1]) + d = get_designer() + if self.kv_code_input not in d.code_inputs: + d.code_inputs.append(self.kv_code_input) diff --git a/designer/components/widgets_tree.py b/designer/components/widgets_tree.py new file mode 100644 index 0000000..09c74d0 --- /dev/null +++ b/designer/components/widgets_tree.py @@ -0,0 +1,154 @@ +from utils.toolbox_widgets import toolbox_widgets +from utils.utils import get_current_project +from kivy.clock import Clock +from kivy.properties import BooleanProperty, ObjectProperty +from kivy.uix.scrollview import ScrollView +from kivy.uix.tabbedpanel import TabbedPanel +from kivy.uix.treeview import TreeViewLabel + + +class WidgetTreeElement(TreeViewLabel): + '''WidgetTreeElement represents each node in WidgetsTree + ''' + node = ObjectProperty(None) + + +class WidgetsTree(ScrollView): + '''WidgetsTree class is used to display the Root Widget's Tree in a + Tree hierarchy. + ''' + playground = ObjectProperty(None) + '''This property is an instance of + :class:`~designer.components.playground.Playground` + :data:`playground` is a :class:`~kivy.properties.ObjectProperty` + ''' + + tree = ObjectProperty(None) + '''This property is an instance of :class:`~kivy.uix.treeview.TreeView`. + This TreeView is responsible for showing Root Widget's Tree. + :data:`tree` is a :class:`~kivy.properties.ObjectProperty` + ''' + + dragging = BooleanProperty(False) + '''Specifies whether a node is dragged or not. + :data:`dragging` is a :class:`~kivy.properties.BooleanProperty` + ''' + + selected_widget = ObjectProperty(allownone=True) + '''Current selected widget. + :data:`dragging` is a :class:`~kivy.properties.ObjectProperty` + ''' + + def __init__(self, **kwargs): + super(WidgetsTree, self).__init__(**kwargs) + self.refresh = Clock.create_trigger(self._refresh) + self._widget_cache = {} + + def recursive_insert(self, node, treenode): + '''This function will add a node to TreeView, by recursively travelling + through the Root Widget's Tree. + ''' + + if node is None: + return + + b = self._get_widget(node) + self.tree.add_node(b, treenode) + class_rules = get_current_project().app_widgets + root_widget = self.playground.root + + is_child_custom = False + for rule_name in class_rules: + if rule_name == type(node).__name__: + is_child_custom = True + break + + is_child_complex = False + for widget in toolbox_widgets: + if widget[0] == type(node).__name__ and widget[1] == 'complex': + is_child_complex = True + break + + if root_widget == node or (not is_child_custom and + not is_child_complex): + if isinstance(node, TabbedPanel): + self.insert_for_tabbed_panel(node, b) + else: + for child in node.children: + self.recursive_insert(child, b) + + def insert_for_tabbed_panel(self, node, treenode): + '''This function will insert nodes in tree specially for TabbedPanel. + ''' + for tab in node.tab_list: + b = self._get_widget(tab) + self.tree.add_node(b, treenode) + self.recursive_insert(tab.content, b) + + def _get_widget(self, node): + try: + wid = self._widget_cache[node] + if not wid: + raise KeyError() + except KeyError: + wid = WidgetTreeElement(node=node) + self._widget_cache[node] = wid.proxy_ref + if wid.parent_node: + self.tree.remove_node(wid) + return wid + + def _clear_tree(self, tree, node): + remove_node = tree.remove_node + for n in node.nodes[:]: + self._clear_tree(tree, n) + remove_node(n) + + def _refresh(self, *l): + '''This function will refresh the tree. It will first remove all nodes + and then insert them using recursive_insert + ''' + self._clear_tree(self.tree, self.tree.root) + self.recursive_insert(self.playground.root, self.tree.root) + self._clean_cache() + + def _clean_cache(self): + for node, wid in list(self._widget_cache.items()): + try: + if node and node.parent and wid and wid.parent_node: + continue + except ReferenceError: + pass + del self._widget_cache[node] + + def on_touch_up(self, touch): + '''Default event handler for 'on_touch_up' event. + ''' + self.dragging = False + Clock.unschedule(self._start_dragging) + return super(WidgetsTree, self).on_touch_up(touch) + + def on_touch_down(self, touch): + '''Default event handler for 'on_touch_down' event. + ''' + if self.collide_point(*touch.pos) and not self.dragging: + self.dragging = True + self.touch = touch + Clock.schedule_once(self._start_dragging, 2) + node = self.tree.get_node_at_pos((self.touch.x, self.touch.y)) + if node: + self.selected_widget = node.node + self.playground.selected_widget = self.selected_widget + else: + self.selected_widget = None + self.playground.selected_widget = None + + return super(WidgetsTree, self).on_touch_down(touch) + + def _start_dragging(self, *args): + '''This function will start dragging the widget. + ''' + if self.dragging and self.selected_widget: + self.playground.selected_widget = self.selected_widget + self.playground.dragging = False + self.playground.touch = self.touch + self.playground.start_widget_dragging() diff --git a/designer/config.ini b/designer/config.ini new file mode 100644 index 0000000..3982e3e --- /dev/null +++ b/designer/config.ini @@ -0,0 +1,58 @@ +[global] +python_shell_path = +num_recent_files = 10 +num_max_kivy_console = 200 +auto_save_time = 5 +code_input_theme = emacs + +[buildozer] +buildozer_path = + +[hanga] +hanga_api_key = + +[desktop] +save_window_size = 1 +exit_on_escape = 0 + +[internal] +window_width = 800 +window_height = 600 +default_profile = +mod_screen_scale = 1.0 +mod_screen_orientation = portrait +mod_screen_device = + +[view] +actn_chk_proj_tree = True +actn_chk_prop_event = True +actn_chk_widget_tree = True +actn_chk_status_bar = True +actn_chk_kv_lang_area = True + +[shortcuts] +new_file = ['ctrl'] + n +new_project = ['ctrl', 'shift'] + n +open_project = ['ctrl'] + o +save = ['ctrl'] + s +save_as = ['ctrl', 'shift'] + s +close_project = ['alt', 'ctrl'] + c +recent = ['alt', 'ctrl'] + r +settings = ['alt', 'ctrl'] + s +exit = ['ctrl'] + q +fullscreen = [] + f11 +run = [] + f5 +stop = [] + f6 +clean = [] + f7 +build = ['ctrl'] + f5 +rebuild = ['ctrl', 'shift'] + f5 +buildozer_init = ['ctrl', 'shift'] + b +export_png = ['ctrl'] + p +check_pep8 = ['ctrl', 'shift'] + p +create_setup_py = ['ctrl'] + t +create_gitignore = ['ctrl', 'shift'] + t +help = [] + f1 +kivy_docs = ['ctrl'] + f1 +kd_docs = ['ctrl', 'shift'] + f1 +kd_repo = ['ctrl'] + r +about = [] + f10 \ No newline at end of file diff --git a/designer/core/__init__.py b/designer/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/core/builder.py b/designer/core/builder.py new file mode 100644 index 0000000..5091242 --- /dev/null +++ b/designer/core/builder.py @@ -0,0 +1,603 @@ +import os +import shutil +import sys + +from uix.confirmation_dialog import ConfirmationDialog +from utils import constants +from utils.utils import ( + get_current_project, + get_fs_encoding, + get_kd_data_dir, + ignore_proj_watcher) +from kivy.event import EventDispatcher +from kivy.properties import ( + Clock, + ConfigParser, + ConfigParserProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.popup import Popup + + +class Builder(EventDispatcher): + '''Builder interface + ''' + + def __init__(self, profiler): + self.profiler = profiler + self.designer = self.profiler.designer + self.designer_settings = self.designer.designer_settings + self.project_watcher = self.designer.project_watcher + self.proj_settings = self.designer.proj_settings + self.ui_creator = self.designer.ui_creator + self.run_command = self.ui_creator.kivy_console.run_command + self.can_run = False # if the env if ok to run the project + self.last_command = None # last method executed. + if not self.profiler.pro_mode: + self.profiler.pro_mode = 'Debug' + + +class Buildozer(Builder): + '''Class to handle Buildozer builder + ''' + + def __init__(self, profiler): + super(Buildozer, self).__init__(profiler) + self.buildozer_path = '' + + def _initialize(self): + '''Try to get the buildozer path and check required variables + If there is something wrong shows an alert. + ''' + if self.designer.popup: + self.can_run = False + self.profiler.dispatch('on_error', 'You must close all popups ' + 'before building your project') + return + # first, check if buildozer is set + self.buildozer_path = self.designer_settings.config_parser.getdefault( + 'buildozer', + 'buildozer_path', + '' + ) + + if self.buildozer_path == '': + self.profiler.dispatch('on_error', + 'Buildozer Path not specified.' + "\n\nUpdate it on File -> Settings") + self.can_run = False + return + + envs = self.proj_settings.config_parser.getdefault( + 'env variables', + 'env', + '' + ) + + for env in envs.split(' '): + self.ui_creator.kivy_console.environment[ + env[:env.find('=')]] = env[env.find('=') + 1:] + # check if buildozer.spec exists + if not os.path.isfile(os.path.join(self.profiler.project_path, + 'buildozer.spec')): + confirm_dlg = ConfirmationDialog( + message='buildozer.spec not found.\n' + 'Do you want to create it now?') + self.designer.popup = Popup(title='Buildozer', + content=confirm_dlg, + size_hint=(None, None), + size=('200pt', '150pt'), + auto_dismiss=False) + confirm_dlg.bind(on_ok=self._perform_create_spec, + on_cancel=self.designer.close_popup) + self.designer.popup.open() + self.can_run = False + return + + # TODO check if buildozer source.dir and main file exists + + self.can_run = True + + def _perform_create_spec(self, *args): + '''Creates the default buildozer.spec file + ''' + templates_dir = os.path.join(get_kd_data_dir(), + constants.DIR_NEW_TEMPLATE) + shutil.copy(os.path.join(templates_dir, 'default.spec'), + os.path.join(self.profiler.project_path, 'buildozer.spec')) + + self.designer.designer_content.update_tree_view(get_current_project()) + self.designer.close_popup() + self.last_command() + + def _create_command(self, extra): + '''Generate the buildozer command + ''' + self.project_watcher.pause_watching() + self._initialize() + self.ui_creator.tab_pannel.switch_to( + self.ui_creator.tab_pannel.tab_list[2]) + + cd = 'cd ' + self.profiler.project_path + args = [self.buildozer_path] + if self.profiler.pro_verbose: + args.append('--verbose') + args.append(self.profiler.pro_target.lower()) # android or ios + args += extra + + return [cd, " ".join(args)] + + def build(self, *args): + '''Build the Buildozer project. + Will read the necessary information from the profile and build the app + ''' + build_mode = self.profiler.pro_mode.lower() + cmd = self._create_command([build_mode]) + if not self.can_run: + self.last_command = self.build + return + self.run_command(cmd) + + self.profiler.dispatch('on_message', 'Building project...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_build) + + def rebuild(self, *args): + '''Update project dependencies, and build it again + ''' + self.clean() + self.profiler.bind(on_clean=self._rebuild) + + def _rebuild(self, *args): + '''Perform the project rebuild + ''' + cmd = self._create_command(['update']) + if not self.can_run: + self.last_command = self.rebuild + return + self.run_command(cmd) + + self.profiler.dispatch('on_message', + 'Updating project dependencies...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.build) + + def run(self, *args, **kwargs): + '''Run the build command and then run the application on the device + ''' + self.build() + if not self.can_run: + self.last_command = self.run + return + if not self.profiler.pro_install: + self.profiler.bind(on_build=self.deploy) + self.profiler.bind(on_deploy=self._run) + + def _run(self, *args): + '''Perform the buildozer run + ''' + cmds = ['run'] + if self.profiler.pro_debug and self.profiler.pro_target == 'Android': + cmds.append('logcat') + cmd = self._create_command(cmds) + if not self.can_run: + return + self.run_command(cmd) + + self.profiler.dispatch('on_message', 'Running on device...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_run) + + def deploy(self, *args): + '''Perform the buildozer deploy + ''' + cmd = self._create_command(['deploy']) + if not self.can_run: + return + self.run_command(cmd) + + self.profiler.dispatch('on_message', 'Installing on device...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_deploy) + + def clean(self, *args): + '''Clean the project directory + ''' + cmd = self._create_command(['clean']) + if not self.can_run: + self.last_command = self.clean + return + self.run_command(cmd) + + self.profiler.dispatch('on_message', 'Cleaning project...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_clean) + + def on_clean(self, *args): + '''on_clean event handler + ''' + self.ui_creator.kivy_console.unbind(on_command_list_done=self.on_clean) + + self.project_watcher.resume_watching(delay=0) + + self.profiler.dispatch('on_message', 'Project clean', 5) + self.profiler.dispatch('on_clean') + + def on_build(self, *args): + '''on_build event handler + ''' + self.ui_creator.kivy_console.unbind(on_command_list_done=self.on_build) + + self.project_watcher.resume_watching(delay=0) + + self.profiler.dispatch('on_message', 'Build complete', 5) + self.profiler.dispatch('on_build') + + if self.profiler.pro_install: + self.deploy() + + def on_deploy(self, *args): + '''on_build event handler + ''' + self.ui_creator.kivy_console.unbind(on_command_list_done=self.on_deploy) + + self.project_watcher.resume_watching(delay=0) + + self.profiler.dispatch('on_message', 'Installed on device', 5) + self.profiler.dispatch('on_deploy') + + def on_stop(self, *args): + '''on_stop event handler + ''' + self.ui_creator.kivy_console.unbind(on_command_list_done=self.on_stop) + + self.profiler.dispatch('on_stop') + + def on_run(self, *args): + '''on_run event handler + ''' + self.ui_creator.kivy_console.unbind(on_command_list_done=self.on_run) + + self.project_watcher.resume_watching(delay=0) + + self.profiler.dispatch('on_message', '', 1) + self.profiler.dispatch('on_run') + self.designer.ids.actn_btn_stop_proj.disabled = True + + +class Hanga(Builder): + '''Class to handle Hanga builder + ''' + + def __init__(self, profiler): + super(Hanga, self).__init__(profiler) + + +class Desktop(Builder): + '''Class to handle Desktop builder + ''' + + def __init__(self, profiler): + super(Desktop, self).__init__(profiler) + self.python_path = '' + self.args = '' + # TODO check if buildozer source.dir and main file is set, if so + # use this file + + def _get_python(self): + '''Initialize python variables + If there is something wrong shows an alert + ''' + self.python_path = self.designer_settings.config_parser.getdefault( + 'global', + 'python_shell_path', + '' + ) + + if self.python_path == '': + self.profiler.dispatch('on_error', 'Python Shell Path not ' + 'specified.' + '\n\nUpdate it on \'File\' -> \'Settings\'') + self.can_run = False + return + + self.args = self.proj_settings.config_parser.getdefault( + 'arguments', + 'arg', + '' + ) + + envs = self.proj_settings.config_parser.getdefault( + 'env variables', + 'env', + '' + ) + + for env in envs.split(' '): + self.ui_creator.kivy_console.environment[ + env[:env.find('=')]] = env[env.find('=') + 1:] + + self.can_run = True + + def _perform_kill_run(self, *args): + '''Stop the running project/command and then run the project + ''' + self.designer.close_popup() + self.stop() + Clock.schedule_once(self.run, 1) + + def run(self, *args, **kwargs): + '''Run the project using Python + ''' + if self.designer.popup: + self.profiler.dispatch('on_error', 'You must close all popups ' + 'before building your project') + return + mod = kwargs.get('mod', '') + data = kwargs.get('data', []) + + self._get_python() + if not self.can_run: + return + + py_main = os.path.join(self.profiler.project_path, 'main.py') + if isinstance(py_main, bytes): + py_main = py_main.decode(get_fs_encoding()) + + if not os.path.isfile(py_main): + self.profiler.dispatch('on_error', 'Cannot find main.py') + return + if sys.platform[0] == 'w': + py_main = py_main.replace(' ', '\x01') + else: + py_main = py_main.replace(' ', '\\ ') + cmd = '' + if mod == '': + cmd = '%s %s %s' % (self.python_path, py_main, self.args) + elif mod == 'screen': + cmd = '%s %s -m screen:%s %s' % (self.python_path, py_main, + data, self.args) + else: + cmd = '%s %s -m %s %s' % (self.python_path, py_main, + mod, self.args) + + status = self.run_command(cmd) + + # popen busy + if status is False: + confirm_dlg = ConfirmationDialog( + message="There is another command running.\n" + "Do you want to stop it to run your project?") + self.designer.popup = Popup(title='Kivy Designer', + content=confirm_dlg, + size_hint=(None, None), + size=('300pt', '150pt'), + auto_dismiss=False) + confirm_dlg.bind(on_ok=self._perform_kill_run, + on_cancel=self.designer.close_popup) + self.designer.popup.open() + return + + self.ui_creator.tab_pannel.switch_to( + self.ui_creator.tab_pannel.tab_list[2]) + + self.profiler.dispatch('on_message', 'Running main.py...') + self.profiler.dispatch('on_run') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_stop) + + def stop(self, *args): + '''If there is a process running, it'll be stopped + ''' + self.ui_creator.kivy_console.kill_process() + self.profiler.dispatch('on_stop') + self.profiler.dispatch('on_message', '', 1) + + def clean(self, *args): + '''Remove .pyc files and __pycache__ folder + ''' + # here it's necessary to stop the listener as long as the + # python is managing files + self.project_watcher.pause_watching() + for _file in os.listdir(self.profiler.project_path): + ext = _file.split('.')[-1] + if ext == 'pyc': + os.remove(os.path.join(self.profiler.project_path, _file)) + __pycache__ = os.path.join(self.profiler.project_path, '__pycache__') + if os.path.exists(__pycache__): + shutil.rmtree(__pycache__) + + self.project_watcher.resume_watching(delay=1) + self.profiler.dispatch('on_message', 'Project cleaned', 5) + + def build(self, *args): + '''Compile all .py to .pyc + ''' + self._get_python() + if not self.can_run: + return + + self.project_watcher.pause_watching() + proj_path = self.profiler.project_path + if isinstance(proj_path, bytes): + proj_path = proj_path.decode(get_fs_encoding()) + + self.run_command( + '%s -m compileall -l %s' % (self.python_path, proj_path)) + + self.ui_creator.tab_pannel.switch_to( + self.ui_creator.tab_pannel.tab_list[2]) + + self.profiler.dispatch('on_message', 'Building project...') + self.ui_creator.kivy_console.bind(on_command_list_done=self.on_build) + + def rebuild(self, *args): + '''Clean and build the project + ''' + self.clean() + self.build() + + def on_build(self, *args): + '''on_build event handler + ''' + self.project_watcher.resume_watching(delay=1) + self.profiler.dispatch('on_message', 'Build complete', 5) + self.profiler.dispatch('on_build') + + def on_stop(self, *args): + '''on_stop event handler + ''' + self.profiler.dispatch('on_message', '', 1) + self.profiler.dispatch('on_stop') + + +class Profiler(EventDispatcher): + profile_path = StringProperty('') + ''' Profile settings path + :class:`~kivy.properties.StringProperty` and defaults to ''. + ''' + + project_path = StringProperty('') + ''' Project path + :class:`~kivy.properties.StringProperty` and defaults to ''. + ''' + + designer = ObjectProperty(None) + '''Reference of :class:`~designer.app.Designer`. + :data:`designer` is a :class:`~kivy.properties.ObjectProperty` + ''' + + profile_config = ObjectProperty(None) + '''Reference to a ConfigParser with the profile settings + :class:`~kivy.properties.ObjectProperty` and defaults to None. + ''' + + pro_name = ConfigParserProperty('', 'profile', 'name', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile name + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_builder = ConfigParserProperty('', 'profile', 'builder', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile builder + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_target = ConfigParserProperty('', 'profile', 'target', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile target + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_mode = ConfigParserProperty('', 'profile', 'mode', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile builder + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_install = ConfigParserProperty('', 'profile', 'install', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile install_on_device + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_debug = ConfigParserProperty('', 'profile', 'debug', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile debug mode + :class:`~kivy.properties.ConfigParserProperty` + ''' + + pro_verbose = ConfigParserProperty('', 'profile', 'verbose', 'profiler') + '''Reference to a ConfigParser with the profile settings + Get the profile verbose mode + :class:`~kivy.properties.ConfigParserProperty` + ''' + + builder = ObjectProperty(None) + '''Reference to the builder class. Can be Hanga, Buildozer or Desktop + :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_run', 'on_stop', 'on_error', 'on_message', 'on_build', + 'on_deploy', 'on_clean') + + def __init__(self, **kwargs): + super(Profiler, self).__init__(**kwargs) + self.profile_config = ConfigParser(name='profiler') + + def run(self, *args, **kwargs): + '''Run project + ''' + self.builder.run(*args, **kwargs) + + def stop(self): + '''Stop project + ''' + self.builder.stop() + + def clean(self): + '''Clean project + ''' + self.builder.clean() + + def build(self): + '''Build project + ''' + self.builder.build() + + def rebuild(self): + '''Rebuild project + ''' + self.builder.rebuild() + + def load_profile(self, prof_path, proj_path): + '''Read the settings + ''' + self.profile_path = prof_path + self.project_path = proj_path + + self.profile_config.read(self.profile_path) + + if self.pro_target == 'Desktop': + self.builder = Desktop(self) + else: + if self.pro_builder == 'Buildozer': + self.builder = Buildozer(self) + elif self.pro_builder == 'Hanga': + # TODO implement hanga + self.builder = Desktop(self) + self.dispatch('on_error', 'Hanga Builder not yet implemented!\n' + 'Using Desktop') + else: + self.builder = Desktop(self) + + def on_error(self, *args): + '''on_error event handler + ''' + pass + + def on_message(self, *args): + '''on_message event handler + ''' + pass + + def on_run(self, *args): + '''on_run event handler + ''' + pass + + def on_stop(self, *args): + '''on_stop event handler + ''' + pass + + def on_build(self, *args): + '''on_build event handler + ''' + pass + + def on_deploy(self, *args): + '''on_deploy event handler + ''' + pass + + def on_clean(self, *args): + '''on_clean event handler + ''' + pass diff --git a/designer/core/profile_settings.py b/designer/core/profile_settings.py new file mode 100644 index 0000000..d769f8b --- /dev/null +++ b/designer/core/profile_settings.py @@ -0,0 +1,256 @@ +import os +import os.path +import shutil + +from uix.confirmation_dialog import ConfirmationDialog +from utils import constants +from utils.utils import get_config_dir, get_kd_data_dir +from kivy.config import ConfigParser +from kivy.properties import DictProperty, ObjectProperty +from kivy.uix.popup import Popup +from kivy.uix.settings import ( + ContentPanel, + InterfaceWithSidebar, + MenuSidebar, + Settings, +) + + +class ProfileContentPanel(ContentPanel): + ''' ContentPanel with a custom design and custom events + ''' + + __events__ = ('on_current_panel', ) + + def __int__(self, **kwargs): + super(ProfileContentPanel, self).__init__(**kwargs) + + def on_current_uid(self, *args): + ''' Override the on_current_uid to to bind panel_change event + ''' + super(ProfileContentPanel, self).on_current_uid(args) + self.dispatch('on_current_panel') + + def on_current_panel(self, *args): + ''' Event handler for panel_change + ''' + pass + + +class ProfileMenuSidebar(MenuSidebar): + pass + + +class ProfileSettingsInterface(InterfaceWithSidebar): + ''' InterfaceWithSidebar with a custom style and custom events + ''' + + button_bar = ObjectProperty(None) + ''' Reference to the widget's GridLayout with buttons + :class:`~kivy.properties.ObjectProperty` and defaults to None. + ''' + + new_button = ObjectProperty(None) + ''' Reference to the widget's New button. + :class:`~kivy.properties.ObjectProperty` and defaults to None. + ''' + + select_prof_button = ObjectProperty(None) + ''' Reference to the widget's Use this Profile button. + :class:`~kivy.properties.ObjectProperty` and defaults to None. + ''' + + __events__ = ('on_delete', 'on_new', 'on_use_this_profile') + + def __init__(self, **kwargs): + super(ProfileSettingsInterface, self).__init__(**kwargs) + self.button_bar.btn_delete_prof.bind( + on_press=lambda j: self.dispatch('on_delete')) + self.button_bar.btn_select_prof.bind( + on_press=lambda j: self.dispatch('on_use_this_profile')) + self.menu.new_button.bind(on_press=lambda j: self.dispatch('on_new')) + self.content.bind(on_current_panel=self.on_current_panel) + + def on_delete(self, *args): + '''Event handler for button "Delete" press + ''' + pass + + def on_new(self, *args): + '''Event handler for button "New" press + ''' + pass + + def on_use_this_profile(self, *args): + '''Event handler for button "Use this Profile" press + ''' + pass + + def on_current_panel(self, *args): + ''' Event handler to panel change + The default profile cannot be deleted, so we watch this event to + prevent the user to delete desktop.ini + ''' + _file = self.content.current_panel.config.filename + filename = os.path.basename(_file) + self.button_bar.btn_delete_prof.disabled = filename == 'desktop.ini' + + +class ProfileSettings(Settings): + '''Subclass of :class:`kivy.uix.settings.Settings` responsible for + showing build profile settings of Kivy Designer. + ''' + + config_parsers = DictProperty({}) + '''List of config parsers + :class:`~kivy.properties.DictProperty` and defaults to {}. + ''' + + selected_config = ObjectProperty(None) + '''ConfigParser of the selected config + :class `~kivy.properties.ObjectProperty` and defaults to None. + ''' + + __events__ = ('on_use_this_profile', 'on_changed') + + def __init__(self, **kwargs): + super(ProfileSettings, self).__init__(**kwargs) + # list of ConfigParsers. Each file has one to handle the settings + self.PROFILES_PATH = '' + self.DEFAULT_PROFILES = '' + self.interface.bind(on_new=self.on_new) + self.interface.bind(on_delete=self.on_delete) + self.interface.bind( + on_use_this_profile=lambda j: self.dispatch('on_use_this_profile')) + self.interface.content.bind(on_current_panel=self.on_current_config) + self.settings_changed = False # changes in name, new or delete + + def load_profiles(self): + '''This function loads project settings + ''' + self.settings_changed = False + self.PROFILES_PATH = os.path.join(get_config_dir(), + constants.DIR_PROFILES) + + self.DEFAULT_PROFILES = os.path.join(get_kd_data_dir(), + constants.DIR_PROFILES) + + if not os.path.exists(self.PROFILES_PATH): + shutil.copytree(self.DEFAULT_PROFILES, self.PROFILES_PATH) + + self.update_panel() + + def update_panel(self): + '''Update the MenuSidebar + ''' + self.config_parsers = {} + self.interface.menu.buttons_layout.clear_widgets() + for _file in os.listdir(self.PROFILES_PATH): + _file_path = os.path.join(self.PROFILES_PATH, _file) + config_parser = ConfigParser() + config_parser.read(_file_path) + prof_name = config_parser.getdefault('profile', 'name', 'PROFILE') + if not prof_name.strip(): + prof_name = 'PROFILE' + self.config_parsers[ + str(prof_name) + '_' + _file_path] = config_parser + + for _file in sorted(self.config_parsers): + prof_name = self.config_parsers[_file].getdefault('profile', + 'name', + 'PROFILE') + if not prof_name.strip(): + prof_name = 'PROFILE' + self.add_json_panel(prof_name, + self.config_parsers[_file], + os.path.join( + get_kd_data_dir(), + 'settings', + 'build_profile.json') + ) + + # force to show the first profile + first_panel = self.interface.menu.buttons_layout.children[-1].uid + self.interface.content.current_uid = first_panel + + def on_config_change(self, instance, section, key, value, *args): + '''This function is default handler of on_config_change event. + ''' + super(ProfileSettings, self).on_config_change( + instance, section, key, value + ) + if key == 'name': + self.update_panel() + self.settings_changed = True + + def on_new(self, *args): + '''Handler for "New Profile" button + ''' + new_name = 'new_profile' + i = 1 + while os.path.exists(os.path.join( + self.PROFILES_PATH, new_name + str(i) + '.ini')): + i += 1 + new_name += str(i) + new_prof_path = os.path.join( + self.PROFILES_PATH, new_name + '.ini') + + shutil.copy2(os.path.join(self.DEFAULT_PROFILES, 'desktop.ini'), + new_prof_path) + config_parser = ConfigParser() + config_parser.read(new_prof_path) + config_parser.set('profile', 'name', new_name.upper()) + config_parser.write() + + self.update_panel() + self.settings_changed = True + + def on_delete(self, *args): + '''Handler to "Delete profile" button + ''' + confirm_dlg = ConfirmationDialog( + message="Do you want to delete this profile?") + self._popup = Popup(title='Delete Profile', + content=confirm_dlg, + size_hint=(None, None), + size=('200pt', '150pt'), + auto_dismiss=False) + confirm_dlg.bind(on_ok=self._perform_delete_prof, + on_cancel=self._popup.dismiss) + self._popup.open() + + def _perform_delete_prof(self, *args): + '''Delete the selected profile + ''' + self.selected_config = self.interface.content.current_panel.config + os.remove(self.selected_config.filename) + self.update_panel() + self._popup.dismiss() + self.settings_changed = True + + def on_use_this_profile(self, *args): + '''Event handler for button "Use this Profile" press + ''' + self.selected_config = self.interface.content.current_panel.config + self.settings_changed = True + + def on_current_config(self, *args): + ''' Event handler to panel change + The default profile cannot be deleted, so we watch this event to + prevent the user to delete desktop.ini + ''' + self.selected_config = self.interface.content.current_panel.config + + def on_changed(self, *args): + '''Event handler to Settings changes. + Will be called when the user delete, + create or change a name of a profile. + ''' + pass + + def on_close(self, *args): + '''Event handler when the settings is closed + ''' + if self.settings_changed: + self.dispatch('on_changed') + self.settings_changed = False diff --git a/designer/core/project_manager.py b/designer/core/project_manager.py new file mode 100644 index 0000000..0c89e49 --- /dev/null +++ b/designer/core/project_manager.py @@ -0,0 +1,539 @@ +import ast +import imp +import inspect +import os +import re +import sys + +from utils.utils import ( + get_app_widget, + get_designer, + show_error_console, + show_message, +) +from kivy.event import EventDispatcher +from kivy.factory import Factory +from kivy.lang import Builder +from kivy.properties import ( + BooleanProperty, + Clock, + DictProperty, + ListProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.widget import Widget +from six import exec_ +from watchdog.events import RegexMatchingEventHandler +from watchdog.observers import Observer +from io import open + + +IGNORED_PATHS = ('/.designer', '/.buildozer', '/.git', '/bin',) +IGNORED_EXTS = ('.pyc',) +KV_EVENT_RE = r'(\s+on_\w+\s*:.+)|(^[\s\w\d]+:[\.]+[\s\w]+\(.*)' +KV_ROOT_WIDGET = r'^([\w\d_]+)\:' +KV_APP_WIDGET = r'^<([\w\d_@]+)>\:' + + +class ProjectEventHandler(RegexMatchingEventHandler): + def __init__(self, project_watcher): + super(ProjectEventHandler, self).__init__() + self.project_watcher = project_watcher + + def on_any_event(self, event): + if self.project_watcher: + self.project_watcher.on_any_event(event) + + +class ProjectWatcher(EventDispatcher): + '''ProjectWatcher is responsible for watching any changes in + project directory. It will call self._callback whenever there + are any changes. It can currently handle only one directory at + a time. + ''' + + _active = BooleanProperty(True) + '''Indicates if the watchdog can dispatch events + :data:`active` is a :class:`~kivy.properties.BooleanProperty` + ''' + + _path = StringProperty('') + '''Project folder + :data:`path` is a :class:`~kivy.properties.StringProperty` + ''' + + __events__ = ('on_project_modified',) + + def __init__(self, **kw): + super(ProjectWatcher, self).__init__(**kw) + + self._observer = None + self._handler = None + self._watcher = None + + def start_watching(self, path): + '''To start watching project_dir. + ''' + self._path = path + self._observer = Observer() + self._handler = ProjectEventHandler(project_watcher=self) + self._watcher = self._observer.schedule( + self._handler, path, + recursive=True) + self._observer.start() + + def on_project_modified(self, *args): + pass + + def stop_watching(self): + '''To stop watching currently watched directory. This will also call + join() on the thread created by Observer. + ''' + + if self._observer: + self._observer.unschedule_all() + self._observer.stop() + self._observer.join() + + self._observer = None + + def pause_watching(self): + '''Pauses the watcher + ''' + self._active = False + + def resume_watching(self, delay=1): + '''Resume the watcher + :param delay: seconds to start the watching + ''' + Clock.schedule_once(self._resume_watching, delay) + + def _resume_watching(self, *args): + if self._observer: + self._observer.event_queue.queue.clear() + self._active = True + + def on_any_event(self, event): + if self._active: + # filter events + path = event.src_path.replace(self._path, '') + if not path: + return + if '__pycache__' in path: + return + for ign in IGNORED_PATHS: + if path.startswith(ign): + return + for ext in IGNORED_EXTS: + if event.src_path.endswith(ext): + return + + self.dispatch('on_project_modified', event) + + +class CallWrapper(ast.NodeTransformer): + def visit_Expr(self, node): + if node.col_offset == 0: + return None + return node + + +class AppWidget(EventDispatcher): + name = StringProperty('') + '''Root Widget name. + :data:`name` is a :class:`~kivy.properties.StringProperty` and + defaults to '' + ''' + + kv_path = StringProperty('') + '''RootWidget associated kv file path. + :data:`kv_path` is a :class:`~kivy.properties.StringProperty` and + default to '' + ''' + + py_path = StringProperty('') + '''RootWidget associated py file path. + :data:`py_path` is a :class:`~kivy.properties.StringProperty` and + default to '' + ''' + + is_root = BooleanProperty(False) + '''Indicates if this widget is a root/default kivy widget or not + :data:`is_root` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False + ''' + + instance = ObjectProperty(None) + '''If the widget is root, it has a instance returned by Builder.load_string + If not is root, instance is None + data:`instance` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None + ''' + + is_dynamic = BooleanProperty(False) + '''Indicates if this widget is a dynamic widget or not + :data:`is_dynamic` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False + ''' + + module_name = StringProperty('') + '''ModuleName used in the class import + :data:`module_name` is a :class:`~kivy.properties.StringProperty` and + default to '' + ''' + + +class Project(EventDispatcher): + path = StringProperty('') + '''Project path. + :data:`path` is a :class:`~kivy.properties.StringProperty` + ''' + + saved = BooleanProperty(True) + '''Indicates if the project was saved. The project is seted as saved when + oppened + :data:`saved` is a :class:`~kivy.properties.BooleanProperty` + ''' + + new_project = BooleanProperty(False) + '''Indicates if it's a new project. + :data:`new_project` is a :class:`~kivy.properties.BooleanProperty` + ''' + + file_list = ListProperty([]) + '''List of files in the project folder. + :data:`file_list` is a :class:`~kivy.properties.ListProperty` + ''' + + kv_list = ListProperty([]) + '''List of kv files in the project folder. + :data:`kv_list` is a :class:`~kivy.properties.ListProperty` + ''' + + py_list = ListProperty([]) + '''List of py files in the project folder. + :data:`kv_list` is a :class:`~kivy.properties.ListProperty` + ''' + + app_widgets = DictProperty({}) + '''List of :class:`~designer.core.project_manager.AppWidget`. + :data:`app_widgets` is a :class:`~kivy.properties.DictProperty` + ''' + + def __init__(self, **kw): + super(Project, self).__init__(**kw) + self._errors = [] # exception messages + + def open(self): + '''Opens then project + ''' + self.saved = True + self.get_files() + self.parse() + + def get_files(self, path=None, force_reload=True): + '''Gets a list of files in the project folder. If force_reload is True, + will gets the list from hard drive. Otherwiser will return the last + file_list + ''' + if path is None: + path = self.path + + if not force_reload: + return self.file_list + + file_list = [] + for ignored in IGNORED_PATHS: + if ignored in path: + return [] + + for _file in os.listdir(path): + file_path = os.path.join(path, _file) + if os.path.isdir(file_path): + file_list += self.get_files(file_path) + else: + if file_path[file_path.rfind('.'):] not in IGNORED_EXTS: + if os.path.dirname(file_path) == self.path: + file_list.insert(0, file_path) + else: + file_list.append(file_path) + + self.file_list = file_list + return file_list + + def parse(self, reload_files=False): + '''Parse project files to analyse python and kv files + ''' + + if reload_files: + self.get_files() + + # reset caches + self.kv_list = [] + self.py_list = [] + self.app_widgets = {} + self._errors = [] + + # find kv and python files + for _file in self.file_list: + # in the first step, loads only kv files + ext = _file[_file.rfind('.'):] + if ext == '.kv': + self.kv_list.append(_file) + elif ext == '.py' or ext == '.py2' or ext == '.py3': + self.py_list.append(_file) + + # find and load classes + for py in self.py_list: + self.parse_py(py) + # find and load root widgets + for kv in self.kv_list: + src = open(kv, 'r', encoding='utf-8').read() + # removes events + src = re.sub(KV_EVENT_RE, '', src, flags=re.MULTILINE) + self.parse_kv(src, kv) + + self.show_errors() + + def show_errors(self, *args): + '''Pop errors got in the last operations and display it on + Error Console + ''' + errors = list(self._errors) + show_error_console('') + if len(errors): + show_message( + 'Errors found while parsing the project. Check Error Console', + 5, 'error' + ) + for er in range(0, len(errors)): + show_error_console('\n\nError: %d\n' % (er + 1), append=True) + show_error_console(errors[er], append=True) + self._errors.remove(errors[er]) + + def _clean_old_kv(self, path): + ''' + Removes widgets and rules already processed to this file + :param path: file path - the same that in app_widgets + ''' + for key in dict(self.app_widgets): + wd = self.app_widgets[key] + if path != wd.kv_path: + continue + wdg = get_app_widget(wd) + if wdg is None: + p = get_designer().ui_creator.playground + if p.root_name == wd.name: + wdg = p._last_root + if not wdg: + continue + if wd.is_dynamic: + del self.app_widgets[key] + + rules = Builder.match(wdg) + + # Cleaning widget rules + for _rule in rules: + for _tuple in Builder.rules[:]: + if _tuple[1] == _rule: + Builder.rules.remove(_tuple) + if wd.is_dynamic: + Factory.unregister(wd.name.split('@')[0]) + + # Cleaning class rules + for rule in Builder.rules[:]: + if rule[1].name == '<' + wd.name + '>': + Builder.rules.remove(rule) + break + + def parse_kv(self, src, path): + ''' + Parses a KV file with Builder.load_string. Identify root widgets and + add them to self.root_widgets dict + :param path: path of the kv file + :param src: kv string + :return boolean indicating if succeed in parsing the file + ''' + self._clean_old_kv(path) + root = None + try: + root = Builder.load_string(src, filename=os.path.basename(path)) + except Exception as e: + self._errors.append(str(e)) + d = get_designer() + d.ui_creator.kv_code_input.have_error = True + return False + # first, if a root widget was found, maps it + if root: + root_widgets = re.findall(KV_ROOT_WIDGET, src, re.MULTILINE) + root_name = type(root).__name__ + for r in root_widgets: + if r != root_name: + continue + if r in self.app_widgets: + wdg = self.app_widgets[r] + else: + wdg = AppWidget() + wdg.name = r + if path: + wdg.kv_path = path + wdg.is_root = True + wdg.instance = root + if wdg not in self.app_widgets: + self.app_widgets[r] = wdg + + # now, get all custom widgets + app_widgets = re.findall(KV_APP_WIDGET, src, re.MULTILINE) + for a in app_widgets: + wdg = self.app_widgets[a] if a in self.app_widgets else AppWidget() + wdg.name = a + if path: + wdg.kv_path = path + wdg.is_dynamic = '@' in a + # dynamic widgets are not preloaded by py files + if wdg not in self.app_widgets: + self.app_widgets[a] = wdg + + return True + + def parse_py(self, path): + '''Parses a Python file and load it. + ''' + + rel_path = path.replace(self.path, '') + + # creates a name to the import based in the file name and its path + module_name = 'KDImport' + ''.join([x.replace('.py', '').capitalize() + for x in rel_path.split('/')]) + + # remove method calls to do a safe import + src = open(path, 'r', encoding='utf-8').read() + try: + p = ast.parse(src, os.path.basename(path)) + except SyntaxError as e: + self._errors.append(str(e)) + return False + p = CallWrapper().visit(p) + p = ast.fix_missing_locations(p) + + # if module is already loaded, removes it + if module_name in sys.modules: + del sys.modules[module_name] + + # imports the new python + module = imp.new_module(module_name) + + try: + exec_(compile(p, os.path.basename(path), 'exec'), module.__dict__) + except Exception as e: + self._errors.append(str(e)) + return False + sys.modules[module_name] = module + + # find classes and possible widgets + classes = inspect.getmembers( + sys.modules[module_name], + lambda member: + inspect.isclass(member) and member.__module__ == module_name + ) + + if classes: + self.load_widgets(path, classes, module_name) + + return True + + def load_widgets(self, path, classes, module_name): + ''' + Analyze classes and loads Widgets from an array + :param classes: array with classes to be analyzed + :return: self.root_widgets + ''' + for klass_name, klass in classes: + if issubclass(klass, Widget): + # updates root_widget dict + if klass_name in self.app_widgets: + # if already exists, update only the path + self.app_widgets[klass_name].py_path = path + else: + # otherwise create a new widget representation + r = AppWidget() + r.py_path = path + r.name = klass_name + r.module_name = module_name + self.app_widgets[klass_name] = r + + return self.app_widgets + + def save(self, code_inputs=None, *args): + '''Get all KD Code input and save the content + :param code_inputs list of files to save. If None, get all open files + ''' + if not code_inputs: + d = get_designer() + code_inputs = d.code_inputs + + try: + for code in code_inputs: + fname = code.path + if not fname: + continue + content = code.text + open(fname, 'w', encoding='utf-8').write(content) + code.saved = True + except IOError as e: + return False + + self.saved = True + self.new_project = False + return True + + +class ProjectManager(EventDispatcher): + current_project = ObjectProperty(None) + '''An instance of the current project. + :data:`current_project` is a :class:`~kivy.properties.ObjectProperty` + ''' + + projects = DictProperty(None) + '''A map of opened projects + :data:`projects` is a :class:`~kivy.properties.DictProperty` + ''' + + project_manager = BooleanProperty(True) + '''Auto save the project + :data:`project_manager` is a :class:`~kivy.properties.BooleanProperty` + ''' + + def __init__(self, **kwargs): + super(ProjectManager, self).__init__(**kwargs) + self.current_project = Project() + + def open_project(self, path): + '''Opens a Python project by path, and returns the Project instance + ''' + if self.projects is None: + return None + + if os.path.isfile(path): + path = os.path.dirname(path) + + if path in self.projects: + self.current_project = self.projects[path] + self.current_project.open() + return self.current_project + + p = Project(path=path) + p.open() + self.projects[path] = p + self.current_project = p + return self.projects[path] + + def close_current_project(self): + '''Closes a project, setting saved as True and new_project as False, + and removing it from current_project + :param project: instance of pro + ''' + self.current_project.saved = True + self.current_project.new_project = False + self.current_project = Project() diff --git a/designer/core/project_settings.py b/designer/core/project_settings.py new file mode 100644 index 0000000..b726d64 --- /dev/null +++ b/designer/core/project_settings.py @@ -0,0 +1,66 @@ +import os + +from utils.utils import get_kd_data_dir, ignore_proj_watcher +from kivy.config import ConfigParser +from kivy.properties import ObjectProperty +from kivy.uix.settings import Settings + + +PROJ_DESIGNER = '.designer' +PROJ_CONFIG = os.path.join(PROJ_DESIGNER, 'config.ini') + + +class ProjectSettings(Settings): + '''Subclass of :class:`kivy.uix.settings.Settings` responsible for + showing settings of project. + ''' + + project = ObjectProperty(None) + '''Reference to :class:`desginer.project_manager.Project` + ''' + + config_parser = ObjectProperty(None) + '''Config Parser for this class. Instance + of :class:`kivy.config.ConfigParser` + ''' + + def load_proj_settings(self): + '''This function loads project settings + ''' + self.config_parser = ConfigParser() + file_path = os.path.join(self.project.path, PROJ_CONFIG) + if not os.path.exists(file_path): + if not os.path.exists(os.path.dirname(file_path)): + os.makedirs(os.path.dirname(file_path)) + + CONFIG_TEMPLATE = '''[proj_name] +name = Project + +[arguments] +arg = + +[env variables] +env = +''' + f = open(file_path, 'w') + f.write(CONFIG_TEMPLATE) + f.close() + + self.config_parser.read(file_path) + + settings_dir = os.path.join(get_kd_data_dir(), 'settings') + self.add_json_panel('Shell Environment', + self.config_parser, + os.path.join(settings_dir, + 'proj_settings_shell_env.json')) + self.add_json_panel('Project Properties', + self.config_parser, + os.path.join(settings_dir, + 'proj_settings_proj_prop.json')) + + @ignore_proj_watcher + def on_config_change(self, *args): + '''This function is default handler of on_config_change event. + ''' + self.config_parser.write() + super(ProjectSettings, self).on_config_change(*args) diff --git a/designer/core/recent_manager.py b/designer/core/recent_manager.py new file mode 100644 index 0000000..ca9aa2d --- /dev/null +++ b/designer/core/recent_manager.py @@ -0,0 +1,82 @@ +import os + +from utils.utils import get_config_dir, get_fs_encoding + + +RECENT_FILES_NAME = 'recent_files' + + +class RecentManager(object): + '''RecentManager is responsible for retrieving/storing the list of recently + opened/saved projects. + ''' + + def __init__(self): + super(RecentManager, self).__init__() + self.list_projects = [] + self.max_recent_files = 10 + self.load_files() + + def add_path(self, path): + '''To add file to RecentManager. + :param path: path of project + ''' + if isinstance(path, bytes): + path = path.decode(get_fs_encoding()).encode(get_fs_encoding()) + + _file_index = 0 + try: + _file_index = self.list_projects.index(path) + except: + _file_index = -1 + + if _file_index != -1: + # If _file is already present in list_files, then move it to 0 index + self.list_projects.remove(path) + + self.list_projects.insert(0, path) + + # Recent files should not be greater than max_recent_files + while len(self.list_projects) > self.max_recent_files: + self.list_projects.pop() + + self.store_files() + + def store_files(self): + '''To store the list of files on disk. + ''' + + _string = '' + for _file in self.list_projects: + _string += _file + '\n' + + recent_file_path = os.path.join(get_config_dir(), + RECENT_FILES_NAME) + f = open(recent_file_path, 'w') + f.write(_string) + f.close() + + def load_files(self): + '''To load the list of files from disk + ''' + + recent_file_path = os.path.join(get_config_dir(), + RECENT_FILES_NAME) + + if not os.path.exists(recent_file_path): + return + + f = open(recent_file_path, 'r') + path = f.readline() + + while path != '': + file_path = path.strip() + if isinstance(file_path, bytes): + file_path = file_path.decode(get_fs_encoding()).encode( + get_fs_encoding()) + if os.path.exists(file_path): + self.list_projects.append(file_path) + + path = f.readline() + + f.close() diff --git a/designer/core/settings.py b/designer/core/settings.py new file mode 100644 index 0000000..348483e --- /dev/null +++ b/designer/core/settings.py @@ -0,0 +1,115 @@ +import os +import os.path +import shutil +import sys +from distutils.spawn import find_executable + +from uix.settings import SettingList, SettingShortcut +from utils import constants +from utils.utils import get_config_dir, get_kd_data_dir, get_kd_dir +from kivy.config import ConfigParser +from kivy.properties import ObjectProperty +from kivy.uix.settings import Settings +from pygments import styles + + +# monkey backport! (https://github.com/kivy/kivy/pull/2288) +if not hasattr(ConfigParser, 'upgrade'): + try: + from ConfigParser import ConfigParser as PythonConfigParser + except ImportError: + from configparser import RawConfigParser as PythonConfigParser + + def upgrade(self, default_config_file): + '''Upgrade the configuration based on a new default config file. + ''' + pcp = PythonConfigParser() + pcp.read(default_config_file) + for section in pcp.sections(): + self.setdefaults(section, dict(pcp.items(section))) + self.write() + + ConfigParser.upgrade = upgrade + + +class DesignerSettings(Settings): + '''Subclass of :class:`kivy.uix.settings.Settings` responsible for + showing settings of Kivy Designer. + ''' + + config_parser = ObjectProperty(None) + '''Config Parser for this class. Instance + of :class:`kivy.config.ConfigParser` + ''' + + def __init__(self, **kwargs): + super(DesignerSettings, self).__init__(*kwargs) + self.register_type('list', SettingList) + self.register_type('shortcut', SettingShortcut) + + def load_settings(self): + '''This function loads project settings + ''' + self.config_parser = ConfigParser(name='DesignerSettings') + DESIGNER_CONFIG = os.path.join(get_config_dir(), + constants.DESIGNER_CONFIG_FILE_NAME) + + DEFAULT_CONFIG = os.path.join(get_kd_dir(), + constants.DESIGNER_CONFIG_FILE_NAME) + if not os.path.exists(DESIGNER_CONFIG): + shutil.copyfile(DEFAULT_CONFIG, + DESIGNER_CONFIG) + + self.config_parser.read(DESIGNER_CONFIG) + self.config_parser.upgrade(DEFAULT_CONFIG) + + # creates a panel before insert it to update code input theme list + return None + panel = self.create_json_panel( + 'Kivy Designer Settings', self.config_parser, + os.path.join(get_kd_data_dir(), 'settings', 'designer_settings.json') + ) + uid = panel.uid + if self.interface is not None: + self.interface.add_panel(panel, 'Kivy Designer Settings', uid) + + # loads available themes + for child in panel.children: + if child.id == 'code_input_theme_options': + child.items = styles.get_all_styles() + + # tries to find python and buildozer path if it's not defined + path = self.config_parser.getdefault( + 'global', 'python_shell_path', '') + + if path.strip() == '': + self.config_parser.set('global', 'python_shell_path', + sys.executable) + self.config_parser.write() + + buildozer_path = self.config_parser.getdefault('buildozer', + 'buildozer_path', '') + + if buildozer_path.strip() == '': + buildozer_path = find_executable('buildozer') + if buildozer_path: + self.config_parser.set('buildozer', + 'buildozer_path', + buildozer_path) + self.config_parser.write() + + self.add_json_panel('Buildozer', self.config_parser, + os.path.join(get_kd_data_dir(), 'settings', + 'buildozer_settings.json')) + self.add_json_panel('Hanga', self.config_parser, + os.path.join(get_kd_data_dir(), 'settings', + 'hanga_settings.json')) + self.add_json_panel('Keyboard Shortcuts', self.config_parser, + os.path.join(get_kd_data_dir(), 'settings', + 'shortcuts.json')) + + def on_config_change(self, *args): + '''This function is default handler of on_config_change event. + ''' + self.config_parser.write() + super(DesignerSettings, self).on_config_change(*args) diff --git a/designer/core/shortcuts.py b/designer/core/shortcuts.py new file mode 100644 index 0000000..4c1eff8 --- /dev/null +++ b/designer/core/shortcuts.py @@ -0,0 +1,248 @@ +from utils.utils import get_designer +from kivy.core.window import Keyboard, Window + + +class Shortcuts(object): + + def __init__(self, **kw): + Window.bind(on_key_down=self.parse_key_down) + # map is the link between a shortcut and a callback + # the key is a formatted keyboard shortcuts string + # and the value is a method declared in this class + self.map = {} + + def map_shortcuts(self, config_parser, *args): + '''Read shortcuts from config_parser + :param config_parser: config parser with all shorcut settings + ''' + g = config_parser.getdefault + + # get all defined shortcuts + m = { + g('shortcuts', 'new_file', ''): + [self.do_new_file, 'new_file'], + g('shortcuts', 'new_project', ''): + [self.do_new_project, 'new_project'], + g('shortcuts', 'open_project', ''): + [self.do_open_project, 'open_project'], + g('shortcuts', 'save', ''): + [self.do_save, 'save'], + g('shortcuts', 'save_as', ''): + [self.do_save_as, 'save_as'], + g('shortcuts', 'close_project', ''): + [self.do_close_project, 'close_project'], + g('shortcuts', 'recent', ''): + [self.do_recent, 'recent'], + g('shortcuts', 'settings', ''): + [self.do_settings, 'settings'], + g('shortcuts', 'exit', ''): + [self.do_exit, 'exit'], + g('shortcuts', 'fullscreen', ''): + [self.do_fullscreen, 'fullscreen'], + g('shortcuts', 'run', ''): + [self.do_run, 'run'], + g('shortcuts', 'stop', ''): + [self.do_stop, 'stop'], + g('shortcuts', 'clean', ''): + [self.do_clean, 'clean'], + g('shortcuts', 'build', ''): + [self.do_build, 'build'], + g('shortcuts', 'rebuild', ''): + [self.do_rebuild, 'rebuild'], + g('shortcuts', 'buildozer_init', ''): + [self.do_buildozer_init, 'buildozer_init'], + g('shortcuts', 'export_png', ''): + [self.do_export_png, 'export_png'], + g('shortcuts', 'check_pep8', ''): + [self.do_check_pep8, 'check_pep8'], + g('shortcuts', 'create_setup_py', ''): + [self.do_create_setup_py, 'create_setup_py'], + g('shortcuts', 'create_gitignore', ''): + [self.do_create_gitignore, 'create_gitignore'], + g('shortcuts', 'help', ''): + [self.do_help, 'help'], + g('shortcuts', 'kivy_docs', ''): + [self.do_kivy_docs, 'kivy_docs'], + g('shortcuts', 'kd_docs', ''): + [self.do_kd_docs, 'kd_docs'], + g('shortcuts', 'kd_repo', ''): + [self.do_kd_repo, 'kd_repo'], + g('shortcuts', 'about', ''): + [self.do_about, 'about'], + } + + self.map = m + + def parse_key_down(self, keyboard, key, codepoint, text, modifier, *args): + '''Parse keys and generate the formatted keyboard shortcut + ''' + key_str = Keyboard.keycode_to_string(Window._system_keyboard, key) + modifier.sort() + value = str(modifier) + ' + ' + key_str + if value in self.map: + self.map.get(value)[0]() + return True + + def do_new_file(self, *args): + d = get_designer() + btn = d.ids.actn_btn_new_file + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_new_file_pressed() + + def do_new_project(self, *args): + d = get_designer() + btn = d.ids.actn_btn_new_project + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_new_project_pressed() + + def do_open_project(self, *args): + d = get_designer() + btn = d.ids.actn_btn_open_project + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_open_pressed() + + def do_save(self, *args): + d = get_designer() + btn = d.ids.actn_btn_save + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_save_pressed() + + def do_save_as(self, *args): + d = get_designer() + btn = d.ids.actn_btn_save_as + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_save_as_pressed() + + def do_close_project(self, *args): + d = get_designer() + btn = d.ids.actn_btn_close_proj + menu = d.ids.actn_menu_file + if not btn.disabled and not menu.disabled: + d.action_btn_close_proj_pressed() + + def do_recent(self, *args): + d = get_designer() + d.action_btn_recent_files_pressed() + + def do_settings(self, *args): + d = get_designer() + d.action_btn_settings_pressed() + + def do_exit(self, *args): + d = get_designer() + d.on_request_close() + + def do_fullscreen(self, *args): + d = get_designer() + check = d.ids.actn_chk_fullscreen + check.checkbox.active = not check.checkbox.active + + def do_run(self, *args): + d = get_designer() + btn = d.ids.actn_btn_run_proj + menu = d.ids.actn_menu_run + if not btn.disabled and not menu.disabled: + d.action_btn_run_project_pressed() + + def do_stop(self, *args): + d = get_designer() + btn = d.ids.actn_btn_stop_proj + menu = d.ids.actn_menu_run + if not btn.disabled and not menu.disabled: + d.action_btn_stop_project_pressed() + + def do_clean(self, *args): + d = get_designer() + btn = d.ids.actn_btn_clean_proj + menu = d.ids.actn_menu_run + if not btn.disabled and not menu.disabled: + d.action_btn_clean_project_pressed() + + def do_build(self, *args): + d = get_designer() + btn = d.ids.actn_btn_build_proj + menu = d.ids.actn_menu_run + if not btn.disabled and not menu.disabled: + d.action_btn_build_project_pressed() + + def do_rebuild(self, *args): + d = get_designer() + btn = d.ids.actn_btn_rebuild_proj + menu = d.ids.actn_menu_run + if not btn.disabled and not menu.disabled: + d.action_btn_rebuild_project_pressed() + + def do_buildozer_init(self, *args): + d = get_designer() + btn = d.ids.actn_btn_buildozer_init + menu = d.ids.actn_menu_tools + if not btn.disabled and not menu.disabled: + d.designer_tools.buildozer_init() + + def do_export_png(self, *args): + d = get_designer() + btn = d.ids.actn_btn_export_png + menu = d.ids.actn_menu_tools + if not btn.disabled and not menu.disabled: + d.designer_tools.export_png() + + def do_check_pep8(self, *args): + d = get_designer() + btn = d.ids.actn_btn_check_pep8 + menu = d.ids.actn_menu_tools + if not btn.disabled and not menu.disabled: + d.designer_tools.check_pep8() + + def do_create_setup_py(self, *args): + d = get_designer() + btn = d.ids.actn_btn_create_setup_py + menu = d.ids.actn_menu_tools + if not btn.disabled and not menu.disabled: + d.designer_tools.create_setup_py() + + def do_create_gitignore(self, *args): + d = get_designer() + btn = d.ids.actn_btn_create_gitignore + menu = d.ids.actn_menu_tools + if not btn.disabled and not menu.disabled: + d.designer_tools.create_gitignore() + + def do_help(self, *args): + d = get_designer() + btn = d.ids.actn_btn_help + menu = d.ids.actn_menu_help + if not btn.disabled and not menu.disabled: + d.show_help() + + def do_kivy_docs(self, *args): + d = get_designer() + btn = d.ids.actn_btn_wiki + menu = d.ids.actn_menu_help + if not btn.disabled and not menu.disabled: + d.open_docs() + + def do_kd_docs(self, *args): + d = get_designer() + btn = d.ids.actn_btn_doc + menu = d.ids.actn_menu_help + if not btn.disabled and not menu.disabled: + d.open_kd_docs() + + def do_kd_repo(self, *args): + d = get_designer() + btn = d.ids.actn_btn_page + menu = d.ids.actn_menu_help + if not btn.disabled and not menu.disabled: + d.open_repo() + + def do_about(self, *args): + d = get_designer() + btn = d.ids.actn_btn_about + menu = d.ids.actn_menu_help + if not btn.disabled and not menu.disabled: + d.action_btn_about_pressed() diff --git a/designer/core/undo_manager.py b/designer/core/undo_manager.py new file mode 100644 index 0000000..b870533 --- /dev/null +++ b/designer/core/undo_manager.py @@ -0,0 +1,171 @@ +from utils.utils import get_current_project +from kivy.uix.checkbox import CheckBox +from kivy.uix.textinput import TextInput + + +class OperationBase(object): + '''UndoOperationBase class, Abstract class for all Undo Operations + ''' + + def __init__(self, operation_type): + super(OperationBase, self).__init__() + self.operation_type = operation_type + + def do_undo(self): + pass + + def do_redo(self): + pass + + +class WidgetOperation(OperationBase): + '''WidgetOperation class for widget operations of add and remove + ''' + + def __init__(self, widget_op_type, widget, parent, playground, kv_str): + super(WidgetOperation, self).__init__('widget') + self.widget_op_type = widget_op_type + self.parent = parent + self.widget = widget + self.playground = playground + self.kv_str = kv_str + + def do_undo(self): + '''Override of :class:`OperationBase`.do_undo. + This will undo a WidgetOperation. + ''' + if self.widget_op_type == 'add': + self.playground.remove_widget_from_parent(self.widget, True) + + else: + self.widget.parent = None + self.playground.add_widget_to_parent(self.widget, self.parent, + from_undo=True, + kv_str=self.kv_str) + + def do_redo(self): + '''Override of :class:`OperationBase`.do_redo. + This will redo a WidgetOperation. + ''' + + if self.widget_op_type == 'remove': + self.playground.remove_widget_from_parent(self.widget, True) + + else: + self.widget.parent = None + self.playground.add_widget_to_parent(self.widget, self.parent, + from_undo=True, + kv_str=self.kv_str) + + +class WidgetDragOperation(OperationBase): + + def __init__(self, widget, cur_parent, prev_parent, prev_index, + playground, extra_args): + self.widget = widget + self.cur_parent = cur_parent + self.prev_parent = prev_parent + self.prev_index = prev_index + self.playground = playground + self.cur_index = extra_args['index'] + self.extra_args = extra_args + + def do_undo(self): + self.cur_parent.remove_widget(self.widget) + self.playground.drag_wigdet(self.widget, self.prev_parent, + extra_args={'index': self.prev_index, + 'prev_index': self.cur_index, + 'x': self.extra_args['prev_x'], + 'y': self.extra_args['prev_y']}, + from_undo=True) + + def do_redo(self): + self.prev_parent.remove_widget(self.widget) + self.playground.drag_wigdet(self.widget, self.cur_parent, + extra_args={'index': self.cur_index, + 'prev_index': self.prev_index, + 'x': self.extra_args['x'], + 'y': self.extra_args['y']}, + from_undo=True) + + +class PropOperation(OperationBase): + '''PropOperation class for Property Operations of changing property value + ''' + + def __init__(self, prop, oldvalue, newvalue): + super(PropOperation, self).__init__('property') + self.prop = prop + self.oldvalue = oldvalue + self.newvalue = newvalue + + def do_undo(self): + '''Override of :class:`OperationBase`.do_undo. + This will undo a PropOperation. + ''' + + setattr(self.prop.propwidget, self.prop.propname, self.oldvalue) + self._update_widget(self.oldvalue) + + def _update_widget(self, value): + '''After do_undo or do_redo, this function will update the PropWidget's + value associated with that property. + ''' + self.prop.record_to_undo = False + if isinstance(self.prop, TextInput): + self.prop.text = value + elif isinstance(self.prop, CheckBox): + self.prop.active = value + + def do_redo(self): + '''Override of :class:`OperationBase`.do_redo. + This will redo a PropOperation. + ''' + + setattr(self.prop.propwidget, self.prop.propname, self.newvalue) + self._update_widget(self.newvalue) + + +class UndoManager(object): + '''UndoManager is reponsible for managing all the operations related + to Widgets. It is also responsible for redoing and undoing the last + available operation. + ''' + + def __init__(self, **kwargs): + super(UndoManager, self).__init__(**kwargs) + self._undo_stack_operation = [] + self._redo_stack_operation = [] + + def push_operation(self, op): + '''To push an operation into _undo_stack. + ''' + get_current_project().saved = False + self._undo_stack_operation.append(op) + + def do_undo(self): + '''To undo last operation + ''' + if self._undo_stack_operation == []: + return + + operation = self._undo_stack_operation.pop() + operation.do_undo() + self._redo_stack_operation.append(operation) + + def do_redo(self): + '''To redo last operation + ''' + + if self._redo_stack_operation == []: + return + + operation = self._redo_stack_operation.pop() + operation.do_redo() + self._undo_stack_operation.append(operation) + + def cleanup(self): + '''To cleanup operation stacks when another project is loaded + ''' + self._undo_stack_operation = [] + self._redo_stack_operation = [] diff --git a/designer/data/icons/error.png b/designer/data/icons/error.png new file mode 100644 index 0000000000000000000000000000000000000000..55e7c95e964e7c1adef371a38d6e29d597f557bd GIT binary patch literal 496 zcmeAS@N?(olHy`uVBq!ia0vp^F(Ayr1|%Q9zZAj1z_{Jh#WAE}&f8m#Ud)aHZV#=x z9`uR?+61a`tUH>`V$-dZcSqw9t4oKi@aY_$yf^z-?Y`fflwjsu&okL!yQCljc-~s~ zCdv9{?A6HIkzcHWLpJDhpGe=CxBA?lo~?7;*PEM2n!RAWp}e>~vA;vE&sL~P>W$!q z%ZcB6=85fg|MKOA?LzbByUf4#C;NYkd$#t>Ya{LIpEG~{JMkj)-kYtm@-_Fi%JTkR z7XG_O<6yMo`Zig{d9r)8%0w!{zP!3I@xjdt+jpOB{#E~1>EQPlA7@LP&hvVEG>zf@ zS+~sJzmpiRv+b}raFkJxub{A@n>j}2!6OE7c66bX2a|25zW!@8*>*~0%)7VGBtO`( z7aaIKukpaoi+|#D9^7IOH<#~|e4=o{eWvizw+EjkC(M;E{jG7{K6KW<$kH!64$t|y zVehIJlkZO3X=Px3II()t`R=5s1K(!3=X2i97TfEZv)=N{w2gbyE>s`*c4_hVmbXh? z-wWRMZog-|Z8}>mOOC!wtxAqQPpwOif1jPvHg#wA>ie5IH*WI1dU8{xg9{v(IK<25 W*;*cI-L@PUlMJ4&elF{r5}E*_*5(`l literal 0 HcmV?d00001 diff --git a/designer/data/icons/info.png b/designer/data/icons/info.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc823699c03f0597bf10dcf40b3bd2ba05d3487 GIT binary patch literal 1599 zcmV-F2Eh4=P))#%K9= zOZaWvQZL|dL+d{mM6LcqjWm0k419(RxE?h-gAEn>+eWkVGrfQVizj;zhJLQv5vSef zsI{hOwHr2aNg39uPQ*#}Z0P3?RxJ3=FxLWRF*` zzq_@Os<-Ix&)%f9{~agYS6n<}E!dL`?j$38G|AUeIkNcerAKVZjN0lkdU?ktjtzm;6 z!nl{Y^y%yXnW%p#GI%kxZX@DUy-Wta!VRRMb*ltIKQ0QXld(tSMlZH>it|J3HWirE zuyJ+`+(;Qi_fm}0=VEfi%^)#A+NgV>*?$xR9+lrwkRh}cw^MzeGe%#sQMWUY8{tNN z8-tLJE}CkUd6}!W>gOUVgp#w??F=pvWh>5JNPF8$Cob*oq74~HY%wqNM;mophf@|a zNL%%zrIi`nW-FxVDq+T-r)|}54O3B_zpyx6f9j|Hy;Sd7p#>(Lzwn(cIXd}e>)Soy;0<<)=gIq(QRBT<~?(8ZfEiiVInVUYm5TM(5Ps zE@V|x3|korkwbT@Z8>@7kq-;$JL~dVY8w-E?Y1qax?+WCvFbaKOxL6rI5xP_u;QOkrTq;N%n@1@u?DpEeMVh_dyJ9E~HRs=t52<#NeJ0 zm*N>2G(A(;!{=zlJCMPT)-NoSiH~1oYVR4GsVN#^b0)9iR_}S+aw_91J?q=xTzb~! zqDLk40j6=c@{4U5-VJW-vaQwXJ-5V$O*=Ma_%t|NqA8dWB66LUuWb=(L9ii4-UT~R&^&h3rF zp#mFqJAvPu*Ew$^oN{e&tjHUijOyLGM{wt7@Y$^O?i~znCxg7imUDwUX05+uoAsx& z*6SQZEX94)vsSyvqzs(!Ej|E^{L~sl>t>>lkuyh5a1$qaGbi-=oB*gyf{Y3*<9$C8+-1LevcTQUf|rze{SXk_GG(~ zkLfyY94eJhZe;FTwu$!X>_GZ|?gdwE*gU3!mxx$;^3_i2@Z9?BY0!}Y6Q>*9`&X>~ zqh(g9TQ7^X)HZr?@08veaYLM4r-;cZ`Q&KnCI%U`z`k}z=pFPD$VjgbH|VH+`rq`~ zu;-{#IAAqRooMy9J1b$2se6xq3|drqp}+%%z{Yw~pIZtMlok%r#kr7mhety?V_Rr_pp5sTLPY$g2` xTN!F`_(>21K@bE%5ClOG1VIo4K@e_X{soyc^FLpUkrn^|002ovPDHLkV1hs)2}=L~ literal 0 HcmV?d00001 diff --git a/designer/data/icons/loading.gif b/designer/data/icons/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..859f31bd7700a84b5e4cd7cf974a13b9783d27ff GIT binary patch literal 2890 zcmb`}X;f2Z9tQB6#sGLP3o~~L!5y@ zW0*FZ+B@2Ze;HOfD;v%<7-9?~zm6b({&M<*K0U-RBWP+=h_|~Z0SBLSdWBL+h!9cR zzGeFv2=VrS%AB&We8K8i{*J2Jf66p>FaB&OG89Bjh?zQtHynIUvTQ_WK*l7yc~`$& zx^I)=Nz1DR+qdq|#bz$X9{X7*UGF-aS4g|wCBHpmsyR=Zgs`y8F_Wfl%9}uEv+4O` z8UE}-7eXS1P)J2l4ihe75l{k0hXx zQqK!{0)JXdBdLVsWk)6(RqcraFS3U0dei@w=RYU|E}ez}iU%N%{$t6n5mEqch=4EP?=Iin z-P8T%^&9OZZR*lgo4?uA-PH~FYIikc1nHzNPq#sS{v!78b$%;a?KSgaRc))XcSl27 z`HhvuC0ozS0&hOe3Wzp-xv6iaj_c% z(_9|EY)Bx~#v^N7Xe?hAZM_niCuC)3aOrfeT%h+AWYT@5i!>virW#Uo_Vs%?uJWK zaWlDstV9QU^z9N#1tp{7?A(e)Yx;|s^%zm#y;Pk24Q~B zFm+KX5ET^{fgTt#Sd5}=MIa2e(CYM6>6-Bxlw>Ra>{+u%Nukw%3_%$T+5i)ttSUQeCz|mbK(rdp&sEiomwxNOZ{|CksQ)g5EZ&oxAd(+Gax{?B%<*@6 zase@raT1LtiGzRFQQ1e+T-!9{7-0p#RO%=NQ>Yr0a(2ROKpc57GCBu<3w4CP0xqb1 zX8N|-U@etMQ<76a7Q%oepv0fzeo+4Zz9GGuVp*nBOA1>KEN`8yb)+9kf0-BTIAecs zG9$jOuQD{Pldr;6J+(wf@Z*RjJ5Rgb)U~So6X&;}5$qK9Ofgx{jZQKZmKW@nnuxM-#ysM>tk@{3Pep!MG|D?`(~X<><&N-&?tV zYtZ%`xYCYp9V%gGgdrj(Q-tU#%Q!T?PGl8DqC;uxL%*H0f_;}#m;?xYk&@NHS+wg_i7A0O*Sa@>@FMNS{SI*-U z3Mx4y49^axMXmo^_B`t%ZoFIX?;*Yw>gwNKPsY(==h^S*fBw_Uto*%E>Eab=u8zNsX`$zM(^3kU^swS3I}>PfF`Zwq1l^;fuamFOG?wSBR%ER`dD@Lc z8i#Rln43@##umC#2w?<@x>?6|<;HTyky6e%ck6l-7@1sSFFdNdsk+kMAs4ktt}58E zU9NUqZjk*loj@Ucrjagg+DqHOC0VXcjJlug&f!Q)m)8yrk`Qd>hgBDUY7AiSR~^O& zDi5e|Za^A1;jV6SnCGmV8;G+m;Iq zJXcx!>ysGDyFqgNoEPSLl+o75YAe(h#vG`jr!d{14Tiuzbk}x=0w#<+Bm$(+Xn3%J z=0cYN9PnYxpvACfF#DrjCJyd(F)PF4Y0)*Q4}E*<-3k^TyC`qlvbrp{qjF%z@T$#s z^13Fja&^3ywtWOqI)vdH$LjsCFCYELQ<=Sca`nlQ-)`=@hXm}+>sZ?Q;2*H?O}(ptu^I!laDJ7 z9zHVp=;3j^${!k=FI{d?N+mSucm`F>h>)_pSsiX-Z*fG7x5T~Q{&s*QY|xD%re0U_ ziNq+yjo!X8f;U0cCi$(*Yy51T^S)~v5i~@WVYxQry{hO_)wfT8@E~DBb620JKLHB? zjPUGX+iU4?3~6GbNf2j)cRKmY&$ literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/default.spec b/designer/data/new_templates/default.spec new file mode 100644 index 0000000..d084dab --- /dev/null +++ b/designer/data/new_templates/default.spec @@ -0,0 +1,233 @@ +[app] + +# (str) Title of your application +title = $app_name + +# (str) Package name +package.name = $package_name + +# (str) Package domain (needed for android/ios packaging) +package.domain = $package_domain + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas + +# (list) List of inclusions using pattern matching +#source.include_patterns = assets/*,images/*.png + +# (list) Source files to exclude (let empty to not exclude anything) +#source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version = $package_version + +# (str) Application versioning (method 2) +# version.regex = __version__ = ['"](.*)['"] +# version.filename = %(source.dir)s/main.py + +# (list) Application requirements +# comma seperated e.g. requirements = sqlite3,kivy +requirements = kivy + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, portrait or all) +orientation = landscape + +# (list) List of service to declare +#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY + +# +# OSX Specific +# + +# +# author = © Copyright Info + +# +# Android specific +# + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 1 + +# (list) Permissions +#android.permissions = INTERNET + +# (int) Android API to use +#android.api = 19 + +# (int) Minimum API required +#android.minapi = 9 + +# (int) Android SDK version to use +#android.sdk = 21 + +# (str) Android NDK version to use +#android.ndk = 9c + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) ANT directory (if empty, it will be automatically downloaded.) +#android.ant_path = + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#android.p4a_dir = + +# (str) The directory in which python-for-android should look for your own build recipes (if any) +#p4a.local_recipes =q + +# (list) python-for-android whitelist +#android.p4a_whitelist = + +# (bool) If True, then skip trying to update the Android sdk +# This can be useful to avoid excess Internet downloads or save time +# when an update is due and you just want to test/build your package +# android.skip_update = False + +# (str) Bootstrap to use for android builds (android_new only) +# android.bootstrap = sdl2 + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (str) python-for-android branch to use, if not master, useful to try +# not yet merged features. +#android.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (list) Android additionnal libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# (str) Android logcat filters to use +#android.logcat_filters = *:S python:D + +# (bool) Copy library instead of making a libpymodules.so +#android.copy_libs = 1 + +# +# iOS specific +# + +# (str) Path to a custom kivy-ios folder +#ios.kivy_ios_dir = ../kivy-ios + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 1 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + +# (str) Path to build artifact storage, absolute or relative to spec file +# build_dir = ./.buildozer + +# (str) Path to build output (i.e. .apk, .ipa) storage +# bin_dir = ./bin + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/designer/data/new_templates/images/actionbar.png b/designer/data/new_templates/images/actionbar.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d56b63ee56fd4dac5bfb59ff1083c6a54aa868 GIT binary patch literal 11019 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 U&L-_Z@1){R>HnDd!=+#U1H^NM00000 literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/images/boxlayout.png b/designer/data/new_templates/images/boxlayout.png new file mode 100644 index 0000000000000000000000000000000000000000..290d4b7a0060885908bee56c1ea3ad5213d3668e GIT binary patch literal 3917 zcmds4X;@Ro8a_xXDxh3NDqxUa1g)}3*ufwo0%eI1N&rzTTf`UwBoH7Vs8v$1sGy)E zvWS4ll8d=45hPte3P@7fA%q|ZVM{^?ixfh7a<%vN$L*i~s?Is*nKR$aIWyn9?>F<# zx2HUul(%VY0{}qT#rcF60BmXo0J*g-n?Xy*?Qg@tY;%yS(+NN-y9%C{W`UNik22_*R}HMNRZVq04wjKrc4>TO*k+1|LfkHQw3-7v8I|{ znmiV5`I;4b5L(n{HJ4S{uXsrby;^~)Tul&%Rr?|a>!X4V8S4wfapT{~0VyVfbR0?} z14|(a=z5!gV`Ym=OA;t4*>jH~0O)$UWInM1fKy?;Nz&&mRRB1MRSBdoZbJY-L8JY} zPXJ&`5CEuv)2B_61CFW411Z|zEZQppz!h+QJ~{FMHu$)_`5lwr^A8)FKYH=uGwqMw z{5M|y9&t0utvZ;7$KzkplS($UXwy?u&Yr3p-E#Iy^G>9J8gxH%fDy|dlJ<|H(P&ps zRr*Bs=@Ne6^MshRht4Pzui|pTE62|yhLir(e)0G6J6_V2!4<-?OtQ9uii@*6jI5&6 zCQr2AU0vuf!bz6;Spy7$aAdD#!M!~_s>RMXNXEC!L z8qLVF@u6z^zPI+%N3SS$_4Q?g2Hl)9v3^V|9%X-9Jnuoy;i~$elPdW$0gj#_qOq3v zg|-7w6O+uV3Oktb1}og;G zQsG~7h{NHOCar`0J`L^2w*7Ug$glRIpB8bhx^ezTMQGkGSj4$=+T-Kn;aDs@dd_fO zz-hp}oqwIUE^4I0f=qMG|kETA)rZKG#mm{gnP7AIrs+m9_lr}Wj zAOn+PYO&lrce44`pn_qoOZ5kml9CV>>iQi|nLROw4qS>ofBx~*^z{9qrj-$5I+h`c z9U{cYU_EMT>TN4s7k4*_TRsca2eCBcEh#H0sltygdEb6C@GJ%``o&?#ZkM#Yyu62X zb#=~Owvxr}mJEnZSio!<^D9+H(F;xNOY5v%)U|H7ok1;zZO(_nZAHyDRw?x-Po7NE zjHv&biRpM+0gWrBQdy9o(VL+^T~`iAqsg`A7YB^Tajx0=E=VL2q>QKPU}HSau<7ks zH;k33>-rSM$_rlui|G}<)j`bQk)#z)8IQzfv!TYuxAv>=-AhTxh^ytL!dC)7ua>`{ z5~sV0SLbM~u*4jj=41EG_Q=gUnSg1WucV`+1BD?geP~hRFB%uSTp8201_lOY-iNxE zC9}&6kYI2ysTrN6IHs#j6y1bljzJR_RVvTQ~)q+6WpP$XI z#Rdf(SCPN6r{LoC)KuJban8mhlFT2Z9q7n00&7; z$b41gRC;{5oZ8fWhQPTC#sY7a%Ge0ZM;mez#qQY#r_C|kuaIYAx`LT@()Fwe`7J>y z&|g6N0vIMdG0TpnurKkJqEVxpL#@JrYMnan zC8AdRo7(}=ZMv(124Qq7gz%=aZp3;zN+OZam-IKXKha-MX+j3W1<~ z0cz6`?TGMyfC}`EHR<&G8XOH&p#?qm!y%x^zhgln=gP*QyM65X_08|3Ye0GL`?kei zM?QTw#ycjY??Ao&@qzI7kQ9HB2!9&pcWg+9@20(DfcYi>_!81A01qItgGfPzeL!Wx zg-t-pPKBsSAL#!8*3{5=k9%;Dj3dqV!#zF!VTq4J@OZVhV0PAZr|_ISP!M20+|`xo zkh*>1=i536yX1{k-i?fm$*EUG>YADz&CMZc3^jEIMt>>csH5kjgWJ00fl)%SSJ@-* z_OC4QuPiPs{9IYN#?;KLm`=9>H=J+`rn50#Y?8dO{*r7V^i_d-c<5QF)z=JF%ae0+ zP0Y=+MIw=#o7?`fkU2^bh*}EtpCok6kT=`h+}y)43XPsM@=TmVIk>vwW|n>xQJpJ& z9&8%}Ke}=ACMr7GfEl0 a?11jb@~XxseYa$%S{Fx;6II90UHJ`{3fFD` literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/images/carousel_actionbar.png b/designer/data/new_templates/images/carousel_actionbar.png new file mode 100644 index 0000000000000000000000000000000000000000..3f1565e8e654ce6647d32f12f9cc370f60f4b958 GIT binary patch literal 8050 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 Rphw?^r%g{4o^bx*KLPy*IvD@} literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/images/floatlayout.png b/designer/data/new_templates/images/floatlayout.png new file mode 100644 index 0000000000000000000000000000000000000000..2fd5e5f4fc4b51f449160638cc179d5f12420f42 GIT binary patch literal 3664 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 literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/images/screenmanager_actionbar.png b/designer/data/new_templates/images/screenmanager_actionbar.png new file mode 100644 index 0000000000000000000000000000000000000000..60f2c2459186eb3faf0c97da071009100bd8fd26 GIT binary patch literal 7428 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 literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/images/tabbedpanel.png b/designer/data/new_templates/images/tabbedpanel.png new file mode 100644 index 0000000000000000000000000000000000000000..c23c52e35691268b9311e7b47a12375791a836eb GIT binary patch literal 5488 zcmdT|c{r5q_kSc&vP^|2Gl?W+-ztM*A}OM>XUR@NWF7RrQI^J(sqDNNN(o7pv4=6r zGOw+WeKHcpZZMedGe%v%@3-IeyRNR@Kfb?fu9@ea`?=3~&VA13bI!S+$cy?qTswqz z006+Hdlq2?09$InFB``;aHfl%)Cpd;UDMM+0CdLZuZrgwaAv#bSu<|{;M~plvH*$6 zd%;O|AKeRQ*!wv4v+X^(Lm<2uoDz1qZ0e)s;qLBm!w38e09rR4Y<(Q;5BRzGI3Li_ zy>QW9+JPMa_H^hXPG9!#nd~!jPF^Uho*7M%YX68mVt#@z$0g}3{iCth2_49u`Sd^k z+!rWH;q3As3IEE%cG?hPxaV}3sB(#AM3#VLj`5i@lCCnn`q9Sjc(RSa?ulK83zDPb zlQSGs&VQuN{jux9^SPE)X#2n?zoIXHutOuRWEnYF#S#UGuf@}7-ac^)+!+3=EP-WuA<6Y#*(IYDZLM|^`^p$2sy5Dcb`@oCm8j9mz zj^fS0U6EOUcHWvxV-p3$n1%6Agg@Ch(=SV&(b3Vl-d`S2FCdmiBouzK+81}#*9^YU zacO3GH0}AYMp*Z8^lk&VTrQ5Bczp70d%Af}{l^>&EEX%u4TU>H6>Jv1b$a3XOb~8v zZjTyAmKHyI*wxGYw-$mC4HNg{H2B+hH9f~t+E6Gg-2?J-p# zj_f9T3h5;=qM@i;O_9C*V!G)$Ia1J|iF{MsO$Sh=S6^OWOecp+JOQQBzc+M!*$KWn zT|3jHK&(qtavQGS=b?xeOeBB^zeZb}oEf~sQ{Qf&A$-&%_x@*(s$k<5R-hi6t5Aat zj_N=tmkn86f7I+qN{^O~>MQdzpk1>>&fxPM8lU*DEqns^h0UcY?k11J85W4pwcfb2 zKP#DU(`&Z7OP=|HG2q5YB`Lc{W#?6z4{mvo%bV+G2SeY3g&dDjrIa{(c%W6M$`wG@ z?46vPgtYpJPsHj;M`}LhO+(J=mVB_AI=INWJsjnaVyD@s?b&N{ohqt<9tLZeL8 zel;~UuUamZx)}ltR^~eyHJTQx9s=5Bi(c#XCUR|$d3U<)EA7OU8QCAE6es*z^L#wau*xx4J=<53orSwG|k5$YQq>=~>bZp{_4Pkmw~}V)cd- zoKhq2K?5a>^Q?qTtb-RGXao_2{p#vb`$+vkQz&6o-@28BaZ>4cZnS_kQjStZUvF?M z@S1B@Ax4F#z2vzRPnlamaO z@sm>g`Xi?*Y0|!vJ$ww8k2J=i{qMJ?nw&@-Oo)uvZ-h&iPuDoRxJ0V-``5x9J)wSm zw@ls^7Zcb7EI0f;(Tnr`$Oymu)92dzaYLxLxC{8&#KXy+nr{mW@mEZ)DhE8j-FS7xBFHtlXsZf$GZRor`1?|Rp3+d*7DSx#Zj zv?^q+`aGqQMq#8*`1%s5;3gFK`)^*T+Z~Nr>OLGqTz6Mq#iBWKzEwxm>RE9y6*h1* zu938|^)(Jz@2XC;n;FJ?5!I-54E!AJw;~dC(yT+44}lS=2!*e00mu#<=px-kkFPpb z&-&44b1q9hd~bi(k_e_%yZ5n&@;wGBA$i`@TCYjn9phXNXI|m`YaCu)bE|B^zIbtj zf*Va;0%^?PBU!F7M4eyCd=ESK{`kjW3cq`I+H+s~*`#j>l`>J8f$na>(bS+DtMly! ztsNbE7})b02v(t#&$V>nyq&HORI2gISnUTfJO4nz=FUL2&-ACEN)Wv|3Od?TT!8=PMau@;E(ni zd2CX@@W~oRqfViV`vV)dj?TbaPK+i;M3m6iY4?y-V1DWkre2+kCkKFqgu%-*(fs(z zVA}9x@j#5q0J5Fu0N`yrTg}Q-_wq{i@Dqz0k={f#l^Binr3QQX!goo&v0|7I?nxgt zuBZJ15~y2rZQ6sd$bMq0W)_$Or>f{si=yj-a5N(;qv>e`XJYua+pR^fOsoP9ffbg& zxw#pe27waiwvR>)QEGkE=1=yN8FuXEU?yWs+_Ei!r@g3KX=M#@m?pw@2_lPH8QjV& z5=Xd)I_W2G8GEc;3J`BF6W^FUP;t}FJq%kB7*wh*Yg6RjfC|jciL^p*6thz#`(y9s z31M9Q@TOWg5Kj_;52zd8%J{5qLcuW0-O|YV*Kdij&0e{JKTq#kScAstt+`r`pC?Z}D?OmVKA zLu9huxAofgg_ldq&XQX`Y*=8kD*rt1svREyoK5MCvvx-ny6GFNY0jqwC%#7~I`9)k zF_Gr&HCc*MQLf^iW{Z=Qrs=sKJTx>uKidyjvEPcLA zbXP!FUK5=a_wh1zBfoiZ9 zefc=zT0s?##rZr(=-ecA1~$6t^vb+Yvb3}`-7R+N=y|(%!bDr?mw1UaYN%O++3eG8mrOiN44R|+L2A_C^S${QVHz6XbilPWj~08QXs=_kj`uD+ueW)@PFiFq0BC|ea+oXt zaM%I>FW6)M(gKFYe@4>}TYlT7+uQF>2PJ8|2G0%|GGQjiJo1^NxD=n*E8zZuMUsi5 zIbd%+#%Zeu{MVWP!x7(u(k5JU^=h9n3vhcr+|zqbjyZaL2-t|{5AV+%4da2Hv}dr_ znnT9Ch(wDlK4E6Xafs$&wz^wKKc#DeLjp?;(HOl>!AsMj+`>cNvj*`Nt4 znoD{_$Tr{rN(6b%B8e$#De+}f(xY;%e^Vf!Tb?Q4kDujdj=FaJBHn&-&VM5IzY&l> zjK0e`L{$#8VTLHrwEx;>uvEvynEp7PMzml9fHr}eC*Yy2rf;Ke6k# zz4wEszpd#<#{TCt?G&h)a$m3B3{ieLeskp8rpxq)r+QlPj*WaO#qyga<0mM6&vXAt z<)0SDck`}OLMt~7L7V$OVg|6>ZwmT7yv}c!W=5X@*>g%#tHhaaBO$6NZ3pX{kzftm z_mzytWypqkK0istcynD_p^V0{|{&sZxV4h3e}V2|T! zUtq?#e^TpsQnR*rT;mx80%utSBD%?9ts<#$1eDSiPn+T#VzgFcWBHok9aZoc-wK|n zAdqEbuFPG|a7_fl$Ip+@UlD{gdSQA5d=ZB_m$@s5Nm4#!eSo2M~GukmeK+^OXVHmnuuw*;d^ z<<)}8x^aabMc9B9B+x*41WE-#j{zY<6D?p2Y2=Y$P<;`|O!UTkfEncPR9#6995IaF z9&`|ZrKZS74-}p>gM<21!Va9i!~pf$2BSAs-dEv}dF-w|)DI8D#gWKV_!EE+6G)L% zQj}#gt%>7~2{+s$BYSOQm?QSpaUBah{faqg<*BiY7u*}9>4f>(I+XSK!w#j1I>4b#W0kCLY2ml5CBZbqSj$xUt z2i#E$L4D^MQJW8pE!&B~VDcB}1{qEe)2CPcGW*Gc0aH<8-4R?s&;r1?zI_7+Ey@|e z{zQs>8zNb5#{R<3*%^jNdc4V#WzRdsOHE*Sn>^VRP&R+nK@7{!2jJAiVO5M-UJe8& ze&{(_zEu2YGDfhjwxWcH+xP6sgZwahoS@+!l;E@pWx97r&5K>Fs~s9`?wK<4t!^Ww z!H$Jk0H8SLe>k^$ib6l&#@<_XlMhRNKCr)* zvW^@kBqIm3OH)tA=@!uq4Swa#tSbKIa>NI{h;XBjQ!j-!LwwBN1R?Xr7oMWr{Qs=( z43-jAl8apg>hs3Vc1*a#n*7q6uZFQEPxgX_erbGXu^Ca0S*I(o%9f|T#X4KD2ueh< zJ*T-A`wkQa`AS*WdmAxu#bC!%!ZU06e5GTAug%_~oep!@u}{Zj+37|bU?U1MVONDgP6%FYXFCIJyQ++313FFgmNvt%`4Exfb3vvL0#y~)lScIcJV=46K> z-?u?9st?X1r}M@}^mf*SkP}X2auW9J_b@xAtc|*Atl|R1=zQQ2eB;+Akrl3U6>dJ{ z=PiBx`ad?oT5j$MXcr9Dyd98EM~NrwR|SxCRETn6r33{d)?wRnI&zl}w26AoV{@BT z&EJ*j;E5(msNFul0B8{IDBt4BFAL5Qse+9%p zum$&-9C`SgX)|YO=R$ehg9V=(|tNVt6G#dnb)jzE+Cxx$?sZX9D)(0T+F`w z@uxbtBCfC%v`0-8@gDQCCnSY?mvRO1o8Y|&hVAzsgpH?$>LLgvWU8Q3){|MI2d-Gp z^a+DXu4eIUN};dINhw@BcSr|FbkO14kYebafAMz_eqNbtj{sqO{402|`}0ztmzGFh zFs?VLI;`RSrFE2t{Jg01hdWL_yY;{+TsICmq{8Zz?i88{nbVml^JmkX)su4-?zyKQ z#1!a^EZ+F(8`J-WsP{D%xt@bUvR1%C7<~iLr&rtf8O{m6eAI@@b+3faoF*as;&F>d zmG8U;Z&~XhhE|94fMp0wh%rRDi?mFKv`0dO62`Qtz_PGuQ#9NMg-*`SS+^73!ujo4 z0vNWDmqJ=pDhic%Fda3N^00diCk9d&7b=FA;!*?XrSWt+3SL6Hjuxz`ZXKKMcX}vF z5-@w%iJWHZg9=FXbN{N8dtwfk)M>Y&qJz>$zsb@U8LzNVtR-s4p??JK^?L(Hb?KBP zPbeBSQkfth@@D1k0XMpZh~tB)k&O1*(TWmJ(kIU3K`r2nkCbGvL~JyktM!2IO*hUD z(_FihYT59x&7B%I?bNNdoJ!2p?sEbA9s7FrJ9atr#S~i81RP&J^u_MfNm5b?I+T9@ zWT@Y*E(PthSdJ48up1(b($dn9Yd`$*8&9q4As6+KtBfhD1u5zGMH2D{Fkq=z4g>Nf zD_RRZRgl9)un5M%H>+9d1CoLarZLxA`2HeX>7~ek=HZ=&c~x{Tg0LN0zI>m Aa{vGU literal 0 HcmV?d00001 diff --git a/designer/data/new_templates/template_actionbar_carousel_kv b/designer/data/new_templates/template_actionbar_carousel_kv new file mode 100644 index 0000000..4163c28 --- /dev/null +++ b/designer/data/new_templates/template_actionbar_carousel_kv @@ -0,0 +1,37 @@ +: +# this is the rule for your root widget, defining it's look and feel. + action_bar: _action + carousel: carousel + #These are the auto provided ActionBar and Carousel, change them according + #to your needs + ActionBar: + id: _action + size_hint: 1,0.1 + pos_hint: {'top':1} + ActionView: + use_separator: True + ActionPrevious: + title: 'Slide 1' + with_previous: False + ActionOverflow: + disabled: True + ActionButton: + text: 'Go To Slide 2' + on_release: carousel.index = 1 + ActionButton: + text: 'Go To Slide 3' + on_release: carousel.index = 2 + + Carousel: + id: carousel + pos_hint: {'top': 0.9} + on_index: root.on_index(*args) + BoxLayout: + Button: + text: 'Slide One' + BoxLayout: + Button: + text: 'Slide Two' + BoxLayout: + Button: + text: 'Slide three' diff --git a/designer/data/new_templates/template_actionbar_carousel_py b/designer/data/new_templates/template_actionbar_carousel_py new file mode 100644 index 0000000..684bd11 --- /dev/null +++ b/designer/data/new_templates/template_actionbar_carousel_py @@ -0,0 +1,84 @@ +from kivy.app import App +from kivy.uix.floatlayout import FloatLayout +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from kivy.uix import actionbar + + +class RootWidget(FloatLayout): + '''This is the class representing your root widget. + By default it is inherited from BoxLayout, + you can use any other layout/widget depending on your usage. + ''' + + cont1 = ObjectProperty(None) + cont2 = ObjectProperty(None) + action_bar = ObjectProperty(None) + carousel = ObjectProperty(None) + + def __init__(self, **kwargs): + super(RootWidget, self).__init__(**kwargs) + self.cont1 = actionbar.ContextualActionView( + action_previous=actionbar.ActionPrevious(title='Go Back')) + self.cont2 = actionbar.ContextualActionView( + action_previous=actionbar.ActionPrevious(title='Go Back')) + + self.action_bar.bind(on_previous=self.on_previous) + self.prev_index = 0 + self.from_actionbar = False + + def on_index(self, instance, value): + if value == 2: + self.action_bar.add_widget(self.cont2) + + elif value == 1: + if self.prev_index == 0: + self.action_bar.add_widget(self.cont1) + + elif not self.from_actionbar: + try: + self.action_bar.on_previous() + except: + pass + + elif self.from_actionbar is False: + try: + self.action_bar.on_previous() + except: + pass + + self.prev_index = value + self.from_actionbar = False + + def on_previous(self, *args): + self.from_actionbar = True + self.carousel.load_previous() + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + return RootWidget() + +if __name__ == '__main__': + MainApp().run() + diff --git a/designer/data/new_templates/template_actionbar_kv b/designer/data/new_templates/template_actionbar_kv new file mode 100644 index 0000000..2c91aae --- /dev/null +++ b/designer/data/new_templates/template_actionbar_kv @@ -0,0 +1,26 @@ +: +# this is the rule for your root widget, defining it's look and feel. + ActionBar: + #this is the auto provided ActionBar with some menus. Replace it with your + #app structure + pos_hint: {'top':1} + ActionView: + use_separator: True + ActionPrevious: + title: 'Action Bar' + with_previous: False + ActionGroup: + text: 'File' + mode: 'spinner' + size_hint_x: None + width: 90 + ActionButton: + text: 'New' + ActionButton: + text: 'Open' + ActionButton: + text: 'Save' + ActionButton: + text: 'Save As' + ActionButton: + text: 'Quit' diff --git a/designer/data/new_templates/template_actionbar_py b/designer/data/new_templates/template_actionbar_py new file mode 100644 index 0000000..85fe386 --- /dev/null +++ b/designer/data/new_templates/template_actionbar_py @@ -0,0 +1,38 @@ +from kivy.uix.floatlayout import FloatLayout +from kivy.app import App + + +class RootWidget(FloatLayout): + '''This is the class representing your root widget. + By default it is inherited from BoxLayout, + you can use any other layout/widget depending on your usage. + ''' + pass + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + return RootWidget() + +if '__main__' == __name__: + MainApp().run() diff --git a/designer/data/new_templates/template_boxlayout_kv b/designer/data/new_templates/template_boxlayout_kv new file mode 100644 index 0000000..04c4752 --- /dev/null +++ b/designer/data/new_templates/template_boxlayout_kv @@ -0,0 +1,5 @@ +: + # this is the rule for your root widget, defining it's look and feel. + Label: + # this is the only auto provided widget, you should replace it with your own app structure. + text: 'Hello World !!' diff --git a/designer/data/new_templates/template_boxlayout_py b/designer/data/new_templates/template_boxlayout_py new file mode 100644 index 0000000..9a734b2 --- /dev/null +++ b/designer/data/new_templates/template_boxlayout_py @@ -0,0 +1,48 @@ +from kivy.app import App +from kivy.uix.boxlayout import BoxLayout + + +class RootWidget(BoxLayout): + '''This the class representing your root widget. + By default it is inherited from BoxLayout, + you can use any other layout/widget depending on your usage. + ''' + pass + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + '''Your app will be build from here. + Return your root widget here. + ''' + return RootWidget() + + def on_pause(self): + '''This is necessary to allow your app to be paused on mobile os. + refer http://kivy.org/docs/api-kivy.app.html#pause-mode . + ''' + return True + +if __name__ == '__main__': + MainApp().run() + diff --git a/designer/data/new_templates/template_floatlayout_kv b/designer/data/new_templates/template_floatlayout_kv new file mode 100644 index 0000000..7a1663f --- /dev/null +++ b/designer/data/new_templates/template_floatlayout_kv @@ -0,0 +1,5 @@ +: + # this is the rule for your root widget, defining it's look and feel. + Button: + # this is the only auto provided widget, you should replace it with your own app structure. + text: 'Hello World !!' diff --git a/designer/data/new_templates/template_floatlayout_py b/designer/data/new_templates/template_floatlayout_py new file mode 100644 index 0000000..bee4d17 --- /dev/null +++ b/designer/data/new_templates/template_floatlayout_py @@ -0,0 +1,49 @@ +from kivy.app import App +from kivy.uix.floatlayout import FloatLayout + + +class RootWidget(FloatLayout): + '''This the class representing your root widget. + By default it is inherited from FloatLayout, + you can use any other layout/widget depending on your usage. + ''' + pass + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + '''Your app will be build from here. + Return your root widget here. + ''' + print('build running') + return RootWidget() + + def on_pause(self): + '''This is necessary to allow your app to be paused on mobile os. + refer http://kivy.org/docs/api-kivy.app.html#pause-mode . + ''' + return True + +if __name__ == '__main__': + MainApp().run() + diff --git a/designer/data/new_templates/template_screen_manager_actionbar_kv b/designer/data/new_templates/template_screen_manager_actionbar_kv new file mode 100644 index 0000000..e49a07a --- /dev/null +++ b/designer/data/new_templates/template_screen_manager_actionbar_kv @@ -0,0 +1,26 @@ +: +#This is the root widget's kv definition + manager: manager + ActionBar: + id: _action + size_hint: 1,0.1 + pos_hint: {'top':1} + ActionView: + use_separator: True + ActionPrevious: + title: 'Previous Slide' + with_previous: False + on_release: root.manager.current = root.manager.previous() + + ScreenManager: + id: manager + pos_hint: {'top': 0.9} + Screen: + name: 'Screen 1' + Label: + text: 'Screen 1' + + Screen: + name: 'Screen 2' + Label: + text: 'Screen 2' diff --git a/designer/data/new_templates/template_screen_manager_actionbar_py b/designer/data/new_templates/template_screen_manager_actionbar_py new file mode 100644 index 0000000..774a30a --- /dev/null +++ b/designer/data/new_templates/template_screen_manager_actionbar_py @@ -0,0 +1,45 @@ +from kivy.app import App +from kivy.uix.screenmanager import ScreenManager, Screen +from kivy.properties import NumericProperty, ObjectProperty +from kivy.lang import Builder +from kivy.uix.floatlayout import FloatLayout + + +class RootWidget(FloatLayout): + '''This the class representing your root widget. + By default it is inherited from ScreenManager, + you can use any other layout/widget depending on your usage. + ''' + manager = ObjectProperty() + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + '''Your app will be build from here. + Return your widget here. + ''' + + return RootWidget() + +if __name__ == '__main__': + MainApp().run() diff --git a/designer/data/new_templates/template_screen_manager_kv b/designer/data/new_templates/template_screen_manager_kv new file mode 100644 index 0000000..7c2f1e8 --- /dev/null +++ b/designer/data/new_templates/template_screen_manager_kv @@ -0,0 +1,12 @@ +: +#This is the root widget's kv definition + Screen: + name: 'Screen 1' + Label: + text: 'Screen 1' + + Screen: + name: 'Screen 2' + Label: + text: 'Screen 2' + diff --git a/designer/data/new_templates/template_screen_manager_py b/designer/data/new_templates/template_screen_manager_py new file mode 100644 index 0000000..73db19e --- /dev/null +++ b/designer/data/new_templates/template_screen_manager_py @@ -0,0 +1,44 @@ +from kivy.app import App +from kivy.uix.screenmanager import ScreenManager, Screen +from kivy.properties import NumericProperty +from kivy.lang import Builder + + +class RootWidget(ScreenManager): + '''This the class representing your root widget. + By default it is inherited from ScreenManager, + you can use any other layout/widget depending on your usage. + ''' + pass + + +class MainApp(App): + '''This is the main class of your app. + Define any app wide entities here. + This class can be accessed anywhere inside the kivy app as, + in python:: + + app = App.get_running_app() + print (app.title) + + in kv language:: + + on_release: print(app.title) + Name of the .kv file that is auto-loaded is derived from the name + of this class:: + + MainApp = main.kv + MainClass = mainclass.kv + + The App part is auto removed and the whole name is lowercased. + ''' + + def build(self): + '''Your app will be build from here. + Return your widget here. + ''' + + return RootWidget() + +if __name__ == '__main__': + MainApp().run() diff --git a/designer/data/new_templates/template_tabbed_panel_kv b/designer/data/new_templates/template_tabbed_panel_kv new file mode 100644 index 0000000..b0c1c66 --- /dev/null +++ b/designer/data/new_templates/template_tabbed_panel_kv @@ -0,0 +1,15 @@ +: + TabbedPanel: + do_default_tab: False + + TabbedPanelItem: + text: 'Item 1' + BoxLayout: + + TabbedPanelItem: + text: 'Item 2' + BoxLayout: + + TabbedPanelItem: + text: 'Item 3' + BoxLayout: diff --git a/designer/data/new_templates/template_tabbed_panel_py b/designer/data/new_templates/template_tabbed_panel_py new file mode 100644 index 0000000..804737b --- /dev/null +++ b/designer/data/new_templates/template_tabbed_panel_py @@ -0,0 +1,15 @@ +from kivy.uix.tabbedpanel import TabbedPanel +from kivy.app import App +from kivy.uix.floatlayout import FloatLayout + + +class RootWidget(FloatLayout): + pass + + +class MainApp(App): + def build(self): + return RootWidget() + +if __name__ == '__main__': + MainApp().run() diff --git a/designer/data/new_templates/template_textinput_scrollview_kv b/designer/data/new_templates/template_textinput_scrollview_kv new file mode 100644 index 0000000..e3c2a78 --- /dev/null +++ b/designer/data/new_templates/template_textinput_scrollview_kv @@ -0,0 +1,6 @@ +: + ScrollView: + id: e_scroll + TextInput: + size_hint_y: None + height: max(e_scroll.height, self.minimum_height) \ No newline at end of file diff --git a/designer/data/new_templates/template_textinput_scrollview_py b/designer/data/new_templates/template_textinput_scrollview_py new file mode 100644 index 0000000..9838938 --- /dev/null +++ b/designer/data/new_templates/template_textinput_scrollview_py @@ -0,0 +1,14 @@ +from kivy.uix.floatlayout import FloatLayout +from kivy.app import App + + +class RootWidget(FloatLayout): + pass + + +class MainApp(App): + def build(self): + return RootWidget() + +if __name__ == '__main__': + MainApp().run() diff --git a/designer/data/profiles/android_buildozer.ini b/designer/data/profiles/android_buildozer.ini new file mode 100644 index 0000000..6080a3d --- /dev/null +++ b/designer/data/profiles/android_buildozer.ini @@ -0,0 +1,9 @@ +[profile] +name = Android - Buildozer +builder = Buildozer +target = Android +mode = Debug +install = False +debug = False +verbose = False + diff --git a/designer/data/profiles/desktop.ini b/designer/data/profiles/desktop.ini new file mode 100644 index 0000000..cdd6b79 --- /dev/null +++ b/designer/data/profiles/desktop.ini @@ -0,0 +1,8 @@ +[profile] +name = Desktop +builder = Desktop +target = Desktop +mode = Debug +install = False +debug = False +verbose = False \ No newline at end of file diff --git a/designer/data/profiles/ios_buildozer.ini b/designer/data/profiles/ios_buildozer.ini new file mode 100644 index 0000000..4d660dc --- /dev/null +++ b/designer/data/profiles/ios_buildozer.ini @@ -0,0 +1,9 @@ +[profile] +name = iOS - Buildozer +builder = Buildozer +target = iOS +mode = Debug +install = False +debug = False +verbose = False + diff --git a/designer/data/settings/build_profile.json b/designer/data/settings/build_profile.json new file mode 100644 index 0000000..896253b --- /dev/null +++ b/designer/data/settings/build_profile.json @@ -0,0 +1,54 @@ +[ + { + "type": "string", + "title": "Name", + "section": "profile", + "desc": "Profile Name", + "key": "name" + }, + { + "type": "options", + "title": "Builder", + "section": "profile", + "desc": "Select the builder.", + "options": ["Desktop", "Buildozer", "Hanga"], + "key": "builder" + }, + { + "type": "options", + "title": "Target", + "section": "profile", + "desc": "Select the build target", + "options": ["Desktop", "Android", "iOS"], + "key": "target" + }, + { + "type": "options", + "title": "Mode", + "section": "profile", + "desc": "Select the build mode.\nUsed with Android and iOS only.", + "options": ["Debug", "Release"], + "key": "mode" + }, + { + "type": "bool", + "title": "Install on Device", + "section": "profile", + "desc": "Automatically install on device. \nUsed with Android and iOS only.", + "key": "install" + }, + { + "type": "bool", + "title": "Debug", + "section": "profile", + "desc": "Show application output on console. \nUsed with Android(logcat)", + "key": "debug" + }, + { + "type": "bool", + "title": "Verbose", + "section": "profile", + "desc": "Show verbose information while building the app. \nUsed with Android and iOS only.", + "key": "verbose" + } +] diff --git a/designer/data/settings/buildozer_settings.json b/designer/data/settings/buildozer_settings.json new file mode 100644 index 0000000..e408031 --- /dev/null +++ b/designer/data/settings/buildozer_settings.json @@ -0,0 +1,8 @@ +[ + { + "type": "string", + "title": "Buildozer Path", + "section": "buildozer", + "key": "buildozer_path" + } +] \ No newline at end of file diff --git a/designer/data/settings/buildozer_spec_android.json b/designer/data/settings/buildozer_spec_android.json new file mode 100644 index 0000000..91447fe --- /dev/null +++ b/designer/data/settings/buildozer_spec_android.json @@ -0,0 +1,267 @@ +[ + { + "type": "title", + "title": "Android Application settings" + }, + { + "type": "list", + "title": "Android Permissions", + "section": "app", + "desc": "Application required permissions.", + "key": "android.permissions", + "items": ["ACCESS_CHECKIN_PROPERTIES", "ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION", "ACCESS_LOCATION_EXTRA_COMMANDS", "ACCESS_MOCK_LOCATION", "ACCESS_NETWORK_STATE", "ACCESS_SURFACE_FLINGER", "ACCESS_WIFI_STATE", "ACCOUNT_MANAGER", "ADD_VOICEMAIL", "AUTHENTICATE_ACCOUNTS", "BATTERY_STATS", "BIND_ACCESSIBILITY_SERVICE", "BIND_APPWIDGET", "BIND_CARRIER_MESSAGING_SERVICE", "BIND_DEVICE_ADMIN", "BIND_DREAM_SERVICE", "BIND_INPUT_METHOD", "BIND_NFC_SERVICE", "BIND_NOTIFICATION_LISTENER_SERVICE", "BIND_PRINT_SERVICE", "BIND_REMOTEVIEWS", "BIND_TEXT_SERVICE", "BIND_TV_INPUT", "BIND_VOICE_INTERACTION", "BIND_VPN_SERVICE", "BIND_WALLPAPER", "BLUETOOTH", "BLUETOOTH_ADMIN", "BLUETOOTH_PRIVILEGED", "BODY_SENSORS", "BRICK", "BROADCAST_PACKAGE_REMOVED", "BROADCAST_SMS", "BROADCAST_STICKY", "BROADCAST_WAP_PUSH", "CALL_PHONE", "CALL_PRIVILEGED", "CAMERA", "CAPTURE_AUDIO_OUTPUT", "CAPTURE_SECURE_VIDEO_OUTPUT", "CAPTURE_VIDEO_OUTPUT", "CHANGE_COMPONENT_ENABLED_STATE", "CHANGE_CONFIGURATION", "CHANGE_NETWORK_STATE", "CHANGE_WIFI_MULTICAST_STATE", "CHANGE_WIFI_STATE", "CLEAR_APP_CACHE", "CLEAR_APP_USER_DATA", "CONTROL_LOCATION_UPDATES", "DELETE_CACHE_FILES", "DELETE_PACKAGES", "DEVICE_POWER", "DIAGNOSTIC", "DISABLE_KEYGUARD", "DUMP", "EXPAND_STATUS_BAR", "FACTORY_TEST", "FLASHLIGHT", "FORCE_BACK", "GET_ACCOUNTS", "GET_PACKAGE_SIZE", "GET_TASKS", "GET_TOP_ACTIVITY_INFO", "GLOBAL_SEARCH", "HARDWARE_TEST", "INJECT_EVENTS", "INSTALL_LOCATION_PROVIDER", "INSTALL_PACKAGES", "INSTALL_SHORTCUT", "INTERNAL_SYSTEM_WINDOW", "INTERNET", "KILL_BACKGROUND_PROCESSES", "LOCATION_HARDWARE", "MANAGE_ACCOUNTS", "MANAGE_APP_TOKENS", "MANAGE_DOCUMENTS", "MASTER_CLEAR", "MEDIA_CONTENT_CONTROL", "MODIFY_AUDIO_SETTINGS", "MODIFY_PHONE_STATE", "MOUNT_FORMAT_FILESYSTEMS", "MOUNT_UNMOUNT_FILESYSTEMS", "NFC", "PERSISTENT_ACTIVITY", "PROCESS_OUTGOING_CALLS", "READ_CALENDAR", "READ_CALL_LOG", "READ_CONTACTS", "READ_EXTERNAL_STORAGE", "READ_FRAME_BUFFER", "READ_HISTORY_BOOKMARKS", "READ_INPUT_STATE", "READ_LOGS", "READ_PHONE_STATE", "READ_PROFILE", "READ_SMS", "READ_SOCIAL_STREAM", "READ_SYNC_SETTINGS", "READ_SYNC_STATS", "READ_USER_DICTIONARY", "READ_VOICEMAIL", "REBOOT", "RECEIVE_BOOT_COMPLETED", "RECEIVE_MMS", "RECEIVE_SMS", "RECEIVE_WAP_PUSH", "RECORD_AUDIO", "REORDER_TASKS", "RESTART_PACKAGES", "SEND_RESPOND_VIA_MESSAGE", "SEND_SMS", "SET_ACTIVITY_WATCHER", "SET_ALARM", "SET_ALWAYS_FINISH", "SET_ANIMATION_SCALE", "SET_DEBUG_APP", "SET_ORIENTATION", "SET_POINTER_SPEED", "SET_PREFERRED_APPLICATIONS", "SET_PROCESS_LIMIT", "SET_TIME", "SET_TIME_ZONE", "SET_WALLPAPER", "SET_WALLPAPER_HINTS", "SIGNAL_PERSISTENT_PROCESSES", "STATUS_BAR", "SUBSCRIBED_FEEDS_READ", "SUBSCRIBED_FEEDS_WRITE", "SYSTEM_ALERT_WINDOW", "TRANSMIT_IR", "UNINSTALL_SHORTCUT", "UPDATE_DEVICE_STATS", "USE_CREDENTIALS", "USE_SIP", "VIBRATE", "WAKE_LOCK", "WRITE_APN_SETTINGS", "WRITE_CALENDAR", "WRITE_CALL_LOG", "WRITE_CONTACTS", "WRITE_EXTERNAL_STORAGE", "WRITE_GSERVICES", "WRITE_HISTORY_BOOKMARKS", "WRITE_PROFILE", "WRITE_SECURE_SETTINGS", "WRITE_SETTINGS", "WRITE_SMS", "WRITE_SOCIAL_STREAM", "WRITE_SYNC_SETTINGS", "WRITE_USER_DICTIONARY", "WRITE_VOICEMAIL"] + }, + { + "type": "dict", + "title": "Android Storage", + "section": "app", + "desc": "Use Android private/public storage", + "key": "android.private_storage", + "options": {"False": "Public", "True": "Private"} + }, + { + "type": "string", + "title": "Android entry point", + "section": "app", + "desc": "Main activity of your app. Default is ok for Kivy-based app", + "key": "android.entrypoint" + }, + { + "type": "dict", + "title": "Screen wake lock", + "section": "app", + "desc": "Indicate whether the screen should stay on. Don't forget to add the WAKE_LOCK permission if you set this to True", + "key": "android.wakelock", + "options": {"False": "Disabled", "True": "Enabled"} + }, + { + "type": "list", + "title": "Application meta-data", + "section": "app", + "desc": "Application meta-data to set.\nCreate each item with in key=value format", + "key": "android.meta_data", + "allow_custom": true, + "items": [] + }, + { + "type": "string", + "title": "Extra XMLs", + "section": "app", + "desc": "Extra XML file to include as an intent filters in tag", + "key": "android.manifest.intent_filters" + }, + { + "type": "title", + "title": "Android Development tools" + }, + { + "type": "numeric", + "title": "Android API", + "section": "app", + "desc": "Android API Level", + "key": "android.api" + }, + { + "type": "numeric", + "title": "Android Minimum API", + "section": "app", + "desc": "Android Minimum API Level", + "key": "android.mimapi" + }, + { + "type": "numeric", + "title": "Android SDK", + "section": "app", + "desc": "Android SDK version to use", + "key": "android.sdk" + }, + { + "type": "string", + "title": "Android NDK", + "section": "app", + "desc": "Android NDK to use", + "key": "android.ndk" + }, + { + "type": "path", + "title": "Android SDK Path", + "section": "app", + "desc": "Custom Android SDK path. If empty, it will be automatically downloaded", + "key": "android.sdk_path" + }, + { + "type": "path", + "title": "Android NDK Path", + "section": "app", + "desc": "Custom Android NDK path. If empty, it will be automatically downloaded", + "key": "android.ndk_path" + }, + { + "type": "title", + "title": "OUYA" + }, + { + "type": "string", + "title": "OUYA Console category", + "section": "app", + "desc": "OUYA Console category. Should be one of GAME or APP. If you leave this blank, OUYA support will not be enabled", + "key": "android.ouya.category" + }, + { + "type": "string", + "title": "OUYA Console icon", + "section": "app", + "desc": "Filename of OUYA Console icon. It must be a 732x412 png image.\nExample: %(source.dir)s/data/ouya_icon.png", + "key": "android.ouya.icon.filename" + }, + { + "type": "title", + "title": "Android Libraries" + }, + { + "type": "list", + "title": "External libraries", + "section": "app", + "desc": "List of extra Java .jar files to add to the libs.\nExample: foo.jar,bar.jar,path/to/more/*.jar", + "key": "android.add_jars", + "allow_custom": true, + "items": [] + }, + { + "type": "list", + "title": "External Java files", + "section": "app", + "desc": "List of Java files to add to the android project (can be .java or a directory containing the files)", + "key": "android.add_src", + "allow_custom": true, + "items": [] + }, + { + "type": "string", + "title": "Android architecture", + "section": "app", + "desc": "The Android arch to build for: armeabi-v7a, arm64-v8a, x86", + "key": "android.arch" + }, + { + "type": "list", + "title": "Android additional libraries to copy into libs/armeabi - armeabi", + "section": "app", + "desc": "Android additional libraries to copy into libs/armeabi.\nExample: libs/android/*.so", + "key": "android.add_libs_armeabi", + "allow_custom": true, + "items": [] + }, + { + "type": "list", + "title": "Android additional libraries to copy into libs/armeabi - armeabi v7a", + "section": "app", + "desc": "Android additional libraries to copy into libs/armeabi.\nExample: libs/android-v7/*.so", + "key": "android.add_libs_armeabi_v7a", + "allow_custom": true, + "items": [] + }, + { + "type": "list", + "title": "Android additional libraries to copy into libs/armeabi - x86", + "section": "app", + "desc": "Android additional libraries to copy into libs/armeabi.\nExample: libs/android-x86/*.so", + "key": "android.add_libs_x86", + "allow_custom": true, + "items": [] + }, + { + "type": "list", + "title": "Android additional libraries to copy into libs/armeabi - mips", + "section": "app", + "desc": "Android additional libraries to copy into libs/armeabi.\nExample: libs/android-mips/*.so", + "key": "android.add_libs_mips", + "allow_custom": true, + "items": [] + }, + { + "type": "list", + "title": "Add library project", + "section": "app", + "desc": "Android library project to add (will be added in the project.properties automatically.)", + "key": "android.library_references", + "allow_custom": true, + "items": [] + }, + { + "type": "title", + "title": "python-for-android" + }, + { + "type": "path", + "title": "python-for-android git clone path", + "section": "app", + "desc": "python-for-android git clone path (if empty, it will be automatically cloned from Github)", + "key": "android.p4a_dir" + }, + { + "type": "list", + "title": "python-for-android whitelist", + "section": "app", + "desc": "python-for-android whitelist", + "key": "android.p4a_whitelist", + "allow_custom": true, + "items": [] + }, + { + "type": "string", + "title": "python-for-android branch", + "section": "app", + "desc": "Set a custom python-for-android branch", + "key": "android.branch" + }, + { + "type": "path", + "title": "ANT directory", + "section": "app", + "desc": "ANT directory (if empty, it will be automatically downloaded.)", + "key": "android.ant_path" + }, + { + "type": "path", + "title": "Own build recipes", + "section": "app", + "desc": "The directory in which python-for-android should look for your own build recipes (if any)", + "key": "p4a.local_recipes" + }, + { + "type": "path", + "title": "python-for-android hook", + "section": "app", + "desc": "Filename to the hook for p4a", + "key": "p4a.hook" + }, + { + "type": "bool", + "title": "Skip SDK update", + "section": "app", + "desc": "Useful to avoid excess Internet downloads or save time.", + "key": "android.skip_update" + }, + { + "type": "string", + "title": "Bootstrap", + "section": "app", + "desc": "Bootstrap to use for android builds (android_new only)", + "key": "android.bootstrap" + }, + { + "type": "string", + "title": "Logcat filters", + "section": "app", + "desc": "Android logcat filters to use", + "key": "android.logcat_filters" + }, + { + "type": "bool", + "title": "Copy libraries", + "section": "app", + "desc": "Copy library instead of making a libpymodules.so", + "key": "android.copy_libs" + } +] diff --git a/designer/data/settings/buildozer_spec_app.json b/designer/data/settings/buildozer_spec_app.json new file mode 100644 index 0000000..6b39fba --- /dev/null +++ b/designer/data/settings/buildozer_spec_app.json @@ -0,0 +1,173 @@ +[ + { + "type": "title", + "title": "Application" + }, + { + "type": "string", + "title": "Application Name", + "section": "app", + "desc": "Application name visible to users", + "key": "title" + }, + { + "type": "string", + "title": "Package Name", + "section": "app", + "desc": "Application package name.\nExample exampleapp.apps", + "key": "package.name" + }, + { + "type": "string", + "title": "Package domain", + "section": "app", + "desc": "Application package domain.\nExample com.mycompany", + "key": "package.domain" + }, + { + "type": "string", + "title": "Presplash of the application", + "section": "app", + "desc": "Presplash image of the application.\nExample %(source.dir)s/data/logo.png", + "key": "presplash.filename" + }, + { + "type": "string", + "title": "Icon of the application", + "section": "app", + "desc": "Icon of the application.\nExample %(source.dir)s/data/icon.png", + "key": "icon.filename" + }, + { + "type": "title", + "title": "Screen Behavior" + }, + { + "type": "dict", + "title": "Orientation", + "section": "app", + "desc": "Application orientation", + "key": "orientation", + "options": {"landscape": "Landscape", "portrait": "Portrait", "all": "Auto"} + }, + { + "type": "bool", + "title": "Fullscreen", + "section": "app", + "desc": "Fullscreen mode", + "values": ["Disabled", "Enabled"], + "key": "fullscreen" + }, + { + "type": "title", + "title": "Source Code" + }, + { + "type": "path", + "title": "Source directory", + "section": "app", + "desc": "Source code where the main.py live", + "key": "source.dir" + }, + { + "type": "list", + "title": "Source files to include", + "section": "app", + "desc": "Source files to include (let empty to include all the files)", + "key": "source.include_exts", + "allow_custom": true, + "items": ["py","png","jpg","kv","atlas"] + }, + { + "type": "list", + "title": "Source files to exclude", + "section": "app", + "desc": "Source files to exclude (let empty to not exclude anything)", + "key": "source.exclude_exts", + "allow_custom": true, + "items": ["rar", "zip", "spec"] + }, + { + "type": "list", + "title": "List of directory to exclude", + "section": "app", + "desc": "List of directory to exclude (let empty to not exclude anything)", + "key": "source.exclude_dirs", + "allow_custom": true, + "items": ["tests", "test", "bin"] + }, + { + "type": "list", + "title": "List of exclusions using pattern matching", + "section": "app", + "desc": "List of exclusions using pattern matching", + "key": "source.exclude_patterns", + "allow_custom": true, + "items": ["license","images/*/*.jpg"] + }, + { + "type": "list", + "title": "List of inclusions using pattern matching", + "section": "app", + "desc": "List of inclusions using pattern matching", + "key": "source.include_patterns", + "allow_custom": true, + "items": ["assets/*", "images/*.png"] + }, + { + "type": "list", + "title": "List of services to declare", + "section": "app", + "desc": "List of services to declare", + "key": "services", + "allow_custom": true, + "items": ["NAME:ENTRYPOINT_TO_PY", "NAME2:ENTRYPOINT2_TO_PY"] + }, + { + "type": "title", + "title": "Application Version" + }, + { + "type": "string", + "title": "Application versioning (method 1)", + "section": "app", + "desc": "Application versioning with regex.\nExample: ['\"](.*)'['\"]", + "key": "version.regex" + }, + { + "type": "string", + "title": "Application versioning file name", + "section": "app", + "desc": "File name to use the versioning regex.\nExample: %(source.dir)s/main.py", + "key": "version.filename" + }, + { + "type": "string", + "title": "Application versioning (method 2)", + "section": "app", + "desc": "Application version.\nExample: 1.0", + "key": "version" + }, + { + "type": "title", + "title": "Application Requirements" + }, + { + "type": "list", + "title": "Requirements", + "section": "app", + "desc": "Application requirements.", + "key": "requirements", + "items": ["apsw", "audiostream", "bidi", "boost", "c_igraph", "cherrypy", "cprotobuf", "cymunk", "django", "docutils", "ecdsa", "enum34", "evdev", "ffmpeg", "ffmpeg2", "ffpyplayer", "freetype", "gevent", "greenlet", "harfbuzz", "hostpython", "igraph", "jpeg", "kivent_core", "kivent_cymunk", "kivy", "leveldb", "libevent", "libpq", "libsodium", "libswift", "libtorrent", "libxml2", "libxslt", "libyaml", "lxml", "m2crypto", "midistream", "msgpack", "mysql_connector", "netifaces", "numpy", "opencv", "openssl", "paramiko", "pil", "plyer", "plyvel", "png", "polygon", "protobuf", "psutil", "psycopg2", "pyasn1", "pycrypto", "pygame", "pyjnius", "pylibpd", "pyopenssl", "pyparsing", "pyqrcode", "python", "pyyaml", "sdl", "setuptools", "six", "sqlalchemy", "sqlite3", "storm", "swift", "thrift", "twisted", "txws", "wokkel", "zeroconf", "zope"], + "allow_custom": true + }, + { + "type": "list", + "title": "Garden requirements", + "section": "app", + "desc": "Application garden requirements.", + "key": "garden_requirements", + "allow_custom": true, + "items": ["androidtabs", "cefpython", "collider", "datetimepicker", "ddd", "desktopvideoplayer", "filebrowser", "filechooserthumbview", "gauge", "geartick", "graph", "knob", "magnet", "mapview", "modernmenu", "moretransitions", "navigationdrawer", "pagecurl", "particlesystem", "pizza", "progressspinner", "qrcode", "recycleview", "roulette", "roulettescroll", "scrolllabel", "segment", "smaa", "stiffscroll", "texturestack", "tickline", "tickmarker", "timeline"] + } +] \ No newline at end of file diff --git a/designer/data/settings/buildozer_spec_buildozer.json b/designer/data/settings/buildozer_spec_buildozer.json new file mode 100644 index 0000000..b15f370 --- /dev/null +++ b/designer/data/settings/buildozer_spec_buildozer.json @@ -0,0 +1,28 @@ +[ + { + "type": "title", + "title": "Logs" + }, + { + "type": "dict", + "title": "Log level", + "section": "buildozer", + "desc": "Buildozer Log Level", + "key": "log_level", + "options": {"0": "Error only", "1": "Info", "2": "debug (with command output)"} + }, + { + "type": "string", + "title": "Path to build artifact storage", + "section": "app", + "desc": "Path to build artifact storage, absolute or relative to spec file", + "key": "build_dir" + }, + { + "type": "string", + "title": "Path to build output", + "section": "app", + "desc": "Path to build output (i.e. .apk, .ipa) storage", + "key": "bin_dir" + } +] diff --git a/designer/data/settings/buildozer_spec_ios.json b/designer/data/settings/buildozer_spec_ios.json new file mode 100644 index 0000000..f465c8c --- /dev/null +++ b/designer/data/settings/buildozer_spec_ios.json @@ -0,0 +1,27 @@ +[ + { + "type": "title", + "title": "Developer Certificates" + }, + { + "type": "string", + "title": "Debug Certificate Name", + "section": "app", + "desc": "Name of the certificate to use for signing the debug version.\nGet a list of available identities: buildozer ios list_identities\nExample: 'iPhone Developer: ()'", + "key": "ios.codesign.debug" + }, + { + "type": "string", + "title": "Release Certificate Name", + "section": "app", + "desc": "Name of the certificate to use for signing the release version.\nGet a list of available identities: buildozer ios list_identities\nExample: 'iPhone Developer: ()'", + "key": "ios.codesign.release" + }, + { + "type": "path", + "title": "Path to a custom kivy-ios folder", + "section": "app", + "desc": "Path to a custom kivy-ios folder", + "key": "ios.kivy_ios_dir" + } +] diff --git a/designer/data/settings/designer_settings.json b/designer/data/settings/designer_settings.json new file mode 100644 index 0000000..a4dd140 --- /dev/null +++ b/designer/data/settings/designer_settings.json @@ -0,0 +1,50 @@ +[ + { + "type": "string", + "title": "Python Shell Path", + "section": "global", + "key": "python_shell_path" + }, + { + "type": "numeric", + "title": "Number of Recent Files", + "section": "global", + "key": "num_recent_files" + }, + { + "type": "numeric", + "title": "Maximum number of lines on Kivy Console", + "section": "global", + "key": "num_max_kivy_console" + }, + { + "type": "numeric", + "title": "Auto Save current Project after every (in mins)", + "section": "global", + "key": "auto_save_time" + }, + { + "type": "bool", + "title": "Save window size on exit", + "desc": "Desktop Only", + "section": "desktop", + "key": "save_window_size" + }, + { + "type": "bool", + "title": "Exit application on Escape key", + "desc": "Desktop Only", + "section": "desktop", + "key": "exit_on_escape" + }, + { + "id": "code_input_theme_options", + "type": "list", + "title": "Code Input Theme", + "desc": "Select the Code Input theme", + "section": "global", + "key": "code_input_theme", + "items": [], + "group": "code_input_theme" + } +] diff --git a/designer/data/settings/hanga_settings.json b/designer/data/settings/hanga_settings.json new file mode 100644 index 0000000..5b3d44a --- /dev/null +++ b/designer/data/settings/hanga_settings.json @@ -0,0 +1,8 @@ +[ + { + "type": "string", + "title": "Hanga API Key", + "section": "hanga", + "key": "hanga_api_key" + } +] \ No newline at end of file diff --git a/designer/data/settings/proj_settings_proj_prop.json b/designer/data/settings/proj_settings_proj_prop.json new file mode 100644 index 0000000..962a96e --- /dev/null +++ b/designer/data/settings/proj_settings_proj_prop.json @@ -0,0 +1,8 @@ +[ + { + "type": "string", + "title": "Project Name", + "section": "proj_name", + "key": "name" + } +] diff --git a/designer/data/settings/proj_settings_shell_env.json b/designer/data/settings/proj_settings_shell_env.json new file mode 100644 index 0000000..37f44d9 --- /dev/null +++ b/designer/data/settings/proj_settings_shell_env.json @@ -0,0 +1,14 @@ +[ + { + "type": "string", + "title": "Arguments", + "section": "arguments", + "key": "arg" + }, + { + "type": "string", + "title": "Environment Variables", + "section": "env variables", + "key": "env" + } +] diff --git a/designer/data/settings/shortcuts.json b/designer/data/settings/shortcuts.json new file mode 100644 index 0000000..7c77a7a --- /dev/null +++ b/designer/data/settings/shortcuts.json @@ -0,0 +1,172 @@ +[ + { + "type": "title", + "title": "File" + }, + { + "type": "shortcut", + "title": "New File", + "section": "shortcuts", + "key": "new_file" + }, + { + "type": "shortcut", + "title": "New Project", + "section": "shortcuts", + "key": "new_project" + }, + { + "type": "shortcut", + "title": "Open Project", + "section": "shortcuts", + "key": "open_project" + }, + { + "type": "shortcut", + "title": "Save", + "section": "shortcuts", + "key": "save" + }, + { + "type": "shortcut", + "title": "Save as", + "section": "shortcuts", + "key": "save_as" + }, + { + "type": "shortcut", + "title": "Close Project", + "section": "shortcuts", + "key": "close_project" + }, + { + "type": "shortcut", + "title": "Recent Projects", + "section": "shortcuts", + "key": "recent" + }, + { + "type": "shortcut", + "title": "Settings", + "section": "shortcuts", + "key": "settings" + }, + { + "type": "shortcut", + "title": "Exit", + "section": "shortcuts", + "key": "exit" + }, + { + "type": "title", + "title": "View" + }, + { + "type": "shortcut", + "title": "Toggle Full Screen", + "section": "shortcuts", + "key": "fullscreen" + }, + { + "type": "title", + "title": "Building" + }, + { + "type": "shortcut", + "title": "Run", + "section": "shortcuts", + "key": "run" + }, + { + "type": "shortcut", + "title": "Stop", + "section": "shortcuts", + "key": "stop" + }, + { + "type": "shortcut", + "title": "Clean", + "section": "shortcuts", + "key": "clean" + }, + { + "type": "shortcut", + "title": "Build", + "section": "shortcuts", + "key": "build" + }, + { + "type": "shortcut", + "title": "Rebuild", + "section": "shortcuts", + "key": "rebuild" + }, + { + "type": "title", + "title": "Tools" + }, + { + "type": "shortcut", + "title": "Buildozer Init", + "section": "shortcuts", + "key": "buildozer_init" + }, + { + "type": "shortcut", + "title": "Export .PNG", + "section": "shortcuts", + "key": "export_png" + }, + { + "type": "shortcut", + "title": "Check PEP8", + "section": "shortcuts", + "key": "check_pep8" + }, + { + "type": "shortcut", + "title": "Create setup.py", + "section": "shortcuts", + "key": "create_setup_py" + }, + { + "type": "shortcut", + "title": "Create .gitignore", + "section": "shortcuts", + "key": "create_gitignore" + }, + { + "type": "title", + "title": "Getting Started" + }, + { + "type": "shortcut", + "title": "Help", + "section": "shortcuts", + "key": "help" + }, + { + "type": "shortcut", + "title": "Kivy Docs", + "section": "shortcuts", + "key": "kivy_docs" + }, + { + "type": "shortcut", + "title": "Kivy Designer Docs", + "section": "shortcuts", + "key": "kd_docs" + }, + { + "type": "shortcut", + "title": "Kivy Designer Repository", + "section": "shortcuts", + "key": "kd_repo" + }, + { + "type": "shortcut", + "title": "About", + "section": "shortcuts", + "key": "About" + } +] diff --git a/designer/designer.kv b/designer/designer.kv new file mode 100644 index 0000000..886b922 --- /dev/null +++ b/designer/designer.kv @@ -0,0 +1,1976 @@ +#:set bgcolor (0.06, 0.07, 0.08) +#:set bordercolor (0.54, 0.59, 0.60) +#:set titlecolor (0.34, 0.39, 0.40) +#:import KivyLexer kivy.extras.highlight.KivyLexer +#:import ContextMenu uix.contextual.ContextMenu + +# +# Helper for keeping a consistency across the whole designer UI +# +# Rules: +# - rows height are 48sp +# - padding is 4sp +# - spacing is 4sp +# - button / label / widget are 40sp height +# - TextInput with a single line is 30sp height +# - modal with just one button(close, cancel, etc). The button must be in the left +# - menu item width is 250 +# - in conditional modals, more positive action in the right +#:set designer_height '40sp' +#:set designer_spacing '4sp' +#:set designer_padding '4sp' +#:set designer_text_input_height '30sp' +#:set designer_action_width 200 +: + size_hint_y: None + height: designer_height + + + size_hint_x: None + width: self.texture_size[0] + sp(32) + +: + text_size: self.width - sp(32), None + +: + size_hint: 1, None + size: self.texture_size[0] + sp(32), designer_height + selected_color: 1, 1, 1, 1 + deselected_color: .8, .8, .8, .5 + +: + size_hint_y: None + height: designer_text_input_height + multiline: False + +# +# Dialogs +# + +: + orientation: 'vertical' + rst: rst + padding: designer_padding + spacing: designer_spacing + RstDocument: + id: rst + DesignerButton: + text: 'Close' + size_hint: None, None + size: '60pt', '30pt' + pos_hint: {'right': 1} + on_release: root.dispatch('on_cancel') + +: + orientation: 'vertical' + user_input: user_input + btn_confirm: btn_confirm + lbl_error: lbl_error + padding: designer_padding + spacing: designer_spacing + Label: + text: root.message + halign: 'left' + text_size: self.size + valign: 'middle' + Label: + id: lbl_error + text: "" + size_hint_x: None + halign: 'left' + color: [1, 0, 0, 1] + UserTextInput: + id: user_input + focus: True + GridLayout: + cols: 2 + size_hint_y: None + spacing: designer_spacing + padding: designer_padding + height: self.minimum_height + DesignerButton: + id: btn_confirm + text: 'Confirm' + disabled: True + on_press: root.dispatch('on_confirm') + DesignerButton: + text: 'Cancel' + on_press: root.dispatch('on_cancel') + +: + size_hint_y: None + height: designer_text_input_height + +: + Image: + source: 'data/logo/kivy-icon-512.png' + pos: root.pos + opacity: 0.2 + BoxLayout: + orientation: 'vertical' + pos: root.pos + background_color: 0, 1, 0 + padding: designer_padding + Label: + id: title + text: 'Kivy Designer' + font_size: '26pt' + halign: 'center' + size_hint_y: None + height: '30pt' + Label: + id: subtitle + markup: True + text: '[i]Innovative User Interfaces, Desktop, and Mobile Development Made Easy.[/i]' + font_size: '10pt' + halign: 'center' + size_hint_y: None + height: '15pt' + Label: + text_size: self.size + padding: 30, 20 + text: ' Kivy Designer is Kivy\'s tool for designing Graphical User Interfaces (GUIs) from Kivy Widgets. \nYou can compose and customize widgets, and test them. It is completely written in Python using Kivy. \nKivy Designer is integrated with Buildozer and Hanga, so you can easily develop and publish your applications to Desktop and Mobile devices.' + font_size: '12pt' + valign: 'top' + DesignerButton: + text: 'Close' + on_release: root.dispatch('on_close') + +: + text_file: text_file + text_folder: text_folder + lbl_error: lbl_error + orientation: 'vertical' + padding: designer_padding + spacing: designer_spacing + Label: + id: lbl_error + size_hint_x: 1 + color: [1, 0, 0, 1] + Label: + text: 'Select the File:' + size_hint_x: None + BoxLayout: + size_hint_y: None + height: '24pt' + TextInput: + id: text_file + multiline: False + Button: + size_hint_x: None + text: 'Open File' + on_press: root.open_file_btn_pressed() + Label: + text: 'Target Folder:' + size_hint_x: None + BoxLayout: + size_hint_y: None + height: '24pt' + TextInput: + id: text_folder + multiline: False + focus: True + Button: + size_hint_x: None + text: 'Open Folder' + on_press: root.open_folder_btn_pressed() + BoxLayout: + padding: designer_padding + spacing: designer_spacing + DesignerButton: + text: 'Add' + on_press: root._perform_add_file() + DesignerButton: + text: 'Cancel' + on_press: root.dispatch('on_cancel') + +: + orientation: 'vertical' + padding: designer_padding + spacing: designer_spacing + Label: + text: root.message + GridLayout: + cols: 2 + size_hint_y: None + height: self.minimum_height + DesignerButton: + text: 'Yes' + on_release: root.dispatch('on_ok') + DesignerButton: + text: 'No' + on_release: root.dispatch('on_cancel') + +: + orientation: 'vertical' + padding: designer_padding + spacing: designer_spacing + Label: + text: root.message + GridLayout: + cols: 3 + size_hint_y: None + height: self.minimum_height + DesignerButton: + text: 'Save' + on_release: root.dispatch('on_save') + DesignerButton: + text: 'Don\'t Save' + on_release: root.dispatch('on_dont_save') + DesignerButton: + text: 'Cancel' + on_release: root.dispatch('on_cancel') + +# +# Profile Settings +# + +: + interface_cls: 'ProfileSettingsInterface' + +<-ProfileSettingsInterface>: + orientation: 'horizontal' + menu: menu + content: content + button_bar: button_bar + ProfileMenuSidebar: + id: menu + GridLayout: + id: button_bar + btn_delete_prof: btn_delete_prof + btn_select_prof: btn_select_prof + cols: 1 + GridLayout: + cols: 2 + size_hint_y: None + height: 50 + spacing: 5 + padding: 5 + DesignerButton: + id: btn_select_prof + text: 'Use this Profile' + size_hint_x: 0.5 + DesignerButton: + id: btn_delete_prof + disabled: True + text: 'Delete this Profile' + size_hint_x: 0.5 + ProfileContentPanel: + id: content + current_uid: menu.selected_uid + +<-ProfileMenuSidebar>: + size_hint_x: None + width: '200dp' + buttons_layout: menu + close_button: button_close + new_button: button_new + ScrollView: + id: e_scroll + y: root.y + 70 + x: root.x + width: root.width + size_hint_y: None + height: root.height - 75 + GridLayout: + size_hint_y: None + height: max(e_scroll.height, self.minimum_height) + cols: 1 + id: menu + # orientation: 'vertical' + padding: 5 + spacing: 5 + canvas.after: + Color: + rgb: .2, .2, .2 + Rectangle: + pos: self.right - 1, self.y + size: 1, self.height + DesignerButton: + text: 'New' + id: button_new + size_hint_x: None + width: root.width - dp(20) + pos: root.x + dp(10), root.y + sp(49) + font_size: '15sp' + DesignerButton: + text: 'Close' + id: button_close + size_hint_x: None + width: root.width - dp(20) + pos: root.x + dp(10), root.y + 5 + font_size: '15sp' + +: + +# +# Tools +# + +: + text_size: self.size + valign: 'middle' + halign: 'left' + size_hint_y: None + height: '30sp' + +: + orientation: 'vertical' + padding: designer_padding + spacing: designer_spacing + BoxLayout: + BoxLayout: + spacing: designer_spacing + padding: designer_padding + size_hint_x: 0.3 + orientation: 'vertical' + SetupPyLabel: + text: 'Package Name: ' + SetupPyLabel: + text: 'Version: ' + SetupPyLabel: + text: 'URL: ' + SetupPyLabel: + text: 'License: ' + SetupPyLabel: + text: 'Author: ' + SetupPyLabel: + text: 'Author Email: ' + SetupPyLabel: + text: 'Description: ' + BoxLayout: + size_hint_x: 0.7 + orientation: 'vertical' + spacing: designer_spacing + padding: designer_padding + DesignerTextInput: + id: package_name + DesignerTextInput: + id: version + DesignerTextInput: + id: url + DesignerTextInput: + id: license + DesignerTextInput: + id: author + DesignerTextInput: + id: author_email + DesignerTextInput: + id: description + GridLayout: + cols: 2 + size_hint_y: None + height: self.minimum_height + spacing: designer_spacing + + DesignerButton: + text: 'Create' + on_press: root.create() + DesignerButton: + text: 'Cancel' + on_press: root.dispatch('on_cancel') + +# +# Menu +# + + + color: (1, 1, 1, 1) if self.state == 'normal' else (0, 0, 0, 1) + font_size: '12dp' + shorten: True + text_size: self.size + padding: '2dp', '2dp' + background_normal: 'atlas://data/images/defaulttheme/action_bar' + background_disabled_normal: 'atlas://data/images/defaulttheme/action_item' + background_down: 'atlas://data/images/defaulttheme/action_item_down' if self.color[3] == 1 else 'atlas://data/images/defaulttheme/action_item' + background_disabled_down: 'atlas://data/images/defaulttheme/action_item_down' + Image: + source: 'atlas://data/images/defaulttheme/tree_closed' + size: (20, 20) if root.show_arrow else (0,0) + center_y: root.center_y + x: (self.parent.right - self.width) if self.parent else 100 + +: + arrow_image: 'atlas://data/images/defaulttheme/tree_closed' + Image: + source: root.arrow_image + size: (20, 20) if root.show_arrow else (0,0) + y: self.parent.y + (self.parent.height/2) - (self.height/2) + x: self.parent.x + (self.parent.width - self.width) + +: + background_normal: 'atlas://data/images/defaulttheme/action_item' + background_down: 'atlas://data/images/defaulttheme/action_item_down' + background_disabled_down: 'atlas://data/images/defaulttheme/action_item_down' + +: + tab_pos: 'bottom_right' + do_default_tab: False + tab_height: '24sp' + +: + background_image: 'atlas://data/images/defaulttheme/action_item' + background_color: 3, 3, 3, 1 + +# +# Menu - Action Items +# + +: + info: info + background_normal: 'atlas://data/images/defaulttheme/action_bar' + size_hint_x: None + width: designer_action_width + canvas.before: + Color: + rgba: [1, 1, 1, .5] if self.disabled else [1, 1, 1, 1] + Rectangle: + pos: self.pos + size: self.size + source: self.background_normal + Label: + pos: root.pos + text_size: (self.width - sp(24), self.size[1]) + valign: 'middle' + size_hint_y: None + height: '40sp' + text: root.text + Label: + id: info + text: root.hint + pos: root.pos + size: root.size + opacity: 0.5 + text_size: self.size + valign: 'bottom' + halign: 'right' + padding: 5, 5 + +: + text_size: (self.width - sp(24), self.size[1]) + valign: 'middle' + size_hint_y: None + height: '40sp' + width: designer_action_width + +: + btn_layout: btn_layout + _label: _label + checkbox: checkbox + background_normal: 'atlas://data/images/defaulttheme/action_bar' + size_hint: None, None + height: sp(49) + width: designer_action_width + canvas.before: + Color: + rgba: 1,1,1,1 + Rectangle: + pos: self.pos + size: self.size + source: self.background_normal + BoxLayout: + id: btn_layout + pos: root.pos + padding: 5, 0, 5, 0 + spacing: 10 + CheckBox: + id: checkbox + size_hint_x: None + width: '20sp' + pos: root.x + 2, root.y + active: root.checkbox_active + group: root.group + allow_no_selection: root.allow_no_selection + on_active: root.dispatch('on_active', *args) + Label: + id: _label + text_size: self.size + valign: 'middle' + text: root.text + Label: + id: info + text: root.desc + pos: root.pos + size: root.size + opacity: 0.3 + text_size: self.size + valign: 'bottom' + halign: 'right' + padding: 5, 5 + +: + size_hint: 1, None + width: designer_action_width + height: sp(48) + background_normal: 'atlas://data/images/defaulttheme/action_bar' + text_size: (self.width - sp(24), self.size[1]) + valign: 'middle' + +: + mode: 'spinner' + size_hint_x: None + dropdown_cls: ContextMenu +# +# Start Page +# + +: + orientation: 'vertical' + size_hint: 1, None + height: 40 + on_touch_down: if self.collide_point(*args[1].pos): root.dispatch('on_press') + canvas.after: + Color: + rgb: .2, .2, .2 + Rectangle: + pos: self.x + 25, self.y + size: self.width - 50, 1 + Label: + text: root.path + text_size: self.size + valign: 'middle' + shorten: True + padding_x: 20 + +: + grid: grid + cols: 1 + padding: '2sp' + size_hint_x: None + bar_width: 10 + scroll_type: ['bars', 'content'] + GridLayout: + id: grid + cols: 1 + size_hint_y: None + height: 1 + +: + color: 0, 0, 1, 1 + background_normal: 'atlas://data/images/defaulttheme/action_item' + background_disabled_normal: 'atlas://data/images/defaulttheme/action_item_disabled' + text_size: self.width, None + +: + btn_open: btn_open + btn_new: btn_new + recent_files_box: recent_files_box + orientation: 'vertical' + padding: 0, 0, 0, 20 + Label: + text: 'Kivy Designer' + font_size: '26pt' + size_hint_y: None + height: '40pt' + Label: + markup: True + text: '[i]Innovative User Interfaces, Desktop, and Mobile Development Made Easy.[/i]' + font_size: '12pt' + halign: 'center' + size_hint_y: None + height: '15pt' + GridLayout: + cols: 2 + size_hint: None, None + height: self.minimum_height + width: self.minimum_width + pos_hint: {'center_x': 0.5} + padding: 0, '15pt', 0, 0 + spacing: '4sp' + DesignerButtonFit: + id: btn_open + text: 'Open Project' + on_release: root.dispatch('on_open_down') + DesignerButtonFit: + id: btn_new + text: 'New Project' + on_release: root.dispatch('on_new_down') + + Label: + text: 'Getting Started' + font_size: '16pt' + bold: True + size_hint_y: None + height: '30pt' + + GridLayout: + kivy_label: kivy_label + cols: 2 + size_hint: None, None + height: self.minimum_height + width: 450 + pos_hint: {'center_x': 0.5} + row_force_default: True + row_default_height: '40sp' + spacing: '4sp' + padding: '16sp', '0sp' + + DesignerLinkLabel: + id: kivy_label + text: ' Kivy' + link: 'http://kivy.org' + + DesignerLinkLabel: + text: ' Kivy Designer Help' + on_release: root.dispatch('on_help') + + DesignerLinkLabel: + id: kivy_label + text: ' Kivy Documentation' + link: 'http://kivy.org/docs' + + DesignerLinkLabel: + text: ' Kivy Designer Documentation' + link: 'http://kivy-designer.readthedocs.org/' + + Label: + text: 'Recent Projects' + font_size: '16pt' + bold: True + size_hint_y: None + height: '30pt' + + RecentFilesBox: + id: recent_files_box + pos_hint: {'center_x': 0.5} + size_hint_x: None + width: 600 + canvas.before: + Color: + rgba: 1, 1, 1, 0.05 + Rectangle: + pos: self.pos + size: self.size + +: + size_hint: None, None + width: '270dp' + height: lbl.texture_size[1] + dp(30) + on_touch_down: self.hide() + BoxLayout: + orientation: 'vertical' + padding: '5dp' + spacing: '2dp' + Label: + id: lbl + text: root.message + text_size: self.width, None + +: + id: scroll + code_input: code_input + line_number: line_number + bar_width: 10 + scroll_type: ['bars', 'content'] + GridLayout: + cols: 2 + size_hint: 1, None + height: max(scroll.height, self.minimum_height) + TextInput: + id: line_number + size_hint: None, 1 + readonly: True + background_color: 1, 1, 1, 0 + foreground_color: 1, 1, 1, 1 + PyCodeInput: + id: code_input + auto_indent: True + size_hint_y: None + height: max(self.minimum_height, scroll.height) + +: + auto_indent: True + lexer: KivyLexer() + canvas.after: + Color: + rgba: .9, .1, .1, (1 if self.have_error else 0) + Line: + points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top] + close: True + width: 2 + +: + grid: grid + cols: 1 + padding: '2sp' + size_hint_x: None + bar_width: 10 + scroll_type: ['bars', 'content'] + GridLayout: + id: grid + cols: 1 + size_hint_y: None + height: 1 + +: + template_preview: template_preview + cancel_button: cancel + select_button: select + template_list: template_list + app_name: app_name + package_name: package_name + package_version: package_version + orientation: 'horizontal' + padding: designer_padding + spacing: designer_spacing + ProjectTemplateBox: + id: template_list + pos_hint: {'center_x': 0.5} + size_hint_x: 0.3 + canvas.before: + Color: + rgba: 1, 1, 1, 0.05 + Rectangle: + pos: self.pos + size: self.size + BoxLayout: + orientation: 'vertical' + size_hint_x: 0.7 + spacing: 20 + BoxLayout: + size_hint_y: 0.1 + orientation: 'horizontal' + Label: + size_hint_x: 0.2 + text_size: self.width, None + padding_x: 10 + halign: 'right' + text: 'App Name: ' + TextInput: + id: app_name + size_hint_x: 0.7 + multiline: False + padding: [15, ( self.height - self.line_height ) / 2] + focus: True + BoxLayout: + size_hint_y: 0.1 + orientation: 'horizontal' + Label: + size_hint_x: 0.2 + text_size: self.width, None + padding_x: 10 + halign: 'right' + text: 'Package Name: ' + TextInput: + id: package_name + size_hint_x: 0.7 + multiline: False + padding: [15, ( self.height - self.line_height ) / 2] + BoxLayout: + size_hint_y: 0.1 + orientation: 'horizontal' + Label: + size_hint_x: 0.2 + text_size: self.width, None + padding_x: 10 + halign: 'right' + text: 'Version: ' + TextInput: + id: package_version + size_hint_x: 0.7 + multiline: False + padding: [15, ( self.height - self.line_height ) / 2] + Image: + id: template_preview + GridLayout: + rows: 1 + size_hint_y: None + height: '48sp' + padding: designer_padding + spacing: designer_spacing + DesignerButton: + id: select + text: 'Create New Project' + DesignerButton: + id: cancel + text: 'Cancel' + +: + select_button: select + cancel_button: cancel + listview: listview + orientation: 'vertical' + padding: designer_padding + + BoxLayout: + padding: 0, 15 + ListView: + id: listview + row_height: 40 + + GridLayout: + cols: 2 + size_hint_y: None + height: self.minimum_height + spacing: designer_spacing + DesignerButton: + id: select + text: 'Open Selected' + DesignerButton: + id: cancel + text: 'Cancel' + +: + selected_color: 1, 1, 1, 0.5 + deselected_color: 0, 0, 0, 0 + text_size: self.size + valign: 'middle' + shorten: True + padding_x: 20 + canvas.after: + Color: + rgba: (47 / 255., 167 / 255., 212 / 255., 1.) if self.is_selected else (.2, .2, .2, 1) + Rectangle: + pos: self.x + 25, self.y + size: self.width - 50, 1 + +# +# Buildozer Spec Editor +# + +: + text_input: text_input + lbl_error: lbl_error + orientation: 'vertical' + spacing: designer_spacing + padding: designer_padding + Label: + text: "Edit the buildozer.spec file" + size_hint_y: None + text_size: self.size + font_size: '11pt' + height: '20pt' + Label: + id: lbl_error + text: 'There is something wrong with your .spec file...' + size_hint_y: None + text_size: self.size + font_size: '11pt' + halign: 'center' + height: '0pt' + ScrollView: + id: spec_scroll + bar_width: 10 + scroll_type: ['bars', 'content'] + CodeInput: + id: text_input + size_hint_y: None + height: max(spec_scroll.height, self.minimum_height) + GridLayout: + cols: 2 + size_hint_y: None + height: self.minimum_height + DesignerButton: + text: 'Apply Modifications' + on_press: root._save_spec() + DesignerButton: + text: 'Cancel Modifications' + on_press: root.load_spec() + +: + interface_cls: 'SpecEditorInterface' + +<-SpecEditorInterface>: + orientation: 'horizontal' + menu: menu + content: content + button_bar: button_bar + SpecMenuSidebar: + id: menu + GridLayout: + id: button_bar + cols: 1 + Label: + text: 'Buildozer Spec Editor' + font_size: '16pt' + halign: 'center' + size_hint_y: None + height: '25pt' + Button: + background_normal: 'atlas://data/images/defaulttheme/action_item' + background_down: 'atlas://data/images/defaulttheme/action_item' + text: 'GUI editor to buildozer.spec.\nRead more at http://buildozer.readthedocs.org' + text_size: self.size + font_size: '11pt' + halign: 'center' + valign: 'top' + size_hint_y: None + height: '30pt' + on_press: root.open_buildozer_docs() + SpecContentPanel: + id: content + current_uid: menu.selected_uid + +<-SpecMenuSidebar>: + size_hint_x: None + width: '200dp' + buttons_layout: menu + close_button: button + GridLayout: + pos: root.pos + cols: 1 + id: menu + # orientation: 'vertical' + padding: 5 + canvas.after: + Color: + rgb: .2, .2, .2 + Rectangle: + pos: self.right - 1, self.y + size: 1, self.height + Button: + id: button + +: + do_scroll_x: False + container: content + GridLayout: + id: content + cols: 1 + size_hint_y: None + height: max(self.minimum_height, root.height) + +: + ActionPrevious: + title: "Screen" + width: 100 + with_previous: True + ActionOverflow: + ActionButton: + text: 'Run' + on_release: root.on_run_press() + DesignerActionGroup: + id: module_screen_device + text: 'Device' + DesignerActionGroup: + id: module_screen_orientation + text: 'Orientation' + DesignerActionProfileCheck: + text: 'Portrait' + group: 'mod_screen_orientation' + allow_no_selection: False + checkbox_active: False + config_key: 'portrait' + on_active: root.on_module_settings(self) + DesignerActionProfileCheck: + text: 'Landscape' + group: 'mod_screen_orientation' + allow_no_selection: False + checkbox_active: False + config_key: 'landscape' + on_active: root.on_module_settings(self) + DesignerActionGroup: + id: module_screen_scale + text: 'Scale' + DesignerActionProfileCheck: + id: module_screen_25 + text: '25%' + group: 'mod_screen_scale' + allow_no_selection: False + checkbox_active: False + config_key: '0.25' + on_active: root.on_module_settings(self) + DesignerActionProfileCheck: + id: module_screen_50 + text: '50%' + group: 'mod_screen_scale' + allow_no_selection: False + checkbox_active: False + config_key: '0.50' + on_active: root.on_module_settings(self) + DesignerActionProfileCheck: + id: module_screen_100 + text: '100%' + group: 'mod_screen_scale' + allow_no_selection: False + checkbox_active: False + config_key: '1.0' + on_active: root.on_module_settings(self) + DesignerActionProfileCheck: + id: module_screen_150 + text: '150%' + group: 'mod_screen_scale' + allow_no_selection: False + checkbox_active: False + config_key: '1.5' + on_active: root.on_module_settings(self) + DesignerActionProfileCheck: + id: module_screen_200 + text: '200%' + group: 'mod_screen_scale' + allow_no_selection: False + checkbox_active: False + config_key: '2.0' + on_active: root.on_module_settings(self) + +: + ActionPrevious: + title: "Modules" + width: 100 + with_previous: True + ActionOverflow: + ActionButton: + text: 'Screen Emulation' + on_release: root.on_screen() + ActionButton: + text: 'Touch Ring' + on_release: root.dispatch('on_module', mod='touchring', data=[]) + ActionButton: + text: 'Monitor' + on_release: root.dispatch('on_module', mod='monitor', data=[]) + ActionButton: + text: 'Inspector' + on_release: root.dispatch('on_module', mod='inspector', data=[]) + ActionButton: + text: 'Web Debugger' + on_release: root.on_webdebugger(self) + +: + ActionPrevious: + title: "Edit" + width: 100 + with_previous: True + ActionOverflow: + ActionButton: + text: 'Undo' + on_press: root.dispatch('on_undo') + ActionButton: + text: 'Redo' + on_press: root.dispatch('on_redo') + ActionButton: + text: 'Cut' + on_press: root.dispatch('on_cut') + ActionButton: + text: 'Copy' + on_press: root.dispatch('on_copy') + ActionButton: + text: 'Paste' + on_press: root.dispatch('on_paste') + ActionButton: + text: 'Select All' + on_touch_up: root.dispatch('on_selectall') + ActionButton: + text: 'Delete' + on_press: root.dispatch('on_delete') + +# +# UI Creator +# + +: + do_scale: False + do_rotation: False + size_hint: None, None + size: 550, 350 + pos: 300, 230 + auto_bring_to_front: False + canvas: + Color: + rgb: 1, 1, 1 + Line: + points: [0, 0, self.width, 0, self.width, self.height, 0, self.height] + width: 2. + close: True + +: + size_hint: None, None + size: 100, 100 + canvas: + Color: + rgb: (0.9, 0.9, 0.9) if self.can_place else (0.9, 0.1, 0.1) + Line: + points: [self.x, self.y, self.center_x - 20, self.y, self.center_x, self.y - 20, self.center_x + 20, self.y, self.right, self.y, self.right, self.top, self.x, self.top] + close: True + width: 1.5 + Color: + rgb: bgcolor + Rectangle: + size: self.size + pos: self.pos + + on_target: app.focus_widget(args[1]) + +: + accordion: accordion + Accordion: + id: accordion + orientation: 'vertical' + pos: root.pos + min_space: '1dp' + size_hint_y: None + height: root.height + +: + gridlayout: gridlayout + size_hint_y: None + height: '22pt' + title: self.title[0].upper() + self.title[1:] + ScrollView: + pos: root.pos + bar_width: 10 + scroll_type: ['bars', 'content'] + GridLayout: + id: gridlayout + cols: 1 + # orientation: 'vertical' + padding: '5sp' + spacing: '3sp' + size_hint_y: None + height: max(self.parent.height, self.minimum_height) + +: + size_hint_y: None + height: '22pt' + font_size: '10pt' + on_press_and_touch: app.create_draggable_element(self, self.text, args[1]) + +: + app: app + navbar: navbar + status_message: status_message + status_info: status_info + canvas: + Color: + rgb: bordercolor + Rectangle: + pos: self.x, self.top - 0.5 + size: self.width, 1 + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + + ScrollView: + id: nav_scroll + size_hint_x: None + width: 0 + do_scroll_y: False + StatusNavbar: + id: navbar + size_hint_x: None + width: max(nav_scroll.width, self.width) + on_children: root._update_content_width() + + StatusMessage: + id: status_message + img: img + size_hint: 0.9, None + height: '20pt' + spacing: 10 + on_message: root._update_content_width() + on_touch_down: if self.collide_point(*args[1].pos): root.dispatch('on_message_press') + Image: + id: img + size_hint: None, None + width: '20pt' + height: '20pt' + opacity: 0 + Label: + size_hint_x: 1 + text: status_message.message + text_size: self.size + halign: 'left' + valign: 'middle' + shorten: True + shorten_from: 'left' + + StatusInfo: + id: status_info + size_hint_x: 0.1 + on_touch_down: if self.collide_point(*args[1].pos): root.dispatch('on_info_press') + Label: + size_hint_x: 1 + text: status_info.message + text_size: self.size + halign: 'center' + valign: 'middle' + shorten: True + shorten_from: 'left' + +: + text: getattr(root.node, '__class__').__name__ + font_size: '10pt' + width: self.texture_size[0] + 20 + size_hint_x: None + on_release: app.focus_widget(root.node) + +: + text: '>' + font_size: '10pt' + width: self.texture_size[0] + 20 + size_hint_x: None + +: + do_scroll_x: False + prop_list: prop_list + + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + + GridLayout: + id: prop_list + cols: 2 + padding: 3 + size_hint_y: None + height: self.minimum_height + row_default_height: '25pt' + +: + font_size: '10pt' + valign: 'middle' + size_hint_x: 0.5 + text_size: self.size + halign: 'left' + valign: 'middle' + shorten: True + canvas.before: + Color: + rgb: .2, .2, .2 + Rectangle: + pos: self.x, self.y - 1 + size: self.width, 1 + +: + propvalue: getattr(self.propwidget, self.propname) + padding: '6pt', '6pt' + + canvas.after: + Color: + rgba: .9, .1, .1, (1 if self.have_error else 0) + Line: + points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top] + close: True + width: 2 + canvas.before: + Color: + rgb: .2, .2, .2 + Rectangle: + pos: self.x, self.y - 1 + size: self.width, 1 + +: + border: 8, 8, 8, 8 + text: str(getattr(self.propwidget, self.propname)) + on_text: self.value_changed(args[1]) + +: + on_active: self.set_value(args[1]) + active: bool(getattr(self.propwidget, self.propname)) + +: + valign: 'middle' + halign: 'left' + shorten: True + shorten_from: 'right' + Image: + source: 'atlas://data/images/defaulttheme/tree_opened' + size_hint: None, None + size: root.height, root.height + pos: root.x + root.width - root.height, root.y + +: + do_scroll_x: False + tree: tree + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: root.pos + size: root.size + + TreeView: + id: tree + height: self.minimum_height + size_hint_y: None + hide_root: True + on_selected_node: args[1] and app.focus_widget(args[1].node) + +: + is_open: True + text: getattr(root.node, '__class__').__name__ + font_size: '10pt' + +: + kv_code_input: code_input + splitter_kv_code_input: splitter_kv + grid_widget_tree: grid_widget_tree + splitter_property: splitter_property + splitter_widget_tree: splitter_widget_tree + propertyviewer: propertyviewer + playground: playground + widgettree: widgettree + error_console: error_console + kivy_console: kivy_console + tab_pannel: tab_pannel + eventviewer: eventviewer + py_console: py_console + + GridLayout: + height: root.height + pos: root.pos + cols: 1 + splitter_widget_tree: splitter_widget_tree + + canvas.before: + StencilPush + Rectangle: + pos: self.pos + size: self.size + StencilUse + canvas.after: + StencilUnUse + Rectangle: + pos: self.pos + size: self.size + StencilPop + + FloatLayout: + size_hint: 1,1 + canvas: + Color: + rgb: .21, .22, .22 + Rectangle: + pos: self.pos + size: self.size + + Playground: + id: playground + canvas.before: + Color: + rgb: 0, 0, 0 + Rectangle: + size: self.size + on_show_edit: root.on_show_edit() + + Splitter: + id: splitter_kv + sizable_from: 'top' + size_hint_y: None + size_hint_x: None + height: 200 + min_size: 150 + max_size: 500 + x: root.x + width: root.width - splitter_widget_tree.width + y: root.y + canvas.before: + Color: + rgb: .21, .22, .22 + + Rectangle: + size: splitter_kv.size + pos: splitter_kv.pos + + + DesignerTabbedPanel: + id: tab_pannel + do_default_tab: False + DesignerTabbedPanelItem: + text: 'KV Lang Area' + BoxLayout: + orientation: 'vertical' + KVLangAreaScroll: + id: scroll + kv_lang_area: code_input + line_number: line_number + GridLayout: + cols: 2 + size_hint: 1, None + height: max(scroll.height, self.minimum_height) + TextInput: + id: line_number + size_hint: None, 1 + readonly: True + background_color: 1, 1, 1, 0 + foreground_color: 1, 1, 1, 1 + KVLangArea: + id: code_input + size_hint_y: None + height: max(scroll.height, self.minimum_height) + on_show_edit: root.on_show_edit() + DesignerButton: + size_hint_x: 0.2 + text: 'Reload Widgets' + pos_hint: {'x': 0.8} + on_release: root.reload_btn_pressed() + + DesignerTabbedPanelItem: + text: 'Kivy Console' + KivyConsole: + id: kivy_console + + DesignerTabbedPanelItem: + text: 'Python Shell' + PythonConsole: + id: py_console + + DesignerTabbedPanelItem: + text: 'Error Console' + ScrollView: + id: e_scroll + CodeInput: + id: error_console + size_hint_y: None + height: max(e_scroll.height, self.minimum_height) + text: '' + + Splitter: + id: splitter_widget_tree + size_hint_x: None + min_size: 170 + width: 250 + pos_hint: {'y': 0, 'right': 1} + + BoxLayout: + orientation: 'vertical' + + GridLayout: + id: grid_playground_settings + cols: 1 + spacing: .5 + height: self.minimum_height + size_hint_y: None + + canvas: + Color: + rgb: titlecolor + Rectangle: + pos: self.pos + size: self.size + + Label: + text: 'Playground Settings' + font_size: '10pt' + height: '20pt' + size_hint_y: None + + BoxLayout: + size_hint_y: None + height: sp(48) + + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + + Label: + text: 'Size:' + size_hint_x: None + width: sp(52) + + PlaygroundSizeSelector: + playground: playground + + BoxLayout: + size_hint_y: None + height: sp(48) + + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + + Label: + text: 'Zoom: %d%%' % (zoom_slider.value * 100) + size_hint_x: None + width: sp(96) + + Slider: + id: zoom_slider + min: 0.25 + max: 1.5 + step: 0.05 + value: playground.scale + on_value: playground.scale = args[1] + + GridLayout: + id: grid_playground_widget + cols: 1 + spacing: .5 + height: self.minimum_height + size_hint_y: None + + canvas: + Color: + rgb: titlecolor + Rectangle: + pos: self.pos + size: self.size + + Label: + text: 'Playground Widget' + font_size: '10pt' + height: '20pt' + size_hint_y: None + + BoxLayout: + size_hint_y: None + height: sp(48) + + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + + Button: + text: playground.root_name + on_press: playground.on_widget_select_pressed() + + GridLayout: + cols: 1 + spacing: .5 + + canvas: + Color: + rgb: titlecolor + Rectangle: + pos: self.pos + size: self.size + + GridLayout: + id: grid_widget_tree + cols: 1 + spacing: .5 + Label: + text: 'Widget Navigator' + font_size: '10pt' + height: '20pt' + size_hint_y: None + + WidgetsTree: + id: widgettree + playground: playground + + Splitter: + id: splitter_property + sizable_from: 'top' + size_hint_y: None + height: 300 + max_size: 500 + canvas.before: + Color: + rgb: bgcolor + Rectangle: + pos: self.pos + size: self.size + DesignerTabbedPanel: + do_default_tab: False + DesignerTabbedPanelItem: + text: 'Properties' + PropertyViewer: + id: propertyviewer + + DesignerTabbedPanelItem: + text: 'Events' + EventViewer: + id: eventviewer + + +: + markup: True + +: + accordion: accordion + + BoxLayout: + orientation: 'vertical' + + BoxLayout: + size_hint_y: None + height: sp(48) + Label: + text: 'Playground Size' + bold: True + + Label: + text: 'Portrait' + halign: 'right' + valign: 'middle' + text_size: self.size + Switch: + size_hint_x: None + width: sp(128) + active: root.selected_orientation == 'portrait' + on_active: root.selected_orientation = ('portrait' if self.active else 'landscape') + + Accordion: + id: accordion + min_space: '1dp' + +: + txt_query: txt_query + size_hint_y: None + height: designer_height + canvas.before: + Color: + rgb: bgcolor + Rectangle: + size: self.size + pos: self.pos + CheckBox: + group: 'find_mode' + size_hint_x: None + width: 20 + active: True + Label + text: 'Text' + size_hint_x: None + padding_x: 10 + size: self.texture_size + CheckBox: + group: 'find_mode' + size_hint_x: None + width: 20 + on_active: root.use_regex = args[1] + Label + text: 'Regex' + size_hint_x: None + padding_x: 10 + size: self.texture_size + CheckBox: + group: 'find_mode' + size_hint_x: None + width: 20 + on_active: root.case_sensitive = args[1] + Label: + text: 'Case sensitive' + size_hint_x: None + padding_x: 10 + size: self.texture_size + TextInput: + id: txt_query + text: '' + on_text: root.query = args[1] + multiline: False + on_text_validate: root.dispatch('on_next') + Button: + text: 'Find' + on_release: root.dispatch('on_next') + size_hint_x: None + width: 100 + Button: + text: 'Find Prev' + on_release: root.dispatch('on_prev') + size_hint_x: None + width: 100 + Image: + source: 'atlas://data/images/defaulttheme/close' + size_hint: None, None + size: designer_height, designer_height + on_touch_down: if self.collide_point(*args[1].pos): root.dispatch('on_close') + +: + ui_creator: ui_creator + tree_view: tree_view + tab_pannel: tab_panel + toolbox: toolbox + splitter_tree: splitter_tree + tree_toolbox_tab_panel: tree_toolbox_tab_panel + find_tool: find_tool + + DesignerTabbedPanel: + id: tab_panel + size_hint: None, None + height: root.height + y: root.y + x: splitter_tree.width + width: root.width - splitter_tree.width + do_default_tab: False + tab_width: None + on_current_tab: root.on_current_tab(self) + + DesignerTabbedPanelItem: + text: 'UI Creator' + UICreator: + id: ui_creator + + Splitter: + id: splitter_tree + pos: root.pos + size_hint_x: None + sizable_from: 'right' + min_size: 220 + width: 220 + DesignerTabbedPanel: + id: tree_toolbox_tab_panel + do_default_tab: False + DesignerTabbedPanelItem: + text: 'Project Tree' + ScrollView: + id: tree_scroll + bar_width: 10 + scroll_type: ['bars', 'content'] + TreeView: + id: tree_view + size_hint: 1, None + height: max(tree_scroll.height, self.minimum_height) + DesignerTabbedPanelItem: + text: 'Toolbox' + Toolbox: + id: toolbox + + CodeInputFind: + id: find_tool + y: root.y if root.in_find else -100 + x: splitter_tree.width + size_hint_x: None + width: root.width - splitter_tree.width + +: + color: 0,0,0,0 + disabled_color: self.color + # variable tab_width + text: root.title + size_hint_x: None + BoxLayout: + pos: root.pos + size_hint: None, None + size: root.size + padding: 3 + Label: + id: lbl + text: root.text + shorten: True + shorten_from: 'right' + text_size: self.size + valign: 'middle' + halign: 'center' + markup: True + BoxLayout: + size_hint: None, 1 + orientation: 'vertical' + width: 22 + Image: + source: 'atlas://data/images/defaulttheme/close' + on_touch_down: + if self.collide_point(*args[1].pos) : root.dispatch('on_close') +: + statusbar: statusbar + actionbar: actionbar + start_page: start_page.__self__ + select_profile_cont_menu: select_profile_cont_menu + designer_git: git_tools + + ActionBar: + id: actionbar + pos_hint: {'top': 1} + on_height: root.on_actionbar_height() + DesignerActionView: + ActionPrevious: + title: 'Kivy Designer' + with_previous: False + ActionOverflow: + DesignerActionGroup: + id: actn_menu_file + text: 'File' + DesignerActionButton: + id: actn_btn_new_file + text: 'New File' + disabled: True + on_press: root.action_btn_new_file_pressed() + DesignerActionButton: + id: actn_btn_new_project + text: 'New Project' + on_press: root.action_btn_new_project_pressed() + DesignerActionButton: + id: actn_btn_open_project + text: 'Open Project' + on_press: root.action_btn_open_pressed() + DesignerActionButton: + id: actn_btn_save + disabled: True + text: 'Save' + on_press: root.action_btn_save_pressed() + DesignerActionButton: + id: actn_btn_save_as + disabled: True + text: 'Save As' + on_press: root.action_btn_save_as_pressed() + DesignerActionButton: + id: actn_btn_close_proj + text: 'Close Project' + disabled: True + on_press: root.action_btn_close_proj_pressed() + DesignerActionButton: + id: actn_btn_recent + text: 'Recent Projects' + on_press: root.action_btn_recent_files_pressed() + DesignerActionButton: + id: actn_btn_settings + text: 'Settings' + on_press: root.action_btn_settings_pressed() + DesignerActionButton: + id: actn_btn_quit + text: 'Quit' + on_press: root.action_btn_quit_pressed() + + DesignerActionGroup: + id: actn_menu_view + text: 'View' + disabled: True + ActionCheckButton: + id: actn_chk_proj_tree + text: 'Project Tree and Toolbox' + on_active: root.action_chk_btn_toolbox_active(self) + + ActionCheckButton: + id: actn_chk_prop_event + text: 'Properties and Events' + on_active: root.action_chk_btn_property_viewer_active(self) + + ActionCheckButton: + id: actn_chk_widget_tree + text: 'Widget Tree' + on_active: root.action_chk_btn_widget_tree_active(self) + + ActionCheckButton: + id: actn_chk_status_bar + text: 'StatusBar' + on_active: root.action_chk_btn_status_bar_active(self) + + ActionCheckButton: + id: actn_chk_kv_lang_area + text: 'KV Lang Area' + on_active: root.action_chk_btn_kv_area_active(self) + ActionCheckButton: + id: actn_chk_fullscreen + checkbox_active: False + text: 'Fullscreen' + on_active: root.toggle_fullscreen(self) + + DesignerActionGroup: + id: actn_menu_proj + text: 'Project' + disabled: True + DesignerActionButton: + id: actn_btn_add_file + text: 'Add File' + on_press: root.action_btn_add_file_pressed() + + DesignerActionButton: + id: actn_btn_proj_pref + text: 'Project Settings' + on_press: root.action_btn_project_settings_pressed() + + DesignerActionGroup: + id: actn_menu_run + text: 'Run' + disabled: True + DesignerActionButton: + id: actn_btn_run_proj + text: 'Run' + on_press: root.action_btn_run_project_pressed() + DesignerActionButton: + id: actn_btn_run_module + text: 'Run with module...' + on_press: root.action_btn_run_module_pressed() + DesignerActionButton: + id: actn_btn_stop_proj + disabled: True + text: 'Stop' + on_press: root.action_btn_stop_project_pressed() + DesignerActionButton: + id: actn_btn_clean_proj + text: 'Clean' + on_press: root.action_btn_clean_project_pressed() + DesignerActionButton: + id: actn_btn_build_proj + text: 'Build' + on_press: root.action_btn_build_project_pressed() + DesignerActionButton: + id: actn_btn_rebuild_proj + text: 'Rebuild' + on_press: root.action_btn_rebuild_project_pressed() + DesignerActionSubMenu: + id: select_profile_cont_menu + text: 'Select Profile' + on_press: root.action_btn_select_prof_project_pressed() + show_arrow: True + DesignerActionButton: + id: actn_btn_edit_prof_proj + text: 'Edit Profiles...' + on_press: root.action_btn_edit_prof_project_pressed() + + DesignerActionGroup: + id: actn_menu_tools + text: 'Tools' + disabled: True + DesignerActionButton: + id: actn_btn_buildozer_init + text: 'Buildozer Init' + on_press: root.designer_tools.buildozer_init() + DesignerActionButton: + id: actn_btn_export_png + text: 'Export .png' + on_press: root.designer_tools.export_png() + DesignerActionButton: + id: actn_btn_check_pep8 + text: 'Check PEP 8' + on_press: root.designer_tools.check_pep8() + DesignerActionButton: + id: actn_btn_create_setup_py + text: 'Create setup.py' + on_press: root.designer_tools.create_setup_py() + DesignerActionButton: + id: actn_btn_create_gitignore + text: 'Create .gitignore' + on_press: root.designer_tools.create_gitignore() + DesignerGit: + id: git_tools + text: 'Git' + show_arrow: True + + DesignerActionGroup: + id: actn_menu_help + text: 'Help' + DesignerActionButton: + id: actn_btn_help + text: 'Help' + on_press: root.show_help() + + DesignerActionButton: + id: actn_btn_wiki + text: 'Kivy Docs' + on_press: root.open_docs() + + DesignerActionButton: + id: actn_btn_doc + text: 'Kivy Designer Docs' + on_press: root.open_kd_docs() + + DesignerActionButton: + id: actn_btn_page + text: 'Kivy Designer repo' + on_press: root.open_repo() + + DesignerActionButton: + id: actn_btn_about + text: 'About ...' + on_press: root.action_btn_about_pressed() + + DesignerStartPage: + id: start_page + size_hint_y: None + height: root.height - actionbar.height - statusbar.height + top: root.height - actionbar.height + on_open_down: root.action_btn_open_pressed() + on_new_down: root.action_btn_new_project_pressed() + on_help: root.show_help() + + StatusBar: + id: statusbar + size_hint_y: None + on_height: root.on_statusbar_height() + height: '20pt' + pos_hint: {'y': 0} diff --git a/designer/files.py b/designer/files.py new file mode 100644 index 0000000..c50c706 --- /dev/null +++ b/designer/files.py @@ -0,0 +1 @@ +init_file = __file__ \ No newline at end of file diff --git a/designer/help.rst b/designer/help.rst new file mode 100644 index 0000000..c802bce --- /dev/null +++ b/designer/help.rst @@ -0,0 +1,269 @@ +Kivy Designer Help +================== + +Kivy Designer is Kivy's tool for designing Graphical User Interfaces (GUIs) from Kivy Widgets. You can compose and customize widgets, and test them. It is completely written in Python using Kivy. Kivy Designer is integrated with Buildozer and Hanga, so you can easily develop and publish your applications to Desktop and Mobile devices. + +Kivy Designer Documentation +--------------------------- + +This help is a simple offline overview of Kivy Designer. Read the full docs at http://kivy-designer.readthedocs.org/ + +Project +------- +A Project is what you would be working on. Each project contains atleast one 'kv' file and one 'py' file. For your project to be compatible completely with Kivy Designer, it will be good to start creating it from Kivy Designer. + +Kivy Designer Start Page +------------------------ +When you will start Kivy Designer, then first screen you will see is "Start Page" of Kivy Designer. It contains four buttons "Open Project", "New Project", "Kivy" and "Kivy Designer" and it shows recently open projects, click on any of them to open. + +Create New Project +------------------ +To create a new project: + * Click File->New Project. + * Select any of the desired templates from the popup. + * Click "New" to create a new project. + +Open a Project +-------------- +To open a project: + * Click on File->Open Project + * Navigate to the path of project. + * If you wish then select any 'kv' file. + * Click "Open" to open project. + +Open Recent Project +--------------------- +* Open a Recent Project using Start Page +* In Start Page, under "Recent Projects" you will see recent projects. Click on any of them. + +* Go to File->Recent Projects. +* Click on the path, you want to open. + +Kivy Designer Interface +----------------------- + +This is a list and overview of some Kivy Designer's components + +After opening a project, you will see following: + 1. **Project Tree** on the left side, shows files and folders inside the project's directory. + 2. **Toolbox** contains widgets which could be drag-drop to the required positions. + 3. **UI Creator** is place where you will be designing your project. + 4. **Widget Tree** shows the Widget hierarchy of the project. + 5. **Property Viewer** shows properties, their values and allows changing the values. + 6. **Events** shows the available events and their event handler. You can change/set an event handler and add an event. + 7. **KV Lang Area** shows what your kv file would be consisting. + 8. **Kivy Console** is a console just like xterm, GNOME Terminal. You can enter commands and execute them. + 9. **Python Shell** is an interactive Python Shell. + 10. **Error Console** shows errors which may occur in the user code, while opening a project or creating custom widget. + 11. **Playground Settings** you can change the playground screen size, orientation and zoom to help the UI development + 12. **Status Bar** The status bar helps you displaying the selected widget hierarchy and messages. + +UI Creator +---------- + +You'll probably spend a big part of your time designing the app interface; so the UI creator is the right place for you :) +When designing the UI, you can get Widget from **Widget Tree** or you insert the KV Lang code in **KV Lang Area** +If you want to change the size or orientaiton of the emulated interface, you can set it on **Playground Settings** + +Adding Widget +~~~~~~~~~~~~~~ +* Click on "Toolbox" in the left of the screen. +* A number of Widgets will be shown. +* Press the Widget which you want to add. +* Drag to the parent on which you want to add the widget. While dragging you can also see how it is going to look when added to that parent. +* Drop the widget when desired. + +Similarly WidgetTree also supports drag-drop of widgets. Select any widget from WidgetTree or drag any widget from WidgetTree and drop it on the parent widget. + +Changing Property Value of a Widget +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Select a Widget by clicking on it. +* On the bottom-right side you can see Widgets with its properties. To change the value, just enter the value. + +Setting Event Handler +~~~~~~~~~~~~~~~~~~~~~ +* Select a Widget. +* On the bottom-right side, select "Events". +* You can current available events of widget. +* To enter Event Handler, set the value of event handler in its corresponding text. +* While entering an id and dot '.', you will see a dropdown with available functions. +* If selected widget is a custom widget, then you can create a new event by entering event name in the last text and saving the project. + +Open Project's Python Files +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Click on "Project Tree" on left side. +* Click on the file name you want to open. +* File will be opened in a new tab. + +Edit Options +~~~~~~~~~~~~ +Edit Options are available for Widgets and also for text in KVLangArea and Python Files. Select a Widget or click on KVLangArea or to see its available options. + * Cut, delete the current widget/selected text from its position. + * Copy, copies the current widget/selected text. + * Paste, add the widget/text to parent widget/KV Lang Area or Python File + * Select All, selects all widgets/text. + * Delete, delete current selected widget/text. + * Find, available on Text files. You can search using a string or regex. + +Moreover you can also access these options using keyboard shortcuts. + +Saving Project +~~~~~~~~~~~~~~ +After creating a project you can save project by File->Save or File->Save As + +Add Files to Project +~~~~~~~~~~~~~~~~~~~~ +You can also add files to a project. + * Go to Project->Add File. + * Click on "Open File" to open the file you want to add. + * Click on "Open Folder" to open the folder where you want to add file inside project. + * Check "Always use this folder" if you want to always use this folder for this file type. + * Click "Add" to Add File. + +Add Custom Widgets to Project +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can also add custom widgets to project. + * Go to Project->Add Custom Widget. + * Select custom widget's python file. + * Click "Open" + +Building +-------- + +To build, and run your project, you'll need to configure the Kivy Designer Builder. The Builder will help you to target your application to the desired platforms. +You can access Builder settings at ``Run -> Edit Profiles...`` + +.. _Builder: + +Builders +~~~~~~~~ +You can use the following tools to build your project: + + * **Desktop** - This is the default Python interpreter available in your system. (Desktop only) + * **Buildozer** - Use `Buildozer `_ to target mobile devices. (Android and iOS) + * **Hanga** - Use Hanga to target mobile devices. (Android) + +Build Profiles +~~~~~~~~~~~~~~ +You can select and configure your Builder using Build Profiles. + +Kivy Designer already provides 3 defaults profiles: + + * Desktop + * Android - Buildozer + * iOS - Buildozer + +You can edit/delete these profiles and create new ones. To use a profile, click in the button ``Use this profile`` or select the profile from the menu ``Run -> Select Profile`` + +Editing a profile +~~~~~~~~~~~~~~~~~ + +Before edit a build profile, it's a good idea to know what you are editing :) Take a look on what each field represents + + * **Name** - Name of the profile. This name will be visible in the profiles list. + * **Builder** - Select which Builder_ do you want to use. + * **Target** - Select the target platform. IMPORTANT: Just make sure that the selected Builder_ supports the desired platform. + * **Mode** - Used by Buildozer and Hanga only. This sets the build mode, Debug or Release. + * **Install On Device** - If you are targeting a mobile device, this tool allows you to auto install the application every build. + * **Debug** - If activated and targeting Android, will show the logcat output on Kivy Console. + * **Verbose** - If activated, will run your Builder_ on verbose mode. + +Run +~~~ + +The ``Run`` menu provides you some options. Take a look in the table bellow to see how it works with each Builder_ + ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| | **Desktop** | **Buildozer** | **Hanga** | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Run** | Run *main.py* with Python interpreter | Build, install and run on target device | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Stop** | Stop the Python interpreter | Nothing | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Clean** | Removes all .pyc and __pycache__ | Clean the Buildozer build | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Build** | Generate .pyc | Build the project. If ``Install On Device``| Not yet implemented | +| | | is set, install it on device. | | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +|**Rebuild**| Run ``Clean`` and the ``Build`` | Run ``Clean`` and the ``Build`` | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ + +Modules +------- + +While developing your application, Kivy provides some `extra modules `_ to help you. + +Kivy Designer has an interface to some of `these modules `_ . + +To use Kivy Modules you must target Desktop, select the desired module at ``Run -> Run with module...``. + +Screen Emulation +~~~~~~~~~~~~~~~~ + +It's really important to see your application running in different screen sizes, dimensions and orientations. + +Kivy Designer provides a simple interface to the `Screen Module `_. + +This module provides some settings. You can change the ``Device``, ``Orientation`` and ``Scale``. And the just press ``Run`` to run your application with Screen Module. + +Touchring +~~~~~~~~~ + +The `Touchring Module `_ shows rings around every touch on the surface / screen. + +You can use this module to check that you don’t have any calibration issues with touches. + +Monitor +~~~~~~~ + +The `Monitor Module `_ is a toolbar that shows the activity of your current application. + +Inspector +~~~~~~~~~ + +.. note:: + `This module is highly experimental, use it with care.` + +The `Inspector Module `_ is a tool for finding a widget in the widget tree by clicking or tapping on it. + +After running your app, you can access the Inspector with: + + - "Ctrl + e": activate / deactivate the inspector view + - "Escape": cancel widget lookup first, then hide the inspector view + +Available inspector interactions: + + - tap once on a widget to select it without leaving inspect mode + - double tap on a widget to select and leave inspect mode (then you can manipulate the widget again) + +.. warning:: + Some properties can be edited live. However, due to the delayed usage of some properties, it might crash if you don’t handle all the cases. + +Web Debugger +~~~~~~~~~~~~ + +The `Web Debugger Module `_ starts a webserver and run in the background. You can see how your application evolves during runtime, examine the internal cache etc. + +To access the debugger, Kivy Designer will open http://localhost:5000/ + +Project Preferences +------------------- +To access Project Preferences, go to Project->Project Preferences. Here you can access environment variables and arguments which must be passed to project to run it. + +Kivy Designer Settings +---------------------- +Kivy Designer Settings can be accessed by File->Settings. Here you can + * Modify Python Shell Path. + * Modify Buildozer Path + * Enable/Disable the option to auto create the buildozer.spec + * Modify whether to load changes in KV Lang Area automatically or not. + * Number of Recent Files, Kivy Designer should keep track of. + * Auto Save time out, after how many mins project should be saved automatically. + +Auto Save +--------- +Kivy Designer supports Auto Save. Your current project will be automatically saved after Auto Save Time out which is specified in Kivy Designer Settings. In case of any failure, you can access your last saved project from ".designer" folder present in the project's directory. + +Detect Runtime Changes +---------------------- +If a project has been changed outside Kivy Designer, then Kivy Designer will detect those changes. Kivy Designer will ask whether to reload the project or not. \ No newline at end of file diff --git a/designer/tools/__init__.py b/designer/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/tools/bug_reporter.py b/designer/tools/bug_reporter.py new file mode 100644 index 0000000..37b6950 --- /dev/null +++ b/designer/tools/bug_reporter.py @@ -0,0 +1,202 @@ +import os +import sys +import webbrowser + +import six.moves.urllib +from kivy.app import App +from kivy.core.clipboard import Clipboard +from kivy.lang import Builder +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.popup import Popup + + +Builder.load_string(''' +: + txt_traceback: txt_traceback + Image: + source: 'data/logo/kivy-icon-256.png' + opacity: 0.2 + BoxLayout: + orientation: 'vertical' + padding: 10 + Label: + id: title + text: 'Sorry, Kivy Designer has experienced an internal error :(' + font_size: '16pt' + halign: 'center' + size_hint_y: None + height: '30pt' + Label: + id: subtitle + text: 'You can report this bug using the button bellow, ' \ + 'helping us to fix it.' + text_size: self.size + font_size: '11pt' + halign: 'center' + valign: 'top' + size_hint_y: None + height: '30pt' + ScrollView: + id: e_scroll + bar_width: 10 + scroll_y: 0 + TextInput: + id: txt_traceback + size_hint_y: None + height: max(e_scroll.height, self.minimum_height) + background_color: 1, 1, 1, 0.05 + text: '' + foreground_color: 1, 1, 1, 1 + readonly: True + BoxLayout: + size_hint: 0.6, None + padding: 10, 10 + height: 70 + pos_hint: {'x':0.2} + spacing: 5 + Button: + text: 'Copy to clipboard' + on_press: root.on_clipboard() + Button: + text: 'Report Bug' + on_press: root.on_report() + Button: + text: 'Close' + on_press: root.on_close() + +: + size_hint: .5, .5 + auto_dismiss: False + title: 'Warning' + BoxLayout: + orientation: 'vertical' + Label: + text_size: self.size + text: root.text + padding: '4sp', '4sp' + valign: 'middle' + BoxLayout: + Button: + size_hint_y: None + height: '40sp' + on_release: root.dispatch('on_release') + text: 'Report' + Button: + size_hint_y: None + height: '40sp' + on_release: root.dismiss() + text: 'Close' +''') + + +class ReportWarning(Popup): + text = StringProperty('') + '''Warning Message + ''' + + __events__ = ('on_release',) + + def on_release(self, *args): + pass + + +class BugReporter(FloatLayout): + txt_traceback = ObjectProperty(None) + '''TextView to show the traceback message + ''' + + def __init__(self, **kw): + super(BugReporter, self).__init__(**kw) + self.warning = None + + def on_clipboard(self, *args): + '''Event handler to "Copy to Clipboard" button + ''' + Clipboard.copy(self.txt_traceback.text) + + def on_report(self, *args): + '''Event handler to "Report Bug" button + ''' + warning = ReportWarning() + warning.text = ('Warning. Some web browsers doesn\'t post the full' + ' traceback error. \n\nPlease, check if the last line' + ' of your report is "End of Traceback". \n\n' + 'If not, use the "Copy to clipboard" button the get' + 'the full report and post it manually."') + warning.bind(on_release=self._do_report) + warning.open() + self.warning = warning + + def _do_report(self, *args): + txt = six.moves.urllib.parse.quote( + self.txt_traceback.text.encode('utf-8')) + url = 'https://github.com/kivy/kivy-designer/issues/new?body=' + txt + webbrowser.open(url) + + def on_close(self, *args): + '''Event handler to "Close" button + ''' + App.get_running_app().stop() + + +class BugReporterApp(App): + title = "Kivy Designer - Bug reporter" + traceback = StringProperty('') + + def __init__(self, **kw): + # self.traceback = traceback + super(BugReporterApp, self).__init__(**kw) + + def build(self): + rep = BugReporter() + template = ''' +## Environment Info + +%s + +## Traceback + +``` +%s +``` + +End of Traceback +''' + env_info = 'Pip is not installed' + try: + from pip.req import parse_requirements + from pip.download import PipSession + import platform + + requirements = parse_requirements(os.path.join( + os.path.dirname(os.path.realpath(__file__)), + '..', + '..', + 'requirements.txt'), + session=PipSession() + ) + env_info = '' + for req in requirements: + version = req.installed_version + if version is None: + version = 'Not installed' + env_info += req.name + ': ' + version + '\n' + env_info += '\nPlatform: ' + platform.platform() + env_info += '\nPython: ' + platform.python_version() + + except ImportError: + pass + if isinstance(self.traceback, bytes): + encoding = sys.getfilesystemencoding() + if not encoding: + encoding = sys.stdin.encoding + if encoding: + self.traceback = self.traceback.decode(encoding) + rep.txt_traceback.text = template % (env_info, self.traceback) + + return rep + + +if __name__ == '__main__': + BugReporterApp(traceback='Bug example').run() diff --git a/designer/tools/git_integration.py b/designer/tools/git_integration.py new file mode 100644 index 0000000..fc87977 --- /dev/null +++ b/designer/tools/git_integration.py @@ -0,0 +1,533 @@ +import os +import subprocess +import threading +from io import open + +from components.designer_content import DesignerCloseableTab +from uix.action_items import ( + DesignerActionSubMenu, + DesignerSubActionButton, +) +from uix.input_dialog import InputDialog +from uix.py_code_input import PyScrollView +from uix.settings import SettingListContent +from utils.utils import ( + FakeSettingList, + get_current_project, + get_designer, + get_kd_dir, + ignore_proj_watcher, + show_alert, + show_message, +) +# from git import GitCommandError, RemoteProgress, Repo +# from git.exc import InvalidGitRepositoryError +from kivy.core.window import Window +from kivy.properties import ( + BooleanProperty, + Clock, + ObjectProperty, + StringProperty, +) +from kivy.uix.label import Label +from kivy.uix.popup import Popup +from pygments.lexers.diff import DiffLexer + + +class RemoteProgress(Label): + pass + +class GitRemoteProgress(RemoteProgress): + + label = None + text = '' + + def __init__(self): + super(GitRemoteProgress, self).__init__() + self.label = Label(text='') + self.label.padding = [10, 10] + + def update(self, op_code, cur_count, max_count=None, message=''): + self.text = 'Progress: %.2f (%d of %d)\n%s' % ( + cur_count / (max_count or 100.0), + cur_count, + (max_count or 100), + message.replace(',', '').strip() + ) + + def update_text(self, *args): + '''Update the label text + ''' + if self.text: + self.label.text = self.text + + def start(self): + '''Start the label updating in a separated thread + ''' + Clock.schedule_interval(self.update_text, 0.2) + + def stop(self): + '''Start the label updating in a separated thread + ''' + Clock.unschedule(self.update_text) + + +class DesignerGit(DesignerActionSubMenu): + + is_repo = BooleanProperty(False) + '''Indicates if it's representing a valid git repository + :data:`is_repo` is a :class:`~kivy.properties.BooleanProperty`, defaults + to False. + ''' + + path = StringProperty('') + '''Project path + :data:`path` is a :class:`~kivy.properties.StringProperty`, + defaults to ''. + ''' + + repo = ObjectProperty(None) + '''Instance of Git repository. + :data:`repo` is a :class:`~kivy.properties.ObjectProperty`, defaults + to None. + ''' + + diff_code_input = ObjectProperty(None) + '''Instance of PyCodeInput with Git diff + :data:`diff_code_input` is a :class:`~kivy.properties.ObjectProperty`, + defaults to None. + ''' + + __events__ = ('on_branch', ) + + def __init__(self, **kwargs): + super(DesignerGit, self).__init__(**kwargs) + self._update_menu() + + def load_repo(self, path): + '''Load a git/non-git repo from path + :param path: project path + ''' + self.path = path + # try: + # self.repo = Repo(path) + # self.is_repo = True + # branch_name = self.repo.active_branch.name + # self.dispatch('on_branch', branch_name) + + # if os.name == 'posix': + # script = os.path.join(get_kd_dir(), + # 'tools', 'ssh-agent', 'ssh.sh') + # self.repo.git.update_environment(GIT_SSH_COMMAND=script) + # except InvalidGitRepositoryError: + # self.is_repo = False + self.is_repo = False + self._update_menu() + + def _update_menu(self, *args): + '''Update the Git ActionSubMenu content. + If a valid repo is open, git tools will be available. + Is not a git repo, git init is available. + ''' + self.remove_children() + d = get_designer() + loader = None + if d: + loader = get_current_project().path + + if loader: + self.disabled = False + if self.is_repo: + btn_commit = DesignerSubActionButton(text='Commit') + btn_commit.bind(on_press=self.do_commit) + + btn_add = DesignerSubActionButton(text='Add...') + btn_add.bind(on_press=self.do_add) + + btn_branches = DesignerSubActionButton(text='Branches...') + btn_branches.bind(on_press=self.do_branches) + + btn_diff = DesignerSubActionButton(text='Diff') + btn_diff.bind(on_press=self.do_diff) + + btn_push = DesignerSubActionButton(text='Push') + btn_push.bind(on_press=self.do_push) + + btn_pull = DesignerSubActionButton(text='Pull') + btn_pull.bind(on_press=self.do_pull) + + self.add_widget(btn_commit) + self.add_widget(btn_add) + self.add_widget(btn_branches) + self.add_widget(btn_diff) + self.add_widget(btn_push) + self.add_widget(btn_pull) + else: + btn_init = DesignerSubActionButton(text='Init repo') + btn_init.bind(on_press=self.do_init) + self.add_widget(btn_init) + self._add_widget() + else: + self.disabled = True + + def validate_remote(self): + '''Validates Git remote auth. If system if posix, returns True. + If on NT, reads tools/ssh-agent/ssh_status.txt, if equals 1, + returns True else runs the tools/ssh-agent/ssh.bat and returns False + ''' + if os.name == 'nt': + script = os.path.join(get_kd_dir(), 'tools', 'ssh-agent', 'ssh.bat') + status_txt = os.path.join(get_kd_dir(), 'tools', 'ssh-agent', + 'ssh_status.txt') + status = open(status_txt, 'r', encoding='utf-8').read() + status = status.strip() + if status == '1': + return True + else: + subprocess.call(script, shell=True) + return False + else: + return True + + @ignore_proj_watcher + def do_init(self, *args): + '''Git init + ''' + return None + try: + self.repo = Repo.init(self.path, mkdir=False) + self.repo.index.commit('Init commit') + self.is_repo = True + self._update_menu() + show_message('Git repo initialized', 5, 'info') + except: + show_alert('Git Init', 'Failted to initialize repo!') + + def do_commit(self, *args): + '''Git commit + ''' + d = get_designer() + if d.popup: + return False + input_dlg = InputDialog('Commit message: ') + d.popup = Popup(title='Git Commit', content=input_dlg, + size_hint=(None, None), size=('300pt', '150pt'), + auto_dismiss=False) + input_dlg.bind(on_confirm=self._perform_do_commit, + on_cancel=d.close_popup) + d.popup.open() + return True + + @ignore_proj_watcher + def _perform_do_commit(self, input, *args): + '''Perform the git commit with data from InputDialog + ''' + # message = input.get_user_input() + # if self.repo.is_dirty(): + # try: + # self.repo.git.commit('-am', message) + # show_message('Commit: ' + message, 5, 'info') + # except GitCommandError as e: + # show_alert('Git Commit', 'Failed to commit!\n' + str(e)) + # else: + # show_alert('Git Commit', 'There is nothing to commit') + + get_designer().close_popup() + + @ignore_proj_watcher + def do_add(self, *args): + '''Git select files from a list to add + ''' + d = get_designer() + if d.popup: + return False + files = self.repo.untracked_files + if not files: + show_alert('Git Add', 'All files are already indexed by Git') + return + + # create the popup + fake_setting = FakeSettingList() + fake_setting.allow_custom = False + fake_setting.items = files + fake_setting.desc = 'Select files to add to Git index' + + content = SettingListContent(setting=fake_setting) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + popup = Popup( + content=content, title='Git - Add files', size_hint=(None, None), + size=(popup_width, popup_height), auto_dismiss=False) + + content.bind(on_apply=self._perform_do_add, + on_cancel=d.close_popup) + + content.show_items() + d.popup = popup + popup.open() + + @ignore_proj_watcher + def _perform_do_add(self, instance, selected_files, *args): + '''Add the selected files to git index + ''' + return None + try: + self.repo.index.add(selected_files) + show_message('%d file(s) added to Git index' % + len(selected_files), 5, 'info') + get_designer().close_popup() + except GitCommandError as e: + show_alert('Git Add', 'Failed to add files to Git!\n' + str(e)) + + def do_branches(self, *args): + '''Shows a list of git branches and allow to change the current one + ''' + d = get_designer() + if d.popup: + return False + branches = [] + for b in self.repo.heads: + branches.append(b.name) + + # create the popup + fake_setting = FakeSettingList() + fake_setting.allow_custom = True + fake_setting.items = branches + fake_setting.desc = 'Checkout to the selected branch. \n' \ + 'You can type a name to create a new branch' + fake_setting.group = 'git_branch' + + content = SettingListContent(setting=fake_setting) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + popup = Popup( + content=content, title='Git - Branches', size_hint=(None, None), + size=(popup_width, popup_height), auto_dismiss=False) + + content.bind(on_apply=self._perform_do_branches, + on_cancel=d.close_popup) + + content.selected_items = [self.repo.active_branch.name] + content.show_items() + d.popup = popup + popup.open() + + @ignore_proj_watcher + def _perform_do_branches(self, instance, branches, *args): + '''If the branch name exists, try to checkout. If a new name, create + the branch and checkout. + If the code has modification, shows an alert and stops + ''' + get_designer().close_popup() + return None + if self.repo.is_dirty(): + show_alert('Git checkout', + 'Please, commit your changes before ' + 'switch branches.') + return None + + if not branches: + return None + + branch = branches[0] + try: + if branch in self.repo.heads: + self.repo.heads[branch].checkout() + else: + self.repo.create_head(branch) + self.repo.heads[branch].checkout() + branch_name = self.repo.active_branch.name + self.dispatch('on_branch', branch_name) + except GitCommandError as e: + show_alert('Git Branches', 'Failed to switch branch!\n' + str(e)) + + def on_branch(self, *args): + '''Dispatch the branch name + ''' + pass + + def do_diff(self, *args): + '''Open a CodeInput with git diff + ''' + diff = self.repo.git.diff() + if not diff: + diff = 'Empty diff' + + d = get_designer() + panel = d.designer_content.tab_pannel + inputs = d.code_inputs + + # check if diff is visible on tabbed panel. + # if so, update the text content + for i, code_input in enumerate(panel.tab_list): + if code_input == self.diff_code_input: + panel.switch_to(panel.tab_list[len(panel.tab_list) - i - 2]) + code_input.content.code_input.text = diff + return + + # if not displayed, create or add it to the screen + if self.diff_code_input is None: + panel_item = DesignerCloseableTab(title='Git diff') + panel_item.bind(on_close=panel.on_close_tab) + scroll = PyScrollView() + _py_code_input = scroll.code_input + _py_code_input.text = diff + _py_code_input.path = '' + _py_code_input.readonly = True + _py_code_input.lexer = DiffLexer() + _py_code_input.saved = True + panel_item.content = scroll + panel_item.rel_path = '' + self.diff_code_input = panel_item + else: + self.diff_code_input.content.code_input.text = diff + panel.add_widget(self.diff_code_input) + panel.switch_to(panel.tab_list[0]) + + def do_push(self, *args): + '''Open a list of remotes to push repository data. + If there is not remote, shows an alert + ''' + d = get_designer() + if d.popup: + return False + if not self.validate_remote(): + show_alert('Git - Remote Authentication', + 'To use Git remote you need to enter your ssh password') + return + remotes = [] + for r in self.repo.remotes: + remotes.append(r.name) + if not remotes: + show_alert('Git Push Remote', 'There is no git remote configured!') + return + + # create the popup + fake_setting = FakeSettingList() + fake_setting.allow_custom = False + fake_setting.items = remotes + fake_setting.desc = 'Push data to the selected remote' + fake_setting.group = 'git_remote' + + content = SettingListContent(setting=fake_setting) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + popup = Popup( + content=content, title='Git - Push Remote', size_hint=(None, None), + size=(popup_width, popup_height), auto_dismiss=False) + + content.bind(on_apply=self._perform_do_push, + on_cancel=d.close_popup) + + content.selected_items = [remotes[0]] + content.show_items() + d.popup = popup + popup.open() + + def _perform_do_push(self, instance, remotes, *args): + '''Try to perform a push + ''' + remote = remotes[0] + remote_repo = self.repo.remotes[remote] + progress = GitRemoteProgress() + + status = Popup(title='Git push progress', + content=progress.label, + size_hint=(None, None), + size=(500, 200)) + status.open() + + @ignore_proj_watcher + def push(*args): + '''Do a push in a separated thread + ''' + # try: + # remote_repo.push(self.repo.active_branch.name, + # progress=progress) + + # def set_progress_done(*args): + # progress.label.text = 'Completed!' + + # Clock.schedule_once(set_progress_done, 1) + # progress.stop() + # show_message('Git remote push completed!', 5, 'info') + # except GitCommandError as e: + # progress.label.text = 'Failed to push!\n' + str(e) + # show_message('Failed to push', 5, 'error') + get_designer().close_popup() + + progress.start() + threading.Thread(target=push).start() + + def do_pull(self, *args): + '''Open a list of remotes to pull remote data. + If there is not remote, shows an alert + ''' + d = get_designer() + if d.popup: + return False + if not self.validate_remote(): + show_alert('Git - Remote Authentication', + 'To use Git remote you need to enter your ssh password') + return + remotes = [] + for r in self.repo.remotes: + remotes.append(r.name) + if not remotes: + show_alert('Git Pull Remote', 'There is no git remote configured!') + return + + # create the popup + fake_setting = FakeSettingList() + fake_setting.allow_custom = False + fake_setting.items = remotes + fake_setting.desc = 'Pull data from the selected remote' + fake_setting.group = 'git_remote' + + content = SettingListContent(setting=fake_setting) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + popup = popup = Popup( + content=content, title='Git - Pull Remote', size_hint=(None, None), + size=(popup_width, popup_height), auto_dismiss=False) + + content.bind(on_apply=self._perform_do_pull, + on_cancel=d.close_popup) + + content.selected_items = [remotes[0]] + content.show_items() + d.popup = popup + popup.open() + + def _perform_do_pull(self, instance, remotes, *args): + '''Try to perform a pull + ''' + remote = remotes[0] + remote_repo = self.repo.remotes[remote] + progress = GitRemoteProgress() + + status = Popup(title='Git pull progress', + content=progress.label, + size_hint=(None, None), + size=(500, 200)) + status.open() + + @ignore_proj_watcher + def pull(*args): + '''Do a pull in a separated thread + ''' + # try: + # remote_repo.pull(progress=progress) + + # def set_progress_done(*args): + # progress.label.text = 'Completed!' + + # Clock.schedule_once(set_progress_done, 1) + # progress.stop() + # show_message('Git remote pull completed!', 5) + # except GitCommandError as e: + # progress.label.text = 'Failed to pull!\n' + str(e) + get_designer().close_popup() + + progress.start() + threading.Thread(target=pull).start() diff --git a/designer/tools/ssh-agent/ssh.bat b/designer/tools/ssh-agent/ssh.bat new file mode 100644 index 0000000..f5c4e05 --- /dev/null +++ b/designer/tools/ssh-agent/ssh.bat @@ -0,0 +1 @@ +start %~dp0ssh_cmd.bat \ No newline at end of file diff --git a/designer/tools/ssh-agent/ssh.sh b/designer/tools/ssh-agent/ssh.sh new file mode 100644 index 0000000..9e50e45 --- /dev/null +++ b/designer/tools/ssh-agent/ssh.sh @@ -0,0 +1 @@ +setsid ssh $@ \ No newline at end of file diff --git a/designer/tools/ssh-agent/ssh_cmd.bat b/designer/tools/ssh-agent/ssh_cmd.bat new file mode 100644 index 0000000..cac9103 --- /dev/null +++ b/designer/tools/ssh-agent/ssh_cmd.bat @@ -0,0 +1,58 @@ +@echo off +rem -- Script originally developed by https://github.com/ericblade/ssh-agent-cmd +rem -- This prevents us from infinite looping at startup. +rem -- Surprise! "FOR" runs a new command processor for every loop! Wow! +IF DEFINED SSH_AGENT_SEARCHING (GOTO :eof) +set SSH_AGENT_SEARCHING=1 +@echo 0 > %~dp0ssh_status.txt + +rem -- *** SET THIS PATH TO THE LOCATION WHERE YOUR SSH BINARIES ARE +set SSH_BIN_PATH="c:\program files (x86)\git\bin\" +rem -- NOTE: If you kill an agent, the socket file remains locked by Windows! Bad! +rem -- This means you'll need to change the below filename if you want to run the +rem -- script again without rebooting. +set SSH_AUTH_SOCK=%TEMP%\ssh-agent-socket.tmp + +:checkAgent +echo Looking for existing ssh-agent... +SET "SSH_AGENT_PID=" +rem -- Call cmd /c to find it, because Take Command's "tasklist" is NOT format compatible with CMD.exe!! +FOR /F "tokens=1-2" %%A IN ('cmd /c tasklist^|find /i "ssh-agent.exe"') DO @(IF %%A==ssh-agent.exe (call :agentexists %%B)) +echo Finished looking... +IF NOT DEFINED SSH_AGENT_PID (GOTO :startagent) +CALL :setregistry +EXIT 0 + +:doAdds + + FOR /R %USERPROFILE%\.ssh\ %%A in (*_rsa.) DO %SSH_BIN_PATH%\ssh-add %%A + EXIT /b + +:agentexists + ECHO Agent exists as process %1 + SET SSH_AGENT_PID=%1 + EXIT /b + +:startagent + ECHO Starting agent + rem -- win 8.1 at least has these set as system, so you can't delete them + attrib -s %SSH_AUTH_SOCK% + del /f /q %SSH_AUTH_SOCK% + %SSH_BIN_PATH%\ssh-agent -a %SSH_AUTH_SOCK% + CALL :doAdds + rem -- Yes, I know this could cause an infinite loop if it can't find one and can't start one. + rem -- I can't seem to figure out how to prevent that. + GOTO :checkAgent + +:setregistry + rem -- store these in the registry. We still need to actually search for the process and + rem -- such at startup, in case the process has died, we rebooted, or what not, but this + rem -- should allow non-CMD command parsers such as bash, Take Command, PowerShell, etc + rem -- or if you're not using the autorun registry change, to pick up the environment. + rem -- Note that SetX does not affect any already open command shells. + SetX SSH_AUTH_SOCK %SSH_AUTH_SOCK% + SetX SSH_AGENT_PID %SSH_AGENT_PID% + @echo 1 > %~dp0ssh_status.txt + EXIT /b + +:eof \ No newline at end of file diff --git a/designer/tools/ssh-agent/ssh_status.txt b/designer/tools/ssh-agent/ssh_status.txt new file mode 100644 index 0000000..e69de29 diff --git a/designer/tools/tools.py b/designer/tools/tools.py new file mode 100644 index 0000000..9c77fc0 --- /dev/null +++ b/designer/tools/tools.py @@ -0,0 +1,226 @@ +import datetime +import os +import shutil +import sys + +from uix.confirmation_dialog import ConfirmationDialog +from utils import constants +from utils.utils import ( + get_current_project, + get_designer, + get_kd_data_dir, + get_kd_dir, + ignore_proj_watcher, + show_alert, +) +from kivy.event import EventDispatcher +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup + + +#### UIs #### +class ToolSetupPy(BoxLayout): + + path = StringProperty('') + '''setup.py path + Instance of :class:`kivy.config.StringProperty` + ''' + + __events__ = ('on_create', 'on_cancel', ) + + @ignore_proj_watcher + def create(self): + '''Create the setup.py + ''' + package_name = self.ids.package_name.text + version = self.ids.version.text + url = self.ids.url.text + license = self.ids.license.text + author = self.ids.author.text + author_email = self.ids.author_email.text + description = self.ids.description.text + + setup_template = '''from distutils.core import setup + +setup( + name='%s', + version='%s', + packages=[], + url='%s', + license='%s', + author='%s', + author_email='%s', + description='%s', +) +''' + setup = setup_template % ( + package_name, + version, + url, + license, + author, + author_email, + description, + ) + + f = open(self.path, 'w').write(setup) + + self.dispatch('on_create') + + def on_create(self, *args): + '''Event handler to "Create" button''' + pass + + def on_cancel(self, *args): + '''Event handler to "Cancel" button''' + pass + + +### Tools ### +class DesignerTools(EventDispatcher): + + designer = ObjectProperty() + '''Instance of Designer + :data:`designer` is a :class:`~kivy.properties.ObjectProperty` + ''' + + @ignore_proj_watcher + def export_png(self): + '''Export playground widget to png file. + If there is a selected widget, export it. + If not, export the root widget + ''' + playground = self.designer.ui_creator.playground + proj_dir = get_current_project().path + status = self.designer.statusbar + + wdg = playground.selected_widget + if wdg is None: + wdg = playground.root + + name = datetime.datetime.now().strftime("%m-%d-%Y_%H-%M-%S.png") + if wdg.id: + name = wdg.id + '_' + name + wdg.export_to_png(os.path.join(proj_dir, name)) + status.show_message('Image saved at ' + name, 5, 'info') + + def check_pep8(self): + '''Check the PEP8 from current project + ''' + proj_dir = get_current_project().path + kd_dir = get_kd_dir() + pep8_dir = os.path.join(kd_dir, 'tools', 'pep8checker', + 'pep8kivy.py') + + python_path =\ + self.designer.designer_settings.config_parser.getdefault( + 'global', + 'python_shell_path', + '' + ) + + if python_path == '': + self.profiler.dispatch('on_error', 'Python Shell Path not ' + 'specified.' + '\n\nUpdate it on \'File\' -> \'Settings\'') + return + + if sys.platform[0] == 'w': + pep8_dir = u'"' + pep8_dir + u'"' + + cmd = '%s %s %s' % (python_path, pep8_dir, proj_dir) + self.designer.ui_creator.tab_pannel.switch_to( + self.designer.ui_creator.tab_pannel.tab_list[2]) + self.designer.ui_creator.kivy_console.run_command(cmd) + + def create_setup_py(self): + '''Runs the GUI to create a setup.py file + ''' + d = get_designer() + if d.popup: + return False + proj_dir = get_current_project().path + designer_content = self.designer.designer_content + + setup_path = os.path.join(proj_dir, 'setup.py') + if os.path.exists(setup_path): + show_alert('Create setup.py', 'setup.py already exists!') + return False + + content = ToolSetupPy(path=setup_path) + d.popup = Popup(title='Create setup.py', content=content, + size_hint=(None, None), size=(550, 350), + auto_dismiss=False) + content.bind(on_cancel=d.close_popup) + + def on_create(*args): + designer_content.update_tree_view(get_current_project()) + d.close_popup() + + content.bind(on_create=on_create) + d.popup.open() + + @ignore_proj_watcher + def create_gitignore(self): + '''Create .gitignore + ''' + proj_dir = get_current_project().path + status = self.designer.statusbar + + gitignore_path = os.path.join(proj_dir, '.gitignore') + + if os.path.exists(gitignore_path): + show_alert('Create .gitignore', '.gitignore already exists!') + return False + + gitignore = '''*.pyc +*.pyo +bin/ +.designer/ +.buildozer/ +__pycache__/''' + + f = open(gitignore_path, 'w').write(gitignore) + status.show_message('.gitignore created successfully', 5, 'info') + + def buildozer_init(self): + '''Checks if the .spec exists or not; and when possible, calls + _perform_buildozer_init + ''' + d = get_designer() + if d.popup: + return False + proj_dir = get_current_project().path + spec_file = os.path.join(proj_dir, 'buildozer.spec') + + if os.path.exists(spec_file): + confirm_dlg = ConfirmationDialog( + message='The buildozer.spec file already exist.' + '\nDo you want to create a new spec?') + d.popup = Popup(title='Buildozer init', + content=confirm_dlg, + size_hint=(None, None), + size=('250pt', '150pt'), + auto_dismiss=False) + confirm_dlg.bind(on_ok=self._perform_buildozer_init, + on_cancel=d.close_popup) + d.popup.open() + else: + self._perform_buildozer_init() + + @ignore_proj_watcher + def _perform_buildozer_init(self, *args): + '''Copies the spec from data/new_templates/default.spec to the project + folder + ''' + get_designer().close_popup() + + proj_dir = get_current_project().path + spec_file = os.path.join(proj_dir, 'buildozer.spec') + + templates_dir = os.path.join(get_kd_data_dir(), + constants.DIR_NEW_TEMPLATE) + shutil.copy(os.path.join(templates_dir, 'default.spec'), spec_file) + + self.designer.designer_content.update_tree_view(get_current_project()) diff --git a/designer/uix/__init__.py b/designer/uix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/uix/action_items.py b/designer/uix/action_items.py new file mode 100644 index 0000000..8f0ba59 --- /dev/null +++ b/designer/uix/action_items.py @@ -0,0 +1,184 @@ +import weakref + +from uix.contextual import ContextSubMenu +from kivy.core.window import Window +from kivy.properties import BooleanProperty, ObjectProperty, StringProperty +from kivy.uix.actionbar import ActionButton, ActionGroup, ActionItem +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.floatlayout import FloatLayout + + +class DesignerActionSubMenu(ContextSubMenu, ActionButton): + pass + + +class ActionCheckButton(ActionItem, FloatLayout): + '''ActionCheckButton is a check button displaying text with a checkbox + ''' + + checkbox = ObjectProperty(None) + '''Instance of :class:`~kivy.uix.checkbox.CheckBox`. + :data:`checkbox` is a :class:`~kivy.properties.ObjectProperty` + ''' + + _label = ObjectProperty(None) + '''Instance of :class:`~kivy.uix.label.Label`. + :data:`_label` is a :class:`~kivy.properties.ObjectProperty` + ''' + + text = StringProperty('Check Button') + '''text which is displayed by ActionCheckButton. + :data:`text` is a :class:`~kivy.properties.StringProperty` + ''' + + desc = StringProperty('') + '''text which is displayed as description to ActionCheckButton. + :data:`desc` is a :class:`~kivy.properties.StringProperty` and defaults + to '' + ''' + + btn_layout = ObjectProperty(None) + '''Instance of :class:`~kivy.uix.boxlayout.BoxLayout`. + :data:`_label` is a :class:`~kivy.properties.ObjectProperty` + ''' + + checkbox_active = BooleanProperty(True) + '''boolean indicating the checkbox.active state + :data:`active` is a :class:`~kivy.properties.BooleanProperty` + ''' + + group = ObjectProperty(None) + '''Checkbox group + :data:`group` is a :class:`~kivy.properties.ObjectProperty` + ''' + + allow_no_selection = BooleanProperty(True) + '''This specifies whether the checkbox in group allows + everything to be deselected. + :data:`allow_no_selection` is a :class:`~kivy.properties.BooleanProperty` + ''' + + cont_menu = ObjectProperty(None) + + __events__ = ('on_active',) + + def on_touch_down(self, touch): + '''Override of its parent's on_touch_down, used to reverse the state + of CheckBox. + ''' + if not self.disabled and self.collide_point(*touch.pos): + self.checkbox._toggle_active() + + def on_active(self, instance, value, *args): + '''Default handler for 'on_active' event. + ''' + self.checkbox_active = value + + +class DesignerActionProfileCheck(ActionCheckButton): + '''DesignerActionSubMenuCheck a + :class `~designer.uix.actioncheckbutton.ActionCheckButton` + It's used to create radio buttons to action menu + ''' + + config_key = StringProperty('') + '''Dict key to the profile config_parser + :data:`config_key` is a :class:`~kivy.properties.StringProperty`, + default to ''. + ''' + + +class DesignerActionGroup(ActionGroup): + + to_open = BooleanProperty(False) + '''To keep check of whether to open the dropdown list or not. + :attr:`to_open` is a :class:`~kivy.properties.BooleanProperty`, + defaults to False. + ''' + hovered = BooleanProperty(False) + '''To keep check of hover over each instance of DesignerActionGroup. + :attr:`hovered` is a :class:`~kivy.properties.BooleanProperty`, + defaults to False. + ''' + instances = [] + '''List to keep the instances of DesignerActionGroup. + ''' + + def __init__(self, **kwargs): + super(DesignerActionGroup, self).__init__(**kwargs) + self.__class__.instances.append(weakref.proxy(self)) + self.register_event_type('on_enter') # Registering the event + Window.bind(mouse_pos=self.on_mouse_pos) + + def on_mouse_pos(self, *args): + try: + pos = args[1] + inside_actionbutton = self.collide_point(*pos) + if self.hovered == inside_actionbutton: + # If mouse is hovering inside the group then return. + return + self.hovered = inside_actionbutton + if inside_actionbutton: + self.dispatch('on_enter') + except: + return + + def on_touch_down(self, touch): + '''Used to determine where touch is down and to change values + of to_open. + ''' + if self.collide_point(touch.x, touch.y): + DesignerActionGroup.to_open = True + return super(DesignerActionGroup, self).on_touch_down(touch) + + if not self.is_open: + DesignerActionGroup.to_open = False + + def on_enter(self): + '''Event handler for on_enter event + ''' + if not self.disabled: + if all(instance.is_open is False for instance in + DesignerActionGroup.instances): + DesignerActionGroup.to_open = False + for instance in DesignerActionGroup.instances: + if instance.is_open: + instance._dropdown.dismiss() + if DesignerActionGroup.to_open is True: + self.is_open = True + self._toggle_dropdown() + + +class DesignerSubActionButton(ActionButton): + + def __init__(self, **kwargs): + super(DesignerSubActionButton, self).__init__(**kwargs) + + def on_press(self): + if self.cont_menu: + self.cont_menu.dismiss() + + +class DesignerActionButton(ActionItem, ButtonBehavior, FloatLayout): + '''DesignerActionButton is a ActionButton to the ActionBar menu + ''' + + text = StringProperty('Button') + '''text which is displayed in the DesignerActionButton. + :data:`text` is a :class:`~kivy.properties.StringProperty` and defaults + to 'Button' + ''' + + hint = StringProperty('') + '''text which is displayed as description to DesignerActionButton. + :data:`hint` is a :class:`~kivy.properties.StringProperty` and defaults + to '' + ''' + cont_menu = ObjectProperty(None) + + def on_press(self, *args): + ''' + Event to hide the ContextualMenu when a ActionButton is pressed + ''' + if self.cont_menu is not None: + self.cont_menu.dismiss() diff --git a/designer/uix/code_find.py b/designer/uix/code_find.py new file mode 100644 index 0000000..10d0043 --- /dev/null +++ b/designer/uix/code_find.py @@ -0,0 +1,57 @@ +from kivy.properties import BooleanProperty, ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout + + +class CodeInputFind(BoxLayout): + '''Widget responsible for searches in the Python Code Input + ''' + + query = StringProperty('') + '''Search query + :data:`query` is a :class:`~kivy.properties.StringProperty` + ''' + + txt_query = ObjectProperty(None) + '''Search query TextInput + :data:`txt_query` is a :class:`~kivy.properties.ObjectProperty` + ''' + + use_regex = BooleanProperty(False) + '''Filter search with regex + :data:`use_regex` is a :class:`~kivy.properties.BooleanProperty` + ''' + + case_sensitive = BooleanProperty(False) + '''Filter search with case sensitive text + :data:`case_sensitive` is a :class:`~kivy.properties.BooleanProperty` + ''' + + __events__ = ('on_close', 'on_next', 'on_prev', ) + + def on_touch_down(self, touch): + '''Enable touche + ''' + if self.collide_point(*touch.pos): + super(CodeInputFind, self).on_touch_down(touch) + return True + + def find_next(self, *args): + '''Search in the opened source code for the search string and updates + the cursor if text is found + ''' + pass + + def find_prev(self, *args): + '''Search in the opened source code for the search string and updates + the cursor if text is found + ''' + pass + + def on_close(self, *args): + pass + + def on_next(self, *args): + pass + + def on_prev(self, *args): + pass diff --git a/designer/uix/code_input.py b/designer/uix/code_input.py new file mode 100644 index 0000000..4a42d1f --- /dev/null +++ b/designer/uix/code_input.py @@ -0,0 +1,189 @@ +import re + +from utils.utils import get_current_project, get_designer, show_alert +from kivy import Config +from kivy.properties import BooleanProperty, StringProperty +from kivy.uix.codeinput import CodeInput +from kivy.utils import get_color_from_hex +from pygments import styles + + +class DesignerCodeInput(CodeInput): + '''A subclass of CodeInput to be used for KivyDesigner. + It has copy, cut and paste functions, which otherwise are accessible + only using Keyboard. + It emits on_show_edit event whenever clicked, this is catched + to show EditContView; + ''' + + __events__ = ('on_show_edit',) + + saved = BooleanProperty(True) + '''Indicates if the current file is saved or not + :data:`saved` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True + ''' + + error = BooleanProperty(False) + '''Indicates if the current file contains any type of error + :data:`error` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + path = StringProperty('') + '''Path of the current file + :data:`path` is a :class:`~kivy.properties.StringProperty` + and defaults to '' + ''' + + clicked = BooleanProperty(False) + '''If clicked is True, then it confirms that this widget has been clicked. + The one checking this property, should set it to False. + :data:`clicked` is a :class:`~kivy.properties.BooleanProperty` + ''' + + def __init__(self, **kwargs): + super(DesignerCodeInput, self).__init__(**kwargs) + parser = Config.get_configparser('DesignerSettings') + if parser: + parser.add_callback(self.on_codeinput_theme, + 'global', 'code_input_theme') + self.style_name = parser.getdefault('global', 'code_input_theme', + 'emacs') + + def on_codeinput_theme(self, section, key, value, *args): + if not value in styles.get_all_styles(): + show_alert("Error", "This theme is not available") + else: + self.style_name = value + + def on_style_name(self, *args): + super(DesignerCodeInput, self).on_style_name(*args) + self.background_color = get_color_from_hex(self.style.background_color) + self._trigger_refresh_text() + + def on_show_edit(self, *args): + pass + + def on_touch_down(self, touch): + '''Override of CodeInput's on_touch_down event. + Used to emit on_show_edit + ''' + if self.collide_point(*touch.pos): + self.clicked = True + self.dispatch('on_show_edit') + + return super(DesignerCodeInput, self).on_touch_down(touch) + + def do_focus(self, *args): + '''Force the focus on this widget + ''' + self.focus = True + + def do_select_all(self, *args): + '''Function to select all text + ''' + self.select_all() + + def on_text(self, *args): + '''Listen text changes + ''' + if self.focus: + self.saved = False + d = get_designer() + get_current_project().saved = False + + def find_next(self, search, use_regex=False, case=False): + '''Find the next occurrence of the string according to the cursor + position + ''' + text = self.text + if not case: + text = text.upper() + search = search.upper() + lines = text.splitlines() + + col = self.cursor_col + row = self.cursor_row + + found = -1 + size = 0 # size of string before selection + line = None + search_size = len(search) + + for i, line in enumerate(lines): + if i >= row: + if use_regex: + if i == row: + line_find = line[col + 1:] + else: + line_find = line[:] + found = re.search(search, line_find) + if found: + search_size = len(found.group(0)) + found = found.start() + else: + found = -1 + else: + # if on current line, consider col + if i == row: + found = line.find(search, col + 1) + else: + found = line.find(search) + # has found the string. found variable indicates the initial po + if found != -1: + self.cursor = (found, i) + break + size += len(line) + + if found != -1: + pos = text.find(line) + found + self.select_text(pos, pos + search_size) + + def find_prev(self, search, use_regex=False, case=False): + '''Find the previous occurrence of the string according to the cursor + position + ''' + text = self.text + if not case: + text = text.upper() + search = search.upper() + lines = text.splitlines() + + col = self.cursor_col + row = self.cursor_row + lines = lines[:row + 1] + lines.reverse() + line_number = len(lines) + + found = -1 + line = None + search_size = len(search) + + for i, line in enumerate(lines): + i = line_number - i - 1 + if use_regex: + if i == row: + line_find = line[:col] + else: + line_find = line[:] + found = re.search(search, line_find) + if found: + search_size = len(found.group(0)) + found = found.start() + else: + found = -1 + else: + # if on current line, consider col + if i == row: + found = line[:col].find(search) + else: + found = line.find(search) + # has found the string. found variable indicates the initial po + if found != -1: + self.cursor = (found, i) + break + + if found != -1: + pos = text.find(line) + found + self.select_text(pos, pos + search_size) diff --git a/designer/uix/completion_bubble.py b/designer/uix/completion_bubble.py new file mode 100644 index 0000000..dd9c1f4 --- /dev/null +++ b/designer/uix/completion_bubble.py @@ -0,0 +1,296 @@ +# from kivy.adapters.listadapter import ListAdapter +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.properties import BooleanProperty, ObjectProperty, StringProperty +from kivy.uix.bubble import Bubble +from kivy.uix.floatlayout import FloatLayout +# from kivy.uix.listview import ListItemButton, ListView +from kivy.uix.button import Button + +class ListView(Button): + adapter = ObjectProperty(None) + +class ListItemButton(Button): + pass + +class ListAdapter(object): + def __init__(self, cls, data, selection_mode, allow_empty_selection, args_converter, *args, **kwwargs): + pass + +Builder.load_string(''' + + + size_hint: None, None + orientation: 'vertical' + width: 200 + height: 210 + arrow_pos: 'top_mid' + +: + size_hint: 1, None + background_normal: '' + background_down: '' + height: 35 + selected_color: 1, 1, 1, 0.1 + deselected_color: 0, 0, 0, 0 + halign: 'left' + valign: 'middle' + text_size: self.width - 20, self.height + shorten: True + shorten_from: 'right' + canvas.before: + Color: + rgba: 0, 0, 0, 0.8 + Rectangle: + pos: self.pos + size: self.size + Color: + rgb: 0.2, 0.2, 0.2 + Rectangle: + pos: self.pos + size: self.width, 1 +''') + + +class SuggestionItem(ListItemButton): + complete = StringProperty('') + '''Completion text + ''' + + selected_by_touch = ObjectProperty(None) + '''Callback function to a item selected by touch + ''' + def on_press(self): + if self.is_selected: + self.selected_by_touch(self) + + +class CompletionListView(ListView): + + scrolled = BooleanProperty(False) + '''(internal) Identify if the user had scrolled the list with the mouse + ''' + + def _scroll(self, scroll_y): + self.scrolled = True + super(CompletionListView, self)._scroll(scroll_y) + + +class CompletionBubble(Bubble): + + list_view = ObjectProperty(None, allownone=True) + '''(internal) Reference a ListView with a list of SuggestionItems + :data:`list_view` is a :class:`~kivy.properties.ObjectProperty` + ''' + + adapter = ObjectProperty(None) + '''(internal) Reference a ListView adapter + :data:`adapter` is a :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_complete', 'on_cancel', ) + + def __init__(self, **kwargs): + super(CompletionBubble, self).__init__(**kwargs) + Window.bind(on_touch_down=self.on_window_touch_down) + + def on_window_touch_down(self, win, touch): + '''Disable the completion if the user clicks anywhere + ''' + if not self.collide_point(*touch.pos): + self.dispatch('on_cancel') + + def _create_list_view(self, data): + '''Create the ListAdapter + ''' + self.adapter = ListAdapter( + data=data, + args_converter=self._args_converter, + cls=SuggestionItem, + selection_mode='single', + allow_empty_selection=False + ) + self.adapter.bind(on_selection_change=self.on_selection_change) + self.list_view = CompletionListView(adapter=self.adapter) + self.add_widget(self.list_view) + + def _args_converter(self, index, completion): + return {'text': completion.name, + 'is_selected': False, + 'complete': completion.complete, + 'selected_by_touch': self.selected_by_touch} + + def selected_by_touch(self, item): + self.dispatch('on_complete', item.complete) + + def show_completions(self, completions, force_scroll=False): + '''Update the Completion ListView with completions + ''' + if completions == []: + fake_completion = type('obj', (object,), + {'name': 'No suggestions', 'complete': ''}) + completions.append(fake_completion) + Window.bind(on_key_down=self.on_key_down) + if not self.list_view: + self._create_list_view(completions) + else: + self.adapter.data = completions + if force_scroll: + self.list_view.scroll_to(0) + + def on_selection_change(self, *args): + pass + + def _scroll_item(self, new_index): + '''Update the scroll view position to display the new_index item + ''' + item = self.adapter.get_view(new_index) + if item: + item.trigger_action(0) + if new_index > 2 and new_index < len(self.adapter.data) - 1: + self.list_view.scroll_to(new_index - 3) + + def on_key_down(self, instance, key, *args): + '''Keyboard listener to grab key codes and interact with the + Completion box + ''' + selected_item = self.adapter.selection[0] + selected_index = selected_item.index + if self.list_view.scrolled: + # recreate list view after mouse scroll due to the bug kivy/#3418 + self.remove_widget(self.list_view) + self.list_view = None + self.show_completions(self.adapter.data) + return self.on_key_down(instance, key, args) + + if key == 273: + # up + if selected_index > 0: + self._scroll_item(selected_index - 1) + return True + + elif key == 274: + # down + if selected_index < len(self.adapter.data) - 1: + self._scroll_item(selected_index + 1) + return True + + elif key in [9, 13, 32]: + # tab, enter or space + self.dispatch('on_complete', selected_item.complete) + return True + + else: + # another key cancel the completion + self.dispatch('on_cancel') + return False + + def reposition(self, pos, line_height): + '''Update the Bubble position. Try to display it in the best place of + the screen + ''' + win = Window + self.x = pos[0] - self.width / 2 + self.y = pos[1] - self.height - line_height + + # fit in the screen horizontally + if self.right > win.width: + self.x = win.width - self.width + if self.x < 0: + self.x = 0 + + # fit in the screen vertically + if self.y < 0: + diff = abs(self.y) + # check if we can move it to top + new_y = pos[1] + line_height + if new_y + self.height < win.height: # fit in the screen + self.y = new_y + else: # doesnt fit on top neither on bottom. Check the best place + new_diff = abs(new_y + self.height - win.height) + if new_diff < diff: # if we lose lest moving it to top + self.y = new_y + + # compare the desired position with the actual position + x_relative = self.x - (pos[0] - self.width / 2) + + x_range = self.width / 4 # consider 25% as the range + + def _get_hpos(): + '''Compare the position of the widget with the parent + to display the arrow in the correct position + ''' + _pos = 'mid' + if x_relative == 0: + _pos = 'mid' + elif x_relative < -x_range: + _pos = 'right' + elif x_relative > x_range: + _pos = 'left' + return _pos + + if self.y == pos[1] - self.height - line_height: + self.arrow_pos = 'top_' + _get_hpos() + else: + self.arrow_pos = 'bottom_' + _get_hpos() + + def on_complete(self, *args): + '''Dispatch a completion selection + ''' + Window.unbind(on_key_down=self.on_key_down) + + def on_cancel(self, *args): + '''Disable key listener on cancel + ''' + Window.unbind(on_key_down=self.on_key_down) + +if __name__ == '__main__': + from kivy.app import App + import jedi + + Builder.load_string(''' +: + background_normal: '' + background_color: 1, 0, 1, 1 + canvas.before: + Color: + rgba: 1, 1, 1, 0.8 + Rectangle: + pos: self.pos + size: self.size + Button: + text: 'Toggle Completion Menu' + size_hint: None, None + width: 250 + height: 50 + pos_hint: {'center_x': 0.5, 'center_y': 0.5} + on_press: root.show_bubble() +''') + + class Test(FloatLayout): + def __init__(self, **kwargs): + super(Test, self).__init__(**kwargs) + self.bubble = CompletionBubble() + self.bubble.pos_hint = {'center_x': 0.5, 'y': 0} + self.bubble.bind(on_cancel=self.on_cancel) + self.bubble.bind(on_complete=self.on_cancel) + + def show_bubble(self): + source = ''' +datetime.da''' + script = jedi.Script(source, 3, len('datetime.da')) + completions = script.completions() + self.bubble.show_completions(completions * 10) + self.add_widget(self.bubble) + + def on_cancel(self, *args): + if self.bubble.parent is not None: + self.bubble.show_completions([]) + self.bubble.parent.remove_widget(self.bubble) + self.is_bubble_visible = False + + class MyApp(App): + def build(self): + return Test() + + MyApp().run() diff --git a/designer/uix/confirmation_dialog.py b/designer/uix/confirmation_dialog.py new file mode 100644 index 0000000..b1901ca --- /dev/null +++ b/designer/uix/confirmation_dialog.py @@ -0,0 +1,56 @@ +from kivy.properties import StringProperty +from kivy.uix.boxlayout import BoxLayout + + +class ConfirmationDialog(BoxLayout): + '''ConfirmationDialog shows a confirmation message with two buttons + "Yes" and "No". It may be used for confirming user about an operation. + It emits 'on_ok' when "Yes" is pressed and 'on_cancel' when "No" is + pressed. + ''' + + message = StringProperty('') + '''It is the message to be shown + :data:`message` is a :class:`~kivy.properties.StringProperty` + ''' + + __events__ = ('on_ok', 'on_cancel') + + def __init__(self, message): + super(ConfirmationDialog, self).__init__() + self.message = message + + def on_ok(self, *args): + pass + + def on_cancel(self, *args): + pass + + +class ConfirmationDialogSave(BoxLayout): + '''ConfirmationDialogSave shows a confirmation message with three buttons + "Save", "Don't Save" and "Cancel". It may be used for confirming user + about an operation. It emits 'on_save' when "Save" is pressed, + 'on_dont_save' when "Don't Save" is pressed and 'on_cancel' + when "No" is pressed. + ''' + + message = StringProperty('') + '''It is the message to be shown + :data:`message` is a :class:`~kivy.properties.StringProperty` + ''' + + __events__ = ('on_save', 'on_dont_save', 'on_cancel') + + def __init__(self, message): + super(ConfirmationDialogSave, self).__init__() + self.message = message + + def on_save(self, *args): + pass + + def on_dont_save(self, *args): + pass + + def on_cancel(self, *args): + pass diff --git a/designer/uix/contextual.py b/designer/uix/contextual.py new file mode 100644 index 0000000..9d0705f --- /dev/null +++ b/designer/uix/contextual.py @@ -0,0 +1,667 @@ +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import BooleanProperty, NumericProperty, ObjectProperty +from kivy.uix.actionbar import ActionItem, ActionView +from kivy.uix.bubble import Bubble +from kivy.uix.button import Button +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.gridlayout import GridLayout +from kivy.uix.image import Image +from kivy.uix.scrollview import ScrollView +from kivy.uix.tabbedpanel import ( + TabbedPanel, + TabbedPanelContent, + TabbedPanelHeader, +) + + +class DesignerActionView(ActionView): + '''Custom ActionView to support custom action group + ''' + + def _layout_random(self): + '''Handle custom action group + ''' + self.overflow_group.show_group = self.show_group + super(DesignerActionView, self)._layout_random() + + def show_group(self, *l): + '''Show custom groups + ''' + over = self.overflow_group + over.clear_widgets() + for item in over._list_overflow_items + over.list_action_item: + item.inside_group = True + if item.parent is not None: + item.parent.remove_widget(item) + group = self.get_group(item) + if group is not None and group.disabled: + continue + if not isinstance(item, ContextSubMenu): + over._dropdown.add_widget(item) + + def get_group(self, item): + '''Get the ActionGroup of an item + ''' + for group in self._list_action_group: + if item in group.list_action_item: + return group + return None + + +class MenuBubble(Bubble): + ''' + ''' + pass + + +class MenuHeader(TabbedPanelHeader): + '''MenuHeader class. To be used as default TabbedHeader. + ''' + show_arrow = BooleanProperty(False) + '''Specifies whether to show arrow or not. + :data:`show_arrow` is a :class:`~kivy.properties.BooleanProperty`, + default to True + ''' + + +class ContextMenuException(Exception): + '''ContextMenuException class + ''' + pass + + +class MenuButton(Button): + '''MenuButton class. Used as a default menu button. It auto provides + look and feel for a menu button. + ''' + cont_menu = ObjectProperty(None) + '''Reference to + :class:`~designer.components.edit_contextual_view.ContextMenu`. + ''' + + def on_release(self, *args): + '''Default Event Handler for 'on_release' + ''' + self.cont_menu.dismiss() + super(MenuButton, self).on_release(*args) + + +class ContextMenu(TabbedPanel): + '''ContextMenu class. See module documentation for more information. + :Events: + `on_select`: data + Fired when a selection is done, with the data of the selection as + first argument. Data is what you pass in the :meth:`select` method + as first argument. + `on_dismiss`: + .. versionadded:: 1.8.0 + + Fired when the ContextMenu is dismissed either on selection or on + touching outside the widget. + ''' + container = ObjectProperty(None) + '''(internal) The container which will be used to contain Widgets of + main menu. + :data:`container` is a :class:`~kivy.properties.ObjectProperty`, default + to :class:`~kivy.uix.boxlayout.BoxLayout`. + ''' + + main_tab = ObjectProperty(None) + '''Main Menu Tab of ContextMenu. + :data:`main_tab` is a :class:`~kivy.properties.ObjectProperty`, default + to None. + ''' + + bubble_cls = ObjectProperty(MenuBubble) + '''Bubble Class, whose instance will be used to create + container of ContextMenu. + :data:`bubble_cls` is a :class:`~kivy.properties.ObjectProperty`, + default to :class:`MenuBubble`. + ''' + + header_cls = ObjectProperty(MenuHeader) + '''Header Class used to create Tab Header. + :data:`header_cls` is a :class:`~kivy.properties.ObjectProperty`, + default to :class:`MenuHeader`. + ''' + + attach_to = ObjectProperty(allownone=True) + '''(internal) Property that will be set to the widget on which the + drop down list is attached to. + + The method :meth:`open` will automatically set that property, while + :meth:`dismiss` will set back to None. + ''' + + auto_width = BooleanProperty(True) + '''By default, the width of the ContextMenu will be the same + as the width of the attached widget. Set to False if you want + to provide your own width. + ''' + + dismiss_on_select = BooleanProperty(True) + '''By default, the ContextMenu will be automatically dismissed + when a selection have been done. Set to False to prevent the dismiss. + + :data:`dismiss_on_select` is a :class:`~kivy.properties.BooleanProperty`, + default to True. + ''' + + max_height = NumericProperty(None, allownone=True) + '''Indicate the maximum height that the dropdown can take. If None, it will + take the maximum height available, until the top or bottom of the screen + will be reached. + + :data:`max_height` is a :class:`~kivy.properties.NumericProperty`, default + to None. + ''' + + __events__ = ('on_select', 'on_dismiss') + + def __init__(self, **kwargs): + self._win = None + self.add_tab = super(ContextMenu, self).add_widget + self.bubble = self.bubble_cls(size_hint=(None, None)) + self.container = None + self.main_tab = self.header_cls(text='Main') + self.main_tab.content = ScrollView(size_hint=(1, 1)) + self.main_tab.content.bind(height=self.on_scroll_height) + + super(ContextMenu, self).__init__(**kwargs) + self.bubble.add_widget(self) + self.bind(size=self._reposition) + self.bubble.bind(on_height=self._bubble_height) + + def _bubble_height(self, *args): + '''Handler for bubble's 'on_height' event. + ''' + self.height = self.bubble.height + + def open(self, widget): + '''Open the dropdown list, and attach to a specific widget. + Depending the position of the widget on the window and + the height of the dropdown, the placement might be + lower or higher off that widget. + ''' + # if trying to open a non-visible widget + if widget.parent is None: + return + + # ensure we are not already attached + if self.attach_to is not None: + self.dismiss() + + # we will attach ourself to the main window, so ensure the widget we are + # looking for have a window + self._win = widget.get_parent_window() + if self._win is None: + raise ContextMenuException( + 'Cannot open a dropdown list on a hidden widget') + + self.attach_to = widget + widget.bind(pos=self._reposition, size=self._reposition) + + self.add_tab(self.main_tab) + self.switch_to(self.main_tab) + self.main_tab.show_arrow = False + + self._reposition() + + # attach ourself to the main window + self._win.add_widget(self.bubble) + self.main_tab.color = (0, 0, 0, 0) + + def on_select(self, data): + '''Default handler for 'on_select' event. + ''' + pass + + def dismiss(self, *largs): + '''Remove the dropdown widget from the window, and detach itself from + the attached widget. + ''' + if self.bubble.parent: + self.bubble.parent.remove_widget(self.bubble) + if self.attach_to: + self.attach_to.unbind(pos=self._reposition, size=self._reposition) + self.attach_to = None + + self.switch_to(self.main_tab) + + for child in self.tab_list[:]: + self.remove_widget(child) + + self.dispatch('on_dismiss') + + def select(self, data): + '''Call this method to trigger the `on_select` event, with the `data` + selection. The `data` can be anything you want. + ''' + self.dispatch('on_select', data) + if self.dismiss_on_select: + self.dismiss() + + def on_dismiss(self): + '''Default event handler for 'on_dismiss' event. + ''' + pass + + def _set_width_to_bubble(self, *args): + '''To set self.width and bubble's width equal. + ''' + self.width = self.bubble.width + + def _reposition(self, *largs): + # calculate the coordinate of the attached widget in the window + # coordinate sysem + win = self._win + widget = self.attach_to + if not widget or not win: + return + + wx, wy = widget.to_window(*widget.pos) + wright, wtop = widget.to_window(widget.right, widget.top) + + # set width and x + if self.auto_width: + # Calculate minimum required width + if len(self.container.children) == 1: + self.bubble.width = max(self.main_tab.parent.parent.width, + self.container.children[0].width) + else: + self.bubble.width = max(self.main_tab.parent.parent.width, + self.bubble.width, + *([i.width + for i in self.container.children])) + + Clock.schedule_once(self._set_width_to_bubble, 0.01) + # ensure the dropdown list doesn't get out on the X axis, with a + # preference to 0 in case the list is too wide. + # try to center bubble with parent position + x = wx - self.bubble.width / 4 + if x + self.bubble.width > win.width: + x = win.width - self.bubble.width + if x < 0: + x = 0 + self.bubble.x = x + # bubble position relative with the parent center + x_relative = x - (wx - self.bubble.width / 4) + x_range = self.bubble.width / 4 # consider 25% as the range + + # determine if we display the dropdown upper or lower to the widget + h_bottom = wy - self.bubble.height + h_top = win.height - (wtop + self.bubble.height) + + def _get_hpos(): + '''Compare the position of the widget with the parent + to display the arrow in the correct position + ''' + _pos = 'mid' + if x_relative == 0: + _pos = 'mid' + elif x_relative < -x_range: + _pos = 'right' + elif x_relative > x_range: + _pos = 'left' + return _pos + + if h_bottom > 0: + self.bubble.top = wy + self.bubble.arrow_pos = 'top_' + _get_hpos() + elif h_top > 0: + self.bubble.y = wtop + self.bubble.arrow_pos = 'bottom_' + _get_hpos() + else: + # none of both top/bottom have enough place to display the widget at + # the current size. Take the best side, and fit to it. + height = max(h_bottom, h_top) + if height == h_bottom: + self.bubble.top = wy + self.bubble.height = wy + self.bubble.arrow_pos = 'top_' + _get_hpos() + else: + self.bubble.y = wtop + self.bubble.height = win.height - wtop + self.bubble.arrow_pos = 'bottom_' + _get_hpos() + + def on_touch_down(self, touch): + '''Default Handler for 'on_touch_down' + ''' + if super(ContextMenu, self).on_touch_down(touch): + return True + if self.collide_point(*touch.pos): + return True + self.dismiss() + + def on_touch_up(self, touch): + '''Default Handler for 'on_touch_up' + ''' + + if super(ContextMenu, self).on_touch_up(touch): + return True + self.dismiss() + + def add_widget(self, widget, index=0): + '''Add a widget. + ''' + if self.content is None: + return + + if widget.parent is not None: + widget.parent.remove_widget(widget) + + if self.tab_list and widget == self.tab_list[0].content or\ + widget == self._current_tab.content or \ + self.content == widget or\ + self._tab_layout == widget or\ + isinstance(widget, TabbedPanelContent) or\ + isinstance(widget, TabbedPanelHeader): + super(ContextMenu, self).add_widget(widget, index) + return + + if not self.container: + self.container = GridLayout(orientation='vertical', + size_hint_y=None, + cols=1) + self.main_tab.content.add_widget(self.container) + self.container.bind(height=self.on_main_box_height) + + self.container.add_widget(widget, index) + + if hasattr(widget, 'cont_menu'): + widget.cont_menu = self + + widget.bind(height=self.on_child_height) + widget.size_hint_y = None + + def remove_widget(self, widget): + '''Remove a widget + ''' + if self.container and widget in self.container.children: + self.container.remove_widget(widget) + else: + super(ContextMenu, self).remove_widget(widget) + + def on_scroll_height(self, *args): + '''Event Handler for scollview's height. + ''' + if not self.container: + return + + self.container.height = max(self.container.height, + self.main_tab.content.height) + + def on_main_box_height(self, *args): + '''Event Handler for main_box's height. + ''' + + if not self.container: + return + + self.container.height = max(self.container.height, + self.main_tab.content.height) + + if self.max_height: + self.bubble.height = min(self.container.height + + self.tab_height + dp(16), + self.max_height) + else: + self.bubble.height = self.container.height + \ + self.tab_height + dp(16) + + def on_child_height(self, *args): + '''Event Handler for children's height. + ''' + height = 0 + for i in self.container.children: + height += i.height + + self.main_tab.content.height = height + self.container.height = height + + def add_tab(self, widget, index=0): + '''To add a Widget as a new Tab. + ''' + super(ContextMenu, self).add_widget(widget, index) + + +class ContextSubMenu(MenuButton): + '''ContextSubMenu class. To be used to add a sub menu. + ''' + + attached_menu = ObjectProperty(None) + '''(internal) Menu attached to this sub menu. + :data:`attached_menu` is a :class:`~kivy.properties.ObjectProperty`, + default to None. + ''' + + cont_menu = ObjectProperty(None) + '''(internal) Reference to the main ContextMenu. + :data:`cont_menu` is a :class:`~kivy.properties.ObjectProperty`, + default to None. + ''' + + container = ObjectProperty(None) + '''(internal) The container which will be used to contain Widgets of + main menu. + :data:`container` is a :class:`~kivy.properties.ObjectProperty`, default + to :class:`~kivy.uix.boxlayout.BoxLayout`. + ''' + + show_arrow = BooleanProperty(False) + '''(internal) To specify whether ">" arrow image should be shown in the + header or not. If there exists a child menu then arrow image will be + shown otherwise not. + :data:`show_arrow` is a + :class:`~kivy.properties.BooleanProperty`, default to False + ''' + + def __init__(self, **kwargs): + super(ContextSubMenu, self).__init__(**kwargs) + self._list_children = [] + + def on_text(self, *args): + '''Default handler for text. + ''' + if self.attached_menu: + self.attached_menu.text = self.text + + def on_attached_menu(self, *args): + '''Default handler for attached_menu. + ''' + self.attached_menu.text = self.text + + def add_widget(self, widget, index=0): + '''Add a widget. + ''' + if isinstance(widget, Image): + Button.add_widget(self, widget, index) + return + + self._list_children.append((widget, index)) + if hasattr(widget, 'cont_menu'): + widget.cont_menu = self.cont_menu + + def remove_children(self): + '''Clear _list_children[] + ''' + for child, index in self._list_children: + self.container.remove_widget(child) + self._list_children = [] + + def on_cont_menu(self, *args): + '''Default handler for cont_menu. + ''' + self._add_widget() + + def _add_widget(self, *args): + if not self.cont_menu: + return + + if not self.attached_menu: + self.attached_menu = self.cont_menu.header_cls(text=self.text) + self.attached_menu.content = ScrollView(size_hint=(1, 1)) + self.attached_menu.content.bind(height=self.on_scroll_height) + self.container = GridLayout(orientation='vertical', + size_hint_y=None, cols=1) + + self.attached_menu.content.add_widget(self.container) + self.container.bind(height=self.on_container_height) + + for widget, index in self._list_children: + self.container.add_widget(widget, index) + widget.cont_menu = self.cont_menu + widget.bind(height=self.on_child_height) + + def on_scroll_height(self, *args): + '''Handler for scrollview's height. + ''' + self.container.height = max(self.container.minimum_height, + self.attached_menu.content.height) + + def on_container_height(self, *args): + '''Handler for container's height. + ''' + self.container.height = max(self.container.minimum_height, + self.attached_menu.content.height) + + def on_child_height(self, *args): + '''Handler for children's height. + ''' + height = 0 + for i in self.container.children: + height += i.height + + self.container.height = height + + def on_release(self, *args): + '''Default handler for 'on_release' event. + ''' + if not self.attached_menu or not self._list_children: + return + + try: + index = self.cont_menu.tab_list.index(self.attached_menu) + self.cont_menu.switch_to(self.cont_menu.tab_list[index]) + tab = self.cont_menu.tab_list[index] + if hasattr(tab, 'show_arrow') and index != 0: + tab.show_arrow = True + else: + tab.show_arrow = False + + except: + if not self.cont_menu.current_tab in self.cont_menu.tab_list: + return + curr_index = self.cont_menu.tab_list.index( + self.cont_menu.current_tab) + for i in range(curr_index - 1, -1, -1): + self.cont_menu.remove_widget(self.cont_menu.tab_list[i]) + + self.cont_menu.add_tab(self.attached_menu) + self.cont_menu.switch_to(self.cont_menu.tab_list[0]) + if hasattr(self.cont_menu.tab_list[1], 'show_arrow'): + self.cont_menu.tab_list[1].show_arrow = True + else: + self.cont_menu.tab_list[1].show_arrow = False + + from kivy.clock import Clock + Clock.schedule_once(self._scroll, 0.1) + + def _scroll(self, dt): + '''To scroll ContextMenu's strip to appropriate place. + ''' + from kivy.animation import Animation + self.cont_menu._reposition() + total_tabs = len(self.cont_menu.tab_list) + tab_list = self.cont_menu.tab_list + curr_index = total_tabs - tab_list.index(self.cont_menu.current_tab) + to_scroll = len(tab_list) / curr_index + anim = Animation(scroll_x=to_scroll, d=0.75) + anim.cancel_all(self.cont_menu.current_tab.parent.parent) + anim.start(self.cont_menu.current_tab.parent.parent) + +if __name__ == '__main__': + from kivy.app import App + + class ActionContext(ContextSubMenu, ActionItem): + pass + + Builder.load_string(''' +#:import ContextMenu contextual.ContextMenu + +: +: +: + ActionBar: + pos_hint: {'top':1} + DesignerActionView: + use_separator: True + ActionPrevious: + title: 'Action Bar' + with_previous: False + ActionOverflow: + ActionButton: + text: 'Btn0' + icon: 'atlas://data/images/defaulttheme/audio-volume-high' + ActionButton: + text: 'Btn1' + ActionButton: + text: 'Btn2' + ActionButton: + text: 'Btn3' + ActionButton: + text: 'Btn4' + ActionGroup: + mode: 'spinner' + text: 'Group1' + dropdown_cls: ContextMenu + ActionButton: + text: 'Btn5' + height: 30 + size_hint_y: None + ActionButton: + text: 'Btnddddddd6' + height: 30 + size_hint_y: None + ActionButton: + text: 'Btn7' + height: 30 + size_hint_y: None + + ActionContext: + text: 'Item2' + size_hint_y: None + height: 30 + ActionButton: + text: '2->1' + size_hint_y: None + height: 30 + ActionButton: + text: '2->2' + size_hint_y: None + height: 30 + ActionButton: + text: '2->2' + size_hint_y: None + height: 30 +''') + + class CMenu(ContextMenu): + pass + + class Test(FloatLayout): + def __init__(self, **kwargs): + super(Test, self).__init__(**kwargs) + self.context_menu = CMenu() + + def add_menu(self, obj, *l): + self.context_menu = CMenu() + self.context_menu.open(self.children[0]) + + class MyApp(App): + def build(self): + return Test() + + MyApp().run() diff --git a/designer/uix/info_bubble.py b/designer/uix/info_bubble.py new file mode 100644 index 0000000..8af04dd --- /dev/null +++ b/designer/uix/info_bubble.py @@ -0,0 +1,54 @@ +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.properties import StringProperty +from kivy.uix.bubble import Bubble + + +class InfoBubble(Bubble): + '''Bubble to be used to display short Help Information''' + + message = StringProperty('') + '''Message to be displayed + :data:`message` is a :class:`~kivy.properties.StringProperty` + ''' + + def show(self, pos, duration, width=None): + '''Animate the bubble into position''' + if width: + self.width = width + # wait for the bubble to adjust it's size according to text then animate + Clock.schedule_once(lambda dt: self._show(pos, duration)) + + def _show(self, pos, duration): + '''To show Infobubble at pos with Animation of duration. + ''' + def on_stop(*l): + if duration: + Clock.schedule_once(self.hide, duration + .5) + + self.opacity = 0 + arrow_pos = self.arrow_pos + if arrow_pos[0] in ('l', 'r'): + pos = pos[0], pos[1] - (self.height / 2) + else: + pos = pos[0] - (self.width / 2), pos[1] + + self.limit_to = Window + self.pos = pos + Window.add_widget(self) + + anim = Animation(opacity=1, d=0.75) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) + + def hide(self, *dt): + ''' Auto fade out the Bubble + ''' + def on_stop(*l): + Window.remove_widget(self) + anim = Animation(opacity=0, d=0.75) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) diff --git a/designer/uix/input_dialog.py b/designer/uix/input_dialog.py new file mode 100644 index 0000000..23ed684 --- /dev/null +++ b/designer/uix/input_dialog.py @@ -0,0 +1,72 @@ +from kivy.properties import ObjectProperty, StringProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.textinput import TextInput + + +class InputDialog(BoxLayout): + '''InputDialog is a widget with a TextInput, Cancel and Confirm button. + ''' + + message = StringProperty('') + '''It is the message to be shown + :data:`message` is a :class:`~kivy.properties.StringProperty` + ''' + + user_input = ObjectProperty() + '''Is the UserTextInput + :data:`user_input` is a + :class:`~designer.uix.input_dialog.UserTextInput` + ''' + + btn_confirm = ObjectProperty() + '''Is the button to confirm the input + :data:`btn_confirm` is a :class:`~kivy.uix.button.Button` + ''' + + lbl_error = ObjectProperty() + '''Is a Label to show errors + :data:`lbl_error` is a :class:`~kivy.uix.label.Label` + ''' + + __events__ = ('on_confirm', 'on_cancel',) + + def __init__(self, message): + super(InputDialog, self).__init__() + self.message = message + self.user_input.bind(text=self.on_text) + + def on_confirm(self, *args): + pass + + def on_cancel(self, *args): + pass + + def get_user_input(self): + ''' + Returns the user input + ''' + return self.user_input.text + + def on_text(self, *args): + self.btn_confirm.disabled = len(args[1]) == 0 + + +class UserTextInput(TextInput): + ''' + TextInput used by InputDialog. + Used to filter the input and handle events + ''' + + def __init__(self, **kwargs): + super(UserTextInput, self).__init__(**kwargs) + + def insert_text(self, substring, from_undo=False): + ''' + Override the default insert_text to add a filter + ''' + s = substring if \ + (substring.isalnum() or substring in ['.', '-', '_']) and \ + len(self.text) < 32 \ + else "" + + return super(UserTextInput, self).insert_text(s, from_undo=from_undo) diff --git a/designer/uix/py_code_input.py b/designer/uix/py_code_input.py new file mode 100644 index 0000000..61a7f19 --- /dev/null +++ b/designer/uix/py_code_input.py @@ -0,0 +1,134 @@ +import jedi +from uix.code_input import DesignerCodeInput +from uix.completion_bubble import CompletionBubble +from kivy.app import App +from kivy.core.window import Window +from kivy.properties import BooleanProperty, ObjectProperty +from kivy.uix.scrollview import ScrollView + + +MarkupLabel = None + + +class PyCodeInput(DesignerCodeInput): + '''PyCodeInput used as the CodeInput for editing Python Files. + It's rel_file_path property, gives the file path of the file it is + currently displaying relative to Project Directory + ''' + + +class PyScrollView(ScrollView): + '''PyScrollView used as a :class:`~kivy.scrollview.ScrollView` + for adding :class:`~designer.uix.py_code_input.PyCodeInput`. + ''' + + code_input = ObjectProperty(None) + '''(internal) Reference to the + :class:`~designer.uix.py_code_input.PyCodeInput`. + :data:`code_input` is a :class:`~kivy.properties.ObjectProperty` + ''' + + line_number = ObjectProperty(None) + '''(internal) Text Input to display line numbers + :data:`line_number` is a :class:`~kivy.properties.ObjectProperty` + ''' + + bubble = ObjectProperty(None) + '''(internal) Bubble to display completions suggestion + :data:`line_number` is a :class:`~kivy.properties.ObjectProperty` + ''' + + is_bubble_visible = BooleanProperty(False) + '''(internal) If bubble is visible in the screen + :data:`line_number` is a :class:`~kivy.properties.ObjectProperty` + ''' + + show_line_number = BooleanProperty(True) + '''Display line number on left + :data:`show_line_number` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True + ''' + + use_autocompletion = BooleanProperty(True) + '''Use autocompletion + :data:`use_autocompletion` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True + ''' + + def __init__(self, **kwargs): + super(PyScrollView, self).__init__(**kwargs) + self._max_num_of_lines = 0 + self.bubble = CompletionBubble() + self.bubble.bind(on_cancel=self.cancel_completion) + self.bubble.bind(on_complete=self.on_complete) + self.root = App.get_running_app().root + + if self.use_autocompletion: + self.code_input.bind(focus=self.on_code_input_focus) + + if not self.show_line_number: + self.line_number.parent.remove_widget(self.line_number) + else: + self.code_input.bind(_lines=self.on_lines_changed) + + def on_code_input_focus(self, *args): + '''Focus on CodeInput, to enable/disable keyboard listener + ''' + if args[1]: + Window.bind(on_keyboard=self.on_keyboard) + else: + Window.unbind(on_keyboard=self.on_keyboard) + + def on_keyboard(self, instance, key, scancode, codepoint, modifier): + if key == 32 and modifier == ['ctrl']: + code = self.code_input + src = code.text + line = code.cursor_row + 1 + col = code.cursor_col + script = jedi.Script(src, line, col) + completions = script.completions() + self.show_completion(completions) + + def on_complete(self, instance, completion): + '''Add the completion to the current cursor position + ''' + self.code_input.insert_text(completion) + self.cancel_completion() + + def show_completion(self, completions): + '''Display the bubble with the completions + ''' + self.bubble.show_completions(completions, force_scroll=True) + self.bubble.reposition( + self.code_input.to_window(*self.code_input.cursor_pos), + self.code_input.line_height + self.code_input.line_spacing + ) + self.root.add_widget(self.bubble) + self.is_bubble_visible = True + + def cancel_completion(self, *args): + '''Event handler to cancel the completion + ''' + if self.bubble.parent is not None: + self.bubble.show_completions([]) + self.bubble.parent.remove_widget(self.bubble) + self.is_bubble_visible = False + + def on_lines_changed(self, *args): + '''Event handler that listen the line modifications to update + line_number + ''' + n = len(self.code_input._lines) + if n > self._max_num_of_lines: + self.update_line_number(self._max_num_of_lines, n) + + def update_line_number(self, old, new): + '''Analyze the difference between old and new number of lines + to update the text input + ''' + self._max_num_of_lines = new + self.line_number.text += \ + '\n'.join([str(i) for i in range(old + 1, new + 1)]) + '\n' + self.line_number.width = self.line_number._label_cached.get_extents( + str(self._max_num_of_lines))[0] + (self.line_number.padding[0] * 2) + # not removing lines, as long as extra lines will not be visible diff --git a/designer/uix/py_console.py b/designer/uix/py_console.py new file mode 100644 index 0000000..4aa5d84 --- /dev/null +++ b/designer/uix/py_console.py @@ -0,0 +1,354 @@ +import code +import sys +import threading + +from utils.utils import show_message +from kivy.base import runTouchApp +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ( + ListProperty, + NumericProperty, + ObjectProperty, + partial, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.codeinput import CodeInput +from pygments.lexers.python import PythonConsoleLexer + + +try: + from rlcompleter import Completer +except ImportError: + Completer = None + +Builder.load_string(''' +: + text_input: interactive_text_input + scroll_view: scroll_view + ScrollView: + id: scroll_view + InteractiveShellInput: + id: interactive_text_input + size_hint: (1, None) + font_size: root.font_size + foreground_color: root.foreground_color + background_color: root.background_color + height: max(self.parent.height, self.minimum_height) + on_ready_to_input: root.ready_to_input() + sh: root.sh +''') + + +class PseudoFile(object): + '''A psuedo file object, to redirect I/O operations from Python Shell to + InteractiveShellInput. + ''' + + def __init__(self, sh): + self.sh = sh + + def write(self, s): + '''To write to a PsuedoFile object. + ''' + self.sh.write(s) + + def writelines(self, lines): + '''To write lines to a PsuedoFile object. + ''' + + for line in lines: + self.write(line) + + def flush(self): + '''To flush a PsuedoFile object. + ''' + pass + + def isatty(self): + '''To determine if PsuedoFile object is a tty or not. + ''' + return True + + +class Shell(code.InteractiveConsole): + "Wrapper around Python that can filter input/output to the shell" + + def __init__(self, root): + code.InteractiveConsole.__init__(self) + self.thread = None + self.root = root + self._exit = False + + def write(self, data): + '''write data to show as output on the screen. + ''' + import functools + Clock.schedule_once(functools.partial(self.root.show_output, data), 0) + + def raw_input(self, prompt=""): + '''To show prompt and get required data from user. + ''' + return self.root.get_input(prompt) + + def runcode(self, _code): + """Execute a code object. + + When an exception occurs, self.showtraceback() is called to + display a traceback. All exceptions are caught except + SystemExit, which is reraised. + + A note about KeyboardInterrupt: this exception may occur + elsewhere in this code, and may not always be caught. The + caller should be prepared to deal with it. + + """ + org_stdout = sys.stdout + sys.stdout = PseudoFile(self) + try: + exec(_code, self.locals) + except SystemExit: + show_message( + 'It\'s not possible to exit from Kivy Designer Python console', + 5, 'error' + ) + except: + self.showtraceback() + + sys.stdout = org_stdout + + def exit(self): + '''To exit PythonConsole. + ''' + self._exit = True + + def interact(self, banner=None): + """Closely emulate the interactive Python console. + + The optional banner argument specify the banner to print + before the first interaction; by default it prints a banner + similar to the one printed by the real Python interpreter, + followed by the current class name in parentheses (so as not + to confuse this with the real interpreter -- since it's so + close!). + + """ + try: + sys.ps1 + except AttributeError: + sys.ps1 = ">>> " + try: + sys.ps2 + except AttributeError: + sys.ps2 = "... " + cprt = 'Type "help", "copyright", "credits" or "license"'\ + ' for more information.' + if banner is None: + self.write("Python %s on %s\n%s\n(%s)\n" % + (sys.version, sys.platform, cprt, + self.__class__.__name__)) + else: + self.write("%s\n" % str(banner)) + more = 0 + while not self._exit: + try: + if more: + prompt = sys.ps2 + else: + prompt = sys.ps1 + try: + line = self.raw_input(prompt) + if line is None: + continue + # Can be None if sys.stdin was redefined + encoding = getattr(sys.stdin, "encoding", None) + if encoding and isinstance(line, bytes): + line = line.decode(encoding) + except EOFError: + self.write("\n") + break + else: + more = self.push(line) + + except KeyboardInterrupt: + self.write("\nKeyboardInterrupt\n") + self.resetbuffer() + more = 0 + + +class InteractiveThread(threading.Thread): + '''Another thread in which main loop of Shell will run. + ''' + def __init__(self, sh): + super(InteractiveThread, self).__init__() + self._sh = sh + self._sh.thread = self + + def run(self): + '''To start main loop of _sh in this thread. + ''' + self._sh.interact() + + +class InteractiveShellInput(CodeInput): + '''Displays Output and sends input to Shell. Emits 'on_ready_to_input' + when it is ready to get input from user. + ''' + + def __init__(self, **kw): + super(InteractiveShellInput, self).__init__(**kw) + self.lexer = PythonConsoleLexer() + + sh = ObjectProperty(None) + '''Instance of :class:`~designer.uix.py_console.Shell` + :data:`sh` is an :class:`~kivy.properties.ObjectProperty` + ''' + + __events__ = ('on_ready_to_input',) + + def __init__(self, **kwargs): + super(InteractiveShellInput, self).__init__(**kwargs) + self.last_line = None + + def keyboard_on_key_down(self, window, keycode, text, modifiers): + '''Override of _keyboard_on_key_down. + ''' + if keycode[0] == 9 and Completer: + # tab, add autocomplete suggestion + txt = self.text[self._cursor_pos:] + + if txt.strip(): + suggestion = Completer(self.sh.locals).complete(txt, 0) + if suggestion: + self.select_text(self._cursor_pos, + self._cursor_pos + len(txt)) + self.delete_selection() + Clock.schedule_once( + partial(self.insert_text, suggestion)) + return False + + elif keycode[0] == 13: + # For enter + self.last_line = self.text[self._cursor_pos:] + self.dispatch('on_ready_to_input') + + return super(InteractiveShellInput, self).keyboard_on_key_down( + window, keycode, text, modifiers) + + def insert_text(self, substring, from_undo=False): + '''Override of insert_text + ''' + if self.cursor_index() < self._cursor_pos: + return + + return super(InteractiveShellInput, self).insert_text(substring, + from_undo) + + def on_ready_to_input(self, *args): + '''Default handler of 'on_ready_to_input' + ''' + pass + + def show_output(self, output): + '''Show output to the user. + ''' + self.text += output + Clock.schedule_once(self._set_cursor_val, 0.1) + + def _set_cursor_val(self, *args): + '''Get last position of cursor where output was added. + ''' + self._cursor_pos = self.cursor_index() + from kivy.animation import Animation + anim = Animation(scroll_y=0, d=0.5) + anim.cancel_all(self.parent) + anim.start(self.parent) + + +class PythonConsole(BoxLayout): + + text_input = ObjectProperty(None) + '''Instance of :class:`~designer.uix.py_console.InteractiveShellInput` + :data:`text_input` is an :class:`~kivy.properties.ObjectProperty` + ''' + + sh = ObjectProperty(None) + '''Instance of :class:`~designer.uix.py_console.Shell` + :data:`sh` is an :class:`~kivy.properties.ObjectProperty` + ''' + + scroll_view = ObjectProperty(None) + '''Instance of :class:`~kivy.uix.scrollview.ScrollView` + :data:`scroll_view` is an :class:`~kivy.properties.ObjectProperty` + ''' + + foreground_color = ListProperty((.5, .5, .5, .93)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(.5, .5, .5, .93)' + ''' + + background_color = ListProperty((0, 0, 0, 1)) + '''This defines the color of the text in the console + + :data:`foreground_color` is an :class:`~kivy.properties.ListProperty`, + Default to '(0, 0, 0, 1)''' + + font_size = NumericProperty(14) + '''Indicates the size of the font used for the console + + :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, + Default to '9' + ''' + + def __init__(self, **kwargs): + super(PythonConsole, self).__init__() + self.sh = Shell(self) + self._thread = InteractiveThread(self.sh) + self._thread.setDaemon(True) + + Clock.schedule_once(self.run_sh) + self._ready_to_input = False + self._exit = False + + def ready_to_input(self, *args): + '''Specifies that PythonConsole is ready to take input from user. + ''' + self._ready_to_input = True + + def run_sh(self, *args): + '''Start Python Shell. + ''' + self._thread.start() + + def show_output(self, data, dt): + '''Show output to user. + ''' + self.text_input.show_output(data) + + def _show_prompt(self, *args): + '''Show prompt to user and asks for input. + ''' + self.text_input.show_output(self.prompt) + + def get_input(self, prompt): + '''Get input from user. + ''' + import time + self.prompt = prompt + Clock.schedule_once(self._show_prompt, 0.1) + while not self._ready_to_input and not self._exit: + time.sleep(0.05) + + self._ready_to_input = False + return self.text_input.last_line + + def exit(self): + '''Exit PythonConsole + ''' + self._exit = True + self.sh.exit() + +if __name__ == '__main__': + runTouchApp(PythonConsole()) diff --git a/designer/uix/sandbox.py b/designer/uix/sandbox.py new file mode 100644 index 0000000..59cd744 --- /dev/null +++ b/designer/uix/sandbox.py @@ -0,0 +1,53 @@ +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import BooleanProperty +from kivy.uix.sandbox import Sandbox, sandbox + + +class DesignerSandbox(Sandbox): + '''DesignerSandbox is subclass of :class:`~kivy.uix.sandbox.Sandbox` + for use with Kivy Designer. It emits on_getting_exeption event + when code running in it will raise some exception. + ''' + + __events__ = ('on_getting_exception',) + error_active = BooleanProperty(False) + '''If True, automatically show the error tab on getting an Exception + ''' + + def __init__(self, **kwargs): + super(DesignerSandbox, self).__init__(**kwargs) + self.exception = None + self.tb = None + self._context['Builder'] = object.__getattribute__(Builder, '_obj') + self._context['Clock'] = object.__getattribute__(Clock, '_obj') + Clock.unschedule(self._clock_sandbox) + Clock.unschedule(self._clock_sandbox_draw) + + def __exit__(self, _type, value, tb): + '''Override of __exit__ + ''' + self._context.pop() + if _type is not None: + return self.on_exception(value, tb=tb) + + def on_exception(self, exception, tb=None): + '''Override of on_exception + ''' + self.exception = exception + self.tb = tb + self.dispatch('on_getting_exception') + return super(DesignerSandbox, self).on_exception(exception, tb) + + def on_getting_exception(self, *args): + '''Default handler for 'on_getting_exception' + ''' + pass + + @sandbox + def _clock_sandbox(self, dt): + pass + + @sandbox + def _clock_sandbox_draw(self, dt): + pass diff --git a/designer/uix/settings.py b/designer/uix/settings.py new file mode 100644 index 0000000..dd20396 --- /dev/null +++ b/designer/uix/settings.py @@ -0,0 +1,710 @@ +from utils.utils import get_designer +from kivy.core.window import Keyboard, Window +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + DictProperty, + ListProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.popup import Popup +from kivy.uix.settings import SettingItem, SettingSpacer +from kivy.uix.togglebutton import ToggleButton +from kivy.uix.widget import Widget + + +Builder.load_string(''' +: + Label: + text: root.options[root.value] if root.value and root.value \ + in root.options else '' + pos: root.pos + font_size: '15sp' + +: + Label: + text: root.value if root.value else 'Click to select' + pos: root.pos + font_size: '15sp' + shorten: True + shorten_from: 'right' + text_size: self.size + halign: 'center' + valign: 'middle' + + +: + item_check: item_check + orientation: 'horizontal' + size_hint_y: None + height: 50 + canvas.before: + Color: + rgba: 1, 1, 1, 0.2 + Rectangle: + pos: self.x, self.top - 1 + size: self.width, 1 + CheckBox: + id: item_check + size_hint_x: 0.1 + active: root.active + on_active: root.on_active(self) + group: root.group + Button: + text: root.item_text + background_normal: 'atlas://data/images/defaulttheme/action_item' + background_down: 'atlas://data/images/defaulttheme/action_item' + size_hint_x: 0.9 + text_size: self.size + shorten: True + valign: 'middle' + on_press: root.item_check._toggle_active() + +: + item_list: item_list + custom_item_layout: custom_item_layout + txt_custom_item: txt_custom_item + orientation: 'vertical' + spacing: '10dp' + Label: + text: root.setting.desc + text_size: self.size + font_size: '11pt' + halign: 'center' + valign: 'middle' + size_hint_y: None + height: '30pt' + ScrollView: + id: i_scroll + GridLayout: + id: item_list + cols: 1 + size_hint_y: None + height: max(i_scroll.height, self.minimum_height) + BoxLayout: + id: custom_item_layout + orientation: 'vertical' + size_hint_y: None + height: dp(30) + pt(15) + Label: + text: 'Custom item:' + size_hint_y: None + height: '15pt' + text_size: self.size + font_size: '11pt' + valign: 'middle' + BoxLayout: + size_hint_y: None + height: '30dp' + orientation: 'horizontal' + TextInput: + id: txt_custom_item + size_hint_x: 0.7 + multiline: False + on_text_validate: root.add_custom_item() + Button: + text: 'Add' + size_hint_x: 0.3 + on_press: root.add_custom_item() + BoxLayout: + size_hint_y: None + height: '40dp' + Button: + text: 'Apply' + on_press: root.on_apply_pressed() + Button: + text: 'Cancel' + on_press: root.dispatch('on_cancel') + +: + Label: + text: root.hint or '' + pos: root.pos + font_size: '15sp' + +: + orientation: 'vertical' + BoxLayout: + GridLayout: + cols: 2 + canvas.after: + Color: + rgb: .3, .3, .3 + Rectangle: + pos: self.right - 1, self.y + size: 1, self.height - 30 + Label: + size_hint: None, None + size: 0, 0 + Label: + size_hint_y: None + height: 30 + text: 'Modifiers' + text_size: self.size + halign: 'center' + CheckBox: + active: root.has_ctrl + on_active: root.has_ctrl = self.active + size_hint_x: 0.2 + Label: + text: 'Ctrl' + text_size: self.size + valign: 'middle' + CheckBox: + active: root.has_shift + on_active: root.has_shift = self.active + size_hint_x: 0.2 + Label: + text: 'Shift' + text_size: self.size + valign: 'middle' + CheckBox: + active: root.has_alt + on_active: root.has_alt = self.active + size_hint_x: 0.2 + Label: + text: 'Alt' + text_size: self.size + valign: 'middle' + + BoxLayout: + orientation: 'vertical' + Label: + size_hint_y: None + height: 30 + text: 'Key' + text_size: self.size + halign: 'center' + Label: + text: root.key + font_size: '20pt' + text_size: self.size + valign: 'middle' + halign: 'center' + Label: + text: root.error + size_hint_y: None + height: 30 + color: [1, 0, 0, 1] + opacity: 1 if self.text else 0 + BoxLayout: + size_hint_y: None + height: '24pt' + Button: + text: 'Cancel' + on_press: root.dispatch('on_cancel') + Button: + text: 'Disable' + on_press: root.dispatch('on_disable') + Button: + text: 'Confirm' + on_press: root.dispatch('on_confirm', root.value) + disabled: not root.valid +''') + + +class SettingDict(SettingItem): + '''Implementation of an option list on top of a :class:`SettingItem`. + Based on SettingOptions, but implemented to use DictProperty. + It is visualized with a :class:`~kivy.uix.label.Label` widget that, when + clicked, will open a :class:`~kivy.uix.popup.Popup` with a + list of options from which the user can select. + ''' + + options = DictProperty({}) + '''Dict with keys to be saved and visible values to the user + + :attr:`options` is a :class:`~kivy.properties.DictProperty` and defaults + to {}. + ''' + + popup = ObjectProperty(None, allownone=True) + '''(internal) Used to store the current popup when it is shown. + + :attr:`popup` is an :class:`~kivy.properties.ObjectProperty` and defaults + to None. + ''' + + def on_panel(self, instance, value): + if value is None: + return + self.bind(on_release=self._create_popup) + + def _set_option(self, instance): + self.value = instance.key + self.popup.dismiss() + + def _create_popup(self, instance): + # create the popup + content = BoxLayout(orientation='vertical', spacing='5dp') + popup_width = min(0.95 * Window.width, dp(500)) + self.popup = popup = Popup( + content=content, title=self.title, size_hint=(None, None), + size=(popup_width, '400dp')) + popup.height = len(self.options) * dp(55) + dp(150) + + # add all the options + content.add_widget(Widget(size_hint_y=None, height=1)) + uid = str(self.uid) + for key in self.options: + state = 'down' if key == self.value else 'normal' + btn = ToggleButton(text=self.options[key], state=state, group=uid) + btn.key = key + btn.bind(on_release=self._set_option) + content.add_widget(btn) + + # finally, add a cancel button to return on the previous panel + content.add_widget(SettingSpacer()) + btn = Button(text='Cancel', size_hint_y=None, height=dp(50)) + btn.bind(on_release=popup.dismiss) + content.add_widget(btn) + + # and open the popup ! + popup.open() + + +class SettingListCheckItem(BoxLayout): + '''Widget with a check button and a label to display each item of list + ''' + item_text = StringProperty('') + '''Item text. :attr:`item_text` is a + :class:`~kivy.properties.StringProperty` and defaults to '' + ''' + + item_check = ObjectProperty(None) + '''Instance of checkbox. :attr:`item_check` is a + :class:`~kivy.properties.ObjectProperty` and defaults to None + ''' + + active = BooleanProperty(False) + '''Alias to the checkbox active state. + :attr:`checked` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False + ''' + + group = StringProperty(None) + '''CheckBox group name. If the CheckBox is in a Group, + it becomes a Radio button. + :attr:`group` is a :class:`~kivy.properties.StringProperty` and + defaults to '' + ''' + + def __init__(self, **kwargs): + super(SettingListCheckItem, self).__init__(**kwargs) + if self.group: + self.item_check.allow_no_selection = False + + def on_active(self, instance, *args): + '''Callback to update the active value + ''' + self.active = instance.active + + +class SettingListContent(BoxLayout): + '''Widget to display SettingList + ''' + setting = ObjectProperty(None) + '''(internal) Reference to the setting SettingList. + :attr:`setting` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None + ''' + + custom_item_layout = ObjectProperty(None) + '''(internal) Widget that allows enter a custom item to the list. + :attr:`custom_item` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None + ''' + + txt_custom_item = ObjectProperty(None) + '''(internal) TextInput with the custom item name. + :attr:`txt_custom_item` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None + ''' + + item_list = ObjectProperty(None) + '''(internal) Widget that shows all items in a list. + :attr:`item_list` is a :class:`~kivy.properties.ObjectProperty` and + defaults to None + ''' + + selected_items = ListProperty([]) + '''List of selected items names. Updated only after clicking on Apply button + :attr:`selected_item` is a :class:`~kivy.properties.ListProperty` and + defaults to [] + ''' + + __events__ = ('on_apply', 'on_cancel',) + + def __init__(self, **kwargs): + super(SettingListContent, self).__init__(**kwargs) + if not self.setting.allow_custom: + self.remove_widget(self.custom_item_layout) + + def show_items(self, *args): + '''Update the list of items + ''' + self.clear_items() + self.setting.items.sort() + for item in self.setting.items: + i = SettingListCheckItem(item_text=item, group=self.setting.group) + if item in self.selected_items: + i.active = True + self.item_list.add_widget(i) + + def clear_items(self, *args): + '''Remove all items from the item_list + ''' + self.item_list.clear_widgets() + + def on_apply_pressed(self, *args): + '''Event handler to Apply button. + Get selected items and update the selected_items. + ''' + self.update_selected_list() + self.dispatch('on_apply', self.selected_items) + + def update_selected_list(self, *args): + '''Update selected_items with the selected items + ''' + self.selected_items = [] + for child in self.item_list.children: + if child.active: + self.selected_items.append(str(child.item_text)) + + def add_custom_item(self, *args): + '''Add a custom item to the list + ''' + txt = self.txt_custom_item.text.strip() + self.txt_custom_item.text = '' + if txt and txt not in self.setting.items: + self.setting.items.append(txt) + self.update_selected_list() + self.show_items() + + def on_cancel(self, *args): + '''Event handler to Cancel button. + ''' + pass + + def on_apply(self, *args): + '''Event handler to Apply button + ''' + pass + + +class SettingList(SettingItem): + '''Implementation of an multi selection list on top of :class:`SettingItem`. + ''' + + items = ListProperty([]) + '''List with default visible items + :attr:`items` is a :class:`~kivy.properties.ListProperty` and defaults + to []. + ''' + + allow_custom = BooleanProperty(False) + '''Allow/disallow a custom item to the list + :attr:`allow_custom` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + group = StringProperty(None) + '''CheckBox group name. If the CheckBox is in a Group, + it becomes a Radio button. + :attr:`group` is a :class:`~kivy.properties.StringProperty` and + defaults to '' + ''' + + popup = ObjectProperty(None, allownone=True) + '''(internal) Used to store the current popup when it is shown. + + :attr:`popup` is an :class:`~kivy.properties.ObjectProperty` and defaults + to None. + ''' + + def on_panel(self, instance, value): + if value is None: + return + self.bind(on_release=self._create_popup) + + def _create_popup(self, instance): + # create the popup + content = SettingListContent(setting=self) + popup_width = min(0.95 * Window.width, 500) + popup_height = min(0.95 * Window.height, 500) + self.popup = popup = Popup( + content=content, title=self.title, size_hint=(None, None), + size=(popup_width, popup_height), auto_dismiss=False) + + content.bind(on_apply=self._set_values, on_cancel=self.popup.dismiss) + selected_items = self.value.split(',') + # update the item list with custom values + for item in selected_items: + item = item.strip() + if item and not item in self.items: + self.items.append(item) + # list of items saved in the property + content.selected_items = selected_items + + content.show_items() + popup.open() + + def _set_values(self, *args): + '''Read items and save them + ''' + selected_items = args[1] + self.value = ','.join(selected_items) + self.popup.dismiss() + + +class SettingShortcutContent(BoxLayout): + + has_ctrl = BooleanProperty(False) + '''Indicates if should listen the Ctrl key + :attr:`has_ctrl` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + has_shift = BooleanProperty(False) + '''Indicates if should listen the Shift key + :attr:`has_shift` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + has_alt = BooleanProperty(False) + '''Indicates if should listen the Alt key + :attr:`has_alt` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + listen_key = BooleanProperty(False) + '''Indicates if should listen the keyboard + :attr:`listen_key` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + valid = BooleanProperty(True) + '''(internal) Indicates if the shortcut is valid + :attr:`valid` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True + ''' + + key = StringProperty('') + '''Indicates the shortcut key + :attr:`key` is a :class:`~kivy.properties.StringProperty` + and defaults to '' + ''' + + config_name = StringProperty('') + '''Indicates the field key on shortcuts.json + :attr:`config_name` is a :class:`~kivy.properties.StringProperty` + and defaults to '' + ''' + + value = StringProperty('') + '''Indicates the shortcut in the String format + :attr:`value` is a :class:`~kivy.properties.StringProperty` + and defaults to '' + ''' + + error = StringProperty('') + '''Error message + :attr:`error` is a :class:`~kivy.properties.StringProperty` + and defaults to '' + ''' + + __events__ = ('on_confirm', 'on_disable', 'on_cancel',) + + def __init__(self, **kwargs): + super(SettingShortcutContent, self).__init__(**kwargs) + self.bind(has_ctrl=self.validate_shortcut) + self.bind(has_shift=self.validate_shortcut) + self.bind(has_alt=self.validate_shortcut) + self.bind(key=self.validate_shortcut) + + def validate_shortcut(self, *args): + '''Check if it's a valid shortcut and if it's being used somewhere else + Updates the error label and return a boolean + ''' + # restore default values + self.valid = True + self.error = '' + + valid = False + if (self.has_ctrl or self.has_shift or self.has_alt) and self.key: + valid = True + + if self.key in ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9' + 'f10', 'f11', 'f12']: + valid = True + + modifier = [] + if self.has_ctrl: + modifier.append('ctrl') + if self.has_shift: + modifier.append('shift') + if self.has_alt: + modifier.append('alt') + modifier.sort() + value = str(modifier) + ' + ' + self.key + # check if shortcut exist + d = get_designer() + if value and value in d.shortcuts.map: + shortcut = d.shortcuts.map[value] + if shortcut[1] != self.config_name: + valid = False + self.error = 'Shortcut already being used at ' + shortcut[1] + + if valid: + self.value = value + self.error = '' + else: + self.value = '' + if not self.error: + self.error = 'This shortcut is not valid' + + self.valid = valid + return valid + + def on_listen_key(self, instance, value, *args): + '''Enable/disable keyboard listener + ''' + if value: + Window.bind(on_key_down=self._on_key_down) + else: + Window.unbind(on_key_down=self._on_key_down) + + def _on_key_down(self, keyboard, key, codepoint, text, modifier, **kwargs): + '''Listen keyboard to create shortcuts. Update class properties + ''' + self.key = Keyboard.keycode_to_string(Window._system_keyboard, key) + if self.key in ['ctrl', 'shift', 'alt']: + self.key = '' + if modifier is None: + modifier = [] + self.has_ctrl = 'ctrl' in modifier + self.has_shift = 'shift' in modifier + self.has_alt = 'alt' in modifier + + return True + + def parse_value(self, value, *args): + '''Parse the value string and update shortcut parameters. + If value is invalid, returns False and set a clean shortcut + :param value: string with formatted shortcut + ''' + try: + mod, key = value.split('+') + key = key.strip() + self.key = key + modifier = eval(mod) + self.has_ctrl = 'ctrl' in modifier + self.has_shift = 'shift' in modifier + self.has_alt = 'alt' in modifier + self.value = value + return True + except: + return False + + def on_cancel(self, *args): + '''Event handler to cancel button + ''' + pass + + def on_disable(self, *args): + '''Event handler to disable button + ''' + self.key = '' + self.has_alt = False + self.has_shift = False + self.has_ctrl = False + self.value = '' + self.error = '' + self.valid = True + + def on_confirm(self, *args): + '''Event handler to confirm button + ''' + pass + + +class SettingShortcut(SettingItem): + '''Implementation of a shortcut listener. + Setting will be stored in the format: + [Modifiers, ...] + keycode(string) + The modifiers are in alphabetical order and separated with a space + All chars is in lowercase + eg + ['ctrl'] + q + ['ctrl', 'shift'] + a + [] + f1 + ''' + + popup = ObjectProperty(None, allownone=True) + '''(internal) Used to store the current popup when it's shown. + + :attr:`popup` is an :class:`~kivy.properties.ObjectProperty` and defaults + to None. + ''' + + hint = StringProperty('') + '''Readable shortcut. Parses value to display on settings panel + :attr:`hint` is an :class:`~kivy.properties.StringProperty` and defaults + to ''. + ''' + + def on_panel(self, instance, value): + if value is None: + return + self.bind(on_release=self._create_popup) + + def _dismiss(self, *largs): + if self.popup: + self.popup.dismiss() + self.popup = None + + def _create_popup(self, instance): + # create popup layout + content = SettingShortcutContent() + content.listen_key = True + content.config_name = self.key + if self.value: + content.parse_value(self.value) + content.bind(on_confirm=self.on_confirm) + content.bind(on_cancel=self._dismiss) + popup_width = min(0.95 * Window.width, dp(500)) + self.popup = popup = Popup( + title='Shortcut - ' + self.title, + content=content, + size_hint=(None, None), + size=(popup_width, '250dp')) + + popup.open() + + def on_confirm(self, instance, value, *args): + '''Callback to shortcut editor confirm + :param instance: instance of shortcut editor(content) + :param value: string with the formatted shortcut + ''' + instance.listen_key = False + self.value = value + self._dismiss() + + def on_cancel(self, instance, *args): + '''Callback to shortcut editor cancel + :param instance: instance of shortcut editor(content) + ''' + instance.listen_key = False + self._dismiss() + + def on_value(self, instance, value): + super(SettingShortcut, self).on_value(instance, value) + mod, key = self.value.split('+') + key = key.strip() + modifier = eval(mod) + hint = ' + '.join(modifier) + ' + ' + key + self.hint = hint.title() diff --git a/designer/uix/xpopup/README.md b/designer/uix/xpopup/README.md new file mode 100644 index 0000000..5ee4bbc --- /dev/null +++ b/designer/uix/xpopup/README.md @@ -0,0 +1,119 @@ +# xpopup +Kivy (http://kivy.org) extensions + +Usefull extensions of the `kivy.uix.popup.Popup` class. + + +Features +======== + +* `XPopup` - extension for the :class:`~kivy.uix.popup.Popup`. Implements methods + for limiting minimum size of the popup and fit popup to the app's window. + For more information, see `xpopup.py`. + +* `XBase` - subclass of `XPopup`, the base class for all popup extensions. + Supports an easy way to add a set of buttons to the popup. Use it to create + your own popup extensions. For more information, see `xbase.py`. + +* `XNotifyBase` - the base class for notifications. Implements the popup with a + label. Use it to create your own notifications. For more information, see + `notification.py`. Subclasses: + + - `XNotification` - a popup that closes automatically after a time limit. + + - `XMessage`, `XError`, `XConfirmation` - templates for often used notifications. + + - `XProgress` - a popup with progress bar. + + - `XLoading` - a popup with a gif image. + +* `XForm` - a simple basis for the UI-forms creation. For more information, + see `form.py`. Subclasses: + + - `XSlider` - a popup with a slider. + + - `XTextInput` - a popup for editing singleline text. + + - `XNotes` - a popup for editing multiline text. + + - `XAuthorization` - a simple authorization form. + +* `XFilePopup` - a popup for file system browsing. For more information, + see `file.py`. Subclasses: + + - `XFileOpen` - a popup for selecting the files. + + - `XFileSave` - a popup for saving file. + + - `XFolder` - a popup for selecting the folders. + + +Demo +==== + +To see a demonstration, you need to perform one of the following: + +* Install `Kivy` library (https://kivy.org/#download) and execute `demo_app.py` + +* Install `Kivy Launcher` on your Android device and copy this package by following these instructions: + https://kivy.org/docs/guide/packaging-android.html#packaging-your-application-for-the-kivy-launcher + (files `main.py` and `android.txt` already in package) + +* Just watch the video: https://youtu.be/UX8gCyEg2J8 + + +Version history +=============== + +* 0.3.1 + + **XFilePopup.filters** - new property, binded to `kivy.uix.filechooser.FileChooser.filters` + + XFolder now shows only folders + +* 0.3.0 + + Added support for localization. For more information, see `tools.py`. + + Added support for custom labels and buttons. For more information, see `tools.py`. + + Added XLoading - shows a 'loading.gif' in the popup + +* 0.2.3 + + XForm.required_fields - new property, list of required fields + + XNotes.lines - new property, default text for the TextInput as list + of strings + + XSlider.title_template - new property, formatted string for display + the slider's value in the title. + + XAuthorization.autologin - now supports 'None' (to hide checkbox) + + XForm.get_value() - fixed bug (exception in Python 3.x) + +* 0.2.2 + + Added support for python 3.x + +* 0.2.1 + + XNotifyBase - added checkbox 'Do not show this message again' + + XProgress.complete() now takes optional parameters: text (for custom + message) and show_time (for custom time-to-close) + + XProgress.autoprogress() - new method which starting infinite progress + increase in the separate thread + +* 0.2 + + Added XFilePopup, XFileOpen, XFileSave, XFolder (classes of the popup for + file system browsing). + + Some minor changes. + +* 0.1 + + Initial release diff --git a/designer/uix/xpopup/__init__.py b/designer/uix/xpopup/__init__.py new file mode 100644 index 0000000..2f7f303 --- /dev/null +++ b/designer/uix/xpopup/__init__.py @@ -0,0 +1,54 @@ +""" +XPopup package. +Usefull extensions of the Popup class + +Package Structure +================= + +Modules: + +* __init__.py: API imports + +* xpopup.py: extension for the :class:`~kivy.uix.popup.Popup` + +* xbase.py: contains the base class for all the xpopup classes (based on + xpopup) + +* notification.py: contains classes of the pop-up notifications + +* form.py: contains classes of the UI-forms + +* file.py: contains classes of the popup for file system browsing + +* tools.py: functions for configure xpopup + +* demo_app.py: contains demo application widget + +* main.py, android.txt - files for `Kivy Launcher` on android + +* xpopup.pot - localization template + +* xpopup_ru.mo - russian language localization file + +""" + +try: + from .tools import configure + from .notification import XNotification, XConfirmation, XMessage, XError,\ + XProgress, XNotifyBase, XLoading + from .form import XSlider, XTextInput, XNotes, XAuthorization, XForm + from .file import XFileOpen, XFileSave, XFolder, XFilePopup + from .xbase import XBase + from .xpopup import XPopup +except: + from tools import configure + from notification import XNotification, XConfirmation, XMessage, XError,\ + XProgress, XNotifyBase, XLoading + from form import XSlider, XTextInput, XNotes, XAuthorization, XForm + from file import XFileOpen, XFileSave, XFolder, XFilePopup + from xbase import XBase + from xpopup import XPopup + +__author__ = 'ophermit' + +__version__ = '0.3.0' diff --git a/designer/uix/xpopup/android.txt b/designer/uix/xpopup/android.txt new file mode 100644 index 0000000..3b0a493 --- /dev/null +++ b/designer/uix/xpopup/android.txt @@ -0,0 +1,3 @@ +title=XPopup Demo +author=ophermit +orientation=landscape \ No newline at end of file diff --git a/designer/uix/xpopup/demo_app.py b/designer/uix/xpopup/demo_app.py new file mode 100644 index 0000000..1ddafc2 --- /dev/null +++ b/designer/uix/xpopup/demo_app.py @@ -0,0 +1,216 @@ +from os.path import expanduser +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.uix.boxlayout import BoxLayout + +# Uncomment this if you want to see a demo of localization. +# from kivy.config import Config +# from os.path import abspath, dirname, join +# Config.add_section('xpopup') +# Config.set('xpopup', 'locale_file', +# join(dirname(abspath(__file__)), 'xpopup_ru.mo')) + +try: + from .tools import * + from .notification import XNotification, XConfirmation, XError, XMessage,\ + XProgress, XLoading + from .form import XSlider, XTextInput, XNotes, XAuthorization + from .file import XFileOpen, XFileSave, XFolder +except: + from tools import * + from notification import XNotification, XConfirmation, XError, XMessage,\ + XProgress, XLoading + from form import XSlider, XTextInput, XNotes, XAuthorization + from file import XFileOpen, XFileSave, XFolder + + +__author__ = 'ophermit' + + +Builder.load_string(''' +#:import metrics kivy.metrics + +: + padding: 5 + spacing: 2 + orientation: 'vertical' + + BoxLayout: + spacing: 2 + + XButton: + text: 'XMessage demo' + on_release: root._on_click('msgbox') + XButton: + text: 'XConfirmation demo' + on_release: root._on_click('confirm') + XButton: + text: 'XError demo' + on_release: root._on_click('error') + XButton: + text: 'XProgress demo' + on_release: root._on_click('progress') + XButton: + text: 'XLoading demo' + on_release: root._loading_demo() + + BoxLayout: + spacing: 2 + + XButton: + text: 'XTextInput demo' + on_release: root._on_click('input') + XButton: + text: 'XNotes demo' + on_release: root._on_click('notes') + XButton: + text: 'XSlider demo' + on_release: root._on_click('slider') + XButton: + text: 'XAuthorization demo' + on_release: root._on_click('login') + + BoxLayout: + spacing: 2 + + XButton: + text:'XOpenFile demo' + on_release: root._open_dialog_demo() + XButton + text: 'XSaveFile demo' + on_release: root._save_dialog_demo() + XButton: + text: 'XFolder demo' + on_release: root._folder_dialog_demo() +''') + + +class XPopupDemo(BoxLayout): + def _on_click(self, sid): + if sid == 'msgbox': + XMessage(text='It could be your Ad', title='XMessage demo') + elif sid == 'error': + XError(text='Don`t panic! Its just the XError demo.') + elif sid == 'confirm': + XConfirmation(text='Do you see a confirmation?', + on_dismiss=self._callback) + elif sid == 'progress': + self._o_popup = XProgress(title='PopupProgress demo', + text='Processing...', max=200) + Clock.schedule_once(self._progress_test, .1) + elif sid == 'input': + XTextInput(title='Edit text', text='I\'m a text', + on_dismiss=self._callback) + elif sid == 'notes': + XNotes(title='Edit notes', on_dismiss=self._callback_notes, + lines=['Text', 'Too many text...', 'Yet another row.']) + elif sid == 'slider': + self._o_popup = XSlider( + min=.4, max=.9, value=.5, size_hint=(.6, .5), + title_template='Slider test, Value: %0.2f', + buttons=['Horizontal', 'Vertical', 'Close'], + on_change=self._slider_value, on_dismiss=self._slider_click) + elif sid == 'login': + XAuthorization( + on_dismiss=self._callback, login='login', + required_fields={'login': 'Login', 'password': 'Password'}, + password='password') + + @staticmethod + def _callback(instance): + if instance.is_canceled(): + return None + + s_message = 'Pressed button: %s\n\n' % instance.button_pressed + + try: + values = instance.values + for kw in values: + s_message += ('<' + kw + '> : ' + str(values[kw]) + '\n') + except AttributeError: + pass + + XNotification( + text=s_message, show_time=3, size_hint=(0.8, 0.4), + title='Results of the popup ( will disappear after 3 seconds ):') + + @staticmethod + def _callback_notes(instance): + if instance.is_canceled(): + return + + s_message = 'Pressed button: %s\n\n' % instance.button_pressed + s_message += str(instance.lines) + + XNotification( + text=s_message, show_time=3, size_hint=(0.8, 0.4), + title='XNotes demo ( will disappear after 3 seconds ):') + + def _progress_test(self, pdt=None): + if self._o_popup.is_canceled(): + return + + self._o_popup.inc() + self._o_popup.text = 'Processing (%d / %d)' %\ + (self._o_popup.value, self._o_popup.max) + if self._o_popup.value < self._o_popup.max: + Clock.schedule_once(self._progress_test, .01) + else: + self._o_popup.complete() + + @staticmethod + def _loading_demo(): + XLoading(buttons=['Close']) + + @staticmethod + def _slider_value(instance, value): + if instance.orientation == 'vertical': + instance.size_hint_x = value + else: + instance.size_hint_y = value + + @staticmethod + def _slider_click(instance): + if instance.button_pressed == 'Horizontal': + instance.orientation = 'horizontal' + instance.size_hint = (.6, .5) + instance.min = .4 + instance.max = .9 + instance.value = .5 + return True + elif instance.button_pressed == 'Vertical': + instance.orientation = 'vertical' + instance.size_hint = (.5, .6) + instance.min = .4 + instance.max = .9 + instance.value = .5 + return True + + def _filepopup_callback(self, instance): + if instance.is_canceled(): + return + s = 'Path: %s' % instance.path + if instance.__class__.__name__ == 'XFileSave': + s += ('\nFilename: %s\nFull name: %s' % + (instance.filename, instance.get_full_name())) + else: + s += ('\nSelection: %s' % instance.selection) + XNotification(title='Pressed button: ' + instance.button_pressed, + text=s, show_time=5) + + def _open_dialog_demo(self): + XFileOpen(on_dismiss=self._filepopup_callback, path=expanduser(u'~'), + multiselect=True) + + def _save_dialog_demo(self): + XFileSave(on_dismiss=self._filepopup_callback, path=expanduser(u'~')) + + def _folder_dialog_demo(self): + XFolder(on_dismiss=self._filepopup_callback, path=expanduser(u'~')) + + +if __name__ == '__main__': + import kivy + # kivy.require('1.9.1') + from kivy.app import runTouchApp + runTouchApp(XPopupDemo()) diff --git a/designer/uix/xpopup/file.py b/designer/uix/xpopup/file.py new file mode 100644 index 0000000..1cc8a25 --- /dev/null +++ b/designer/uix/xpopup/file.py @@ -0,0 +1,397 @@ +""" +Module file.py +============== + +.. versionadded:: 0.2 + +This module contains the class which represents +:class:`~kivy.uix.filechooser.FileChooser` in the popup and some templates +for this class. + +Classes: + +* XFilePopup: represents :class:`~kivy.uix.filechooser.FileChooser` in the + popup. + +* XFolder: :class:`XFilePopup` template for folder selection. + +* XFileOpen: :class:`XFilePopup` template for files selection. + +* XFileSave: :class:`XFilePopup` template for save file. + + +XFilePopup class +================ + +Subclass of :class:`xpopup.XBase`. +This class represents :class:`~kivy.uix.filechooser.FileChooser` in the +popup with following features: + +* label which shows current path + +* buttons which allows you to select view mode (icon/list) + +* button `New folder` + +Usage example:: + + popup = XFilePopup(title='XFilePopup demo', buttons=['Select', 'Close']) + +To set path on the filesystem that this controller should refer to, you can +use :attr:`XFilePopup.path`. The same property you should use to get the +selected path in your callback. + +By default it possible to select only one file. If you need to select multiple +files, set :attr:`XFilePopup.multiselect` to True. + +By default it possible to select files only. If you need to select the +files and folders, set :attr:`XFilePopup.dirselect` to True. + +To obtain selected files and/or folders you need just use +:attr:`XFilePopup.selection`. + +You can add custom preview filters via :attr:`XFilePopup.filters` + +Following example shows how to use properties:: + + def my_callback(instance): + print(u'Path: ' + instance.path) + print(u'Selection: ' + str(instance.selection)) + + from os.path import expanduser + popup = XFilePopup(title='XFilePopup demo', buttons=['Select', 'Close'], + path=expanduser(u'~'), on_dismiss=my_callback, + multiselect=True, dirselect=True) + + +XFolder class +============= + +Subclass of :class:`xpopup.XFilePopup`. +This class is a template with predefined property values for selecting +the folders. He also checks the validity of the selected values. In this case, +selection is allowed only folders. + +By default the folder selection is disabled. It means that the folder cannot be +selected because it will be opened by one click on it. In this case the +selected folder is equal to the current path. + +By the way, the folder selection is automatically enabled when you set +:attr:`XFilePopup.multiselect` to True. But in this case the root folder +cannot be selected. + + +XFileOpen class +=============== + +Subclass of :class:`xpopup.XFilePopup`. +This class is a template with predefined property values for selecting +the files. He also checks the validity of the selected values. In this case, +selection is allowed only files. + + +XFileSave class +=============== + +Subclass of :class:`xpopup.XFilePopup`. +This class is a template with predefined property values for entering name of +file which will be saved. +It contains the :class:`~kivy.uix.textinput.TextInput` widget for input +filename. + +To set a default value in the TextInput widget, use :attr:`XFileSave.filename`. +Also this property can be used to get the file name entered. + +To get full filename (including path), use :meth:`XFileSave.get_full_name`. + +Following example shows how to use properties:: + + def my_callback(instance): + print(u'Path: ' + instance.path) + print(u'Filename: ' + instance.filename) + print(u'Full name: ' + instance.get_full_name()) + + popup = XFileSave(filename='file_to_save.txt', on_dismiss=my_callback) + +""" + +from kivy import metrics +from kivy.factory import Factory +from kivy.lang.builder import Builder +from textwrap import dedent +from kivy.properties import StringProperty, NumericProperty, ListProperty,\ + OptionProperty, BooleanProperty, ObjectProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.textinput import TextInput + +from os import path, makedirs + +try: + from .tools import gettext_ as _ + from .xbase import XBase + from .notification import XError + from .form import XTextInput +except: + from tools import gettext_ as _ + from xbase import XBase + from notification import XError + from form import XTextInput + +__author__ = 'ophermit' + +__all__ = ('XFileSave', 'XFileOpen', 'XFolder') + + +class XFilePopup(XBase): + """XFilePopup class. See module documentation for more information. + """ + + size_hint_x = NumericProperty(1., allownone=True) + size_hint_y = NumericProperty(1., allownone=True) + '''Default size properties for the popup + ''' + + browser = ObjectProperty(None) + '''This property represents the FileChooser object. The property contains + an object after creation :class:`xpopup.XFilePopup` object. + ''' + + path = StringProperty(u'/') + '''Initial path for the browser. + + Binded to :attr:`~kivy.uix.filechooser.FileChooser.path` + ''' + + selection = ListProperty() + '''Contains the selection in the browser. + + Binded to :attr:`~kivy.uix.filechooser.FileChooser.selection` + ''' + + multiselect = BooleanProperty(False) + '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.multiselect` + ''' + + dirselect = BooleanProperty(False) + '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.dirselect` + ''' + + filters = ListProperty() + '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.filters` + ''' + + CTRL_VIEW_ICON = 'icon' + CTRL_VIEW_LIST = 'list' + CTRL_NEW_FOLDER = 'new_folder' + + view_mode = OptionProperty( + CTRL_VIEW_ICON, options=(CTRL_VIEW_ICON, CTRL_VIEW_LIST)) + '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.view_mode` + ''' + + def _get_body(self): + from kivy.lang import Builder + import textwrap + self.browser = Builder.load_string(textwrap.dedent('''\ + FileChooser: + FileChooserIconLayout + FileChooserListLayout + ''')) + + self.browser.path = self.path + self.browser.multiselect = self.multiselect + self.browser.dirselect = self.dirselect + self.browser.filters = self.filters + self.browser.bind(path=self.setter('path'), + selection=self.setter('selection')) + self.bind(view_mode=self.browser.setter('view_mode'), + multiselect=self.browser.setter('multiselect'), + dirselect=self.browser.setter('dirselect'), + filters=self.browser.setter('filters')) + + lbl_path = Factory.XLabel( + text=self.browser.path, valign='top', halign='left', + size_hint_y=None, height=metrics.dp(25)) + self.browser.bind(path=lbl_path.setter('text')) + + layout = BoxLayout(orientation='vertical') + layout.add_widget(self._ctrls_init()) + layout.add_widget(lbl_path) + layout.add_widget(self.browser) + return layout + + def _ctrls_init(self): + btn = Factory.XButton + pnl_controls = BoxLayout(size_hint_y=None, height=metrics.dp(25)) + pnl_controls.add_widget(btn(text=_('Icons'),on_release=self._ctrls_click)) + pnl_controls.add_widget(btn(text=_('List'), on_release=self._ctrls_click)) + pnl_controls.add_widget(btn(text=_('New folder'), on_release=self._ctrls_click)) + return pnl_controls + + def _ctrls_click(self, instance): + try: + value = instance.id + except Exception: + value = instance.text + + if value in self.property('view_mode').options: + self.view_mode = value + elif value == self.CTRL_NEW_FOLDER: + XTextInput(title=_('Input folder name'), + text=_('New folder'), + on_dismiss=self._create_dir) + + def _create_dir(self, instance): + """Callback for create a new folder. + """ + if instance.is_canceled(): + return + new_folder = self.path + path.sep + instance.get_value() + if path.exists(new_folder): + XError(text=_('Folder "%s" is already exist. Maybe you should ' + 'enter another name?') % instance.get_value()) + return True + makedirs(new_folder) + self.browser.property('path').dispatch(self.browser) + + def _filter_selection(self, folders=True, files=True): + """Filter the list of selected objects + + :param folders: if True - folders will be included in selection + :param files: if True - files will be included in selection + """ + if folders and files: + return + + t = [] + for entry in self.selection: + if entry == '..' + path.sep: + pass + elif folders and self.browser.file_system.is_dir(entry): + t.append(entry) + elif files and not self.browser.file_system.is_dir(entry): + t.append(entry) + self.selection = t + + +class XFileSave(XFilePopup): + """XFileSave class. See module documentation for more information. + """ + + BUTTON_SAVE = _('Save') + TXT_ERROR_FILENAME = _('Maybe you should enter a filename?') + + filename = StringProperty(u'') + '''Represents entered file name. Can be used for setting default value. + ''' + + title = StringProperty(_('Save file')) + '''Default title for the popup + ''' + + buttons = ListProperty([BUTTON_SAVE, XFilePopup.BUTTON_CANCEL]) + '''Default button set for the popup + ''' + + def _get_body(self): + txt = TextInput( + text=self.filename, multiline=False, + size_hint_y=None, height=metrics.dp(30) + ) + txt.bind(text=self.setter('filename')) + self.bind(filename=txt.setter('text')) + + layout = super(XFileSave, self)._get_body() + layout.add_widget(txt) + return layout + + def on_selection(self, *largs): + if len(self.selection) == 0: + return + + if not self.browser.file_system.is_dir(self.selection[0]): + self.filename = self.selection[0].split(path.sep)[-1] + + def dismiss(self, *largs, **kwargs): + """Pre-validation before closing. + """ + if self.button_pressed == self.BUTTON_SAVE: + if self.filename == '': + # must be entered filename + XError(text=self.TXT_ERROR_FILENAME) + return self + + return super(XFileSave, self).dismiss(*largs, **kwargs) + + def get_full_name(self): + """Returns full filename (including path) + """ + return self.path + path.sep + self.filename + + +class XFileOpen(XFilePopup): + """XFileOpen class. See module documentation for more information. + """ + + BUTTON_OPEN = _('Open') + TXT_ERROR_SELECTION = _('Maybe you should select a file?') + + title = StringProperty(_('Open file')) + '''Default title for the popup + ''' + + buttons = ListProperty([BUTTON_OPEN, XFilePopup.BUTTON_CANCEL]) + '''Default button set for the popup + ''' + + def dismiss(self, *largs, **kwargs): + """Pre-validation before closing. + """ + if self.button_pressed == self.BUTTON_OPEN: + self._filter_selection(folders=False) + if len(self.selection) == 0: + # files must be selected + XError(text=self.TXT_ERROR_SELECTION) + return self + return super(XFileOpen, self).dismiss(*largs, **kwargs) + + +class XFolder(XFilePopup): + """XFolder class. See module documentation for more information. + """ + + BUTTON_SELECT = _('Select') + TXT_ERROR_SELECTION = _('Maybe you should select a folders?') + + title = StringProperty(_('Choose folder')) + '''Default title for the popup + ''' + + buttons = ListProperty([BUTTON_SELECT, XFilePopup.BUTTON_CANCEL]) + '''Default button set for the popup + ''' + + def __init__(self, **kwargs): + super(XFolder, self).__init__(**kwargs) + # enabling the folder selection if multiselect is allowed + self.filters.append(self._is_dir) + if self.multiselect: + self.dirselect = True + + def _is_dir(self, directory, filename): + return self.browser.file_system.is_dir(path.join(directory, filename)) + + def dismiss(self, *largs, **kwargs): + """Pre-validation before closing. + """ + if self.button_pressed == self.BUTTON_SELECT: + if not self.multiselect: + # setting current path as a selection + self.selection = [self.path] + + self._filter_selection(files=False) + if len(self.selection) == 0: + # folders must be selected + XError(text=self.TXT_ERROR_SELECTION) + return self + return super(XFolder, self).dismiss(*largs, **kwargs) diff --git a/designer/uix/xpopup/form.py b/designer/uix/xpopup/form.py new file mode 100644 index 0000000..9225c9e --- /dev/null +++ b/designer/uix/xpopup/form.py @@ -0,0 +1,498 @@ +""" +Module form.py +============== + +This module contains the base class for GUI forms. Also +subclasses which implement some simple forms. + +Classes: + +* XForm: Base class for all the GUI forms. + +* XSlider: Represents :class:`~kivy.uix.slider.Slider` in popup. + +* XTextInput: Represents a single line TextInput in popup. + +* XNotes: Represents a multiline TextInput in popup. + +* XAuthorization: Represents simple authorization form. + + +XForm class +=========== + +Subclass of :class:`xpopup.XBase`. +The base class for all the GUI forms. Also you can use this class to create +your own forms. To do this you need to implement :meth:`XForm._get_form` in +your subclass:: + + class MyForm(XForm): + def _get_form(self): + layout = BoxLayout() + layout.add_widget(Label(text='Show must go')) + layout.add_widget(Switch(id='main_switch')) + return layout + + popup = MyForm(title='Party switch') + +IMPORTANT: widgets, the values of which must be received after the close of +the form, must have an "id" attribute (see an example above). Current version +supports obtaining values of following widgets: TextInput, Switch, CheckBox, +Slider. + +To obtain this values you need just use :meth:`XForm.get_value`:: + + def my_callback(instance): + print('Switch value: ' + str(instance.get_value('main_switch'))) + + popup = MyForm(title='Party switch', on_dismiss=my_callback) + +If you omit an argument for the :meth:`XForm.get_value`, method returns +a first value from the values dictionary. It is useful if the layout has only +one widget. + +Another way to obtain values is :attr:`XForm.values`:: + + def my_callback(instance): + print('Values: ' + str(instance.values)) + + popup = MyForm(title='Party switch', on_dismiss=my_callback) + +NOTE: The values are available only when the event `on_dismiss` was triggered. + +.. versionadded:: 0.2.3 + You can set list of the required fields using following parameter:: + + popup = XForm(required_fields={ + 'login': 'Login', 'password': 'Password'}) + + Required fields checked when you press any button other than the "Cancel". + + +XSlider class +============= + +Subclass of :class:`xpopup.XForm`. +Represents :class:`~kivy.uix.slider.Slider` in a popup. Properties +:attr:`XSlider.value`, :attr:`XSlider.min`, :attr:`XSlider.max` and +:attr:`XSlider.orientation` is binded to an appropriate properties of +the :class:`~kivy.uix.progressbar.Slider`. + +Also :class:`xpopup.XSlider` has the event 'on_change'. You can bind +your callback to respond on the slider's position change. + +Following example will create a :class:`xpopup.XSlider` object:: + + def my_callback(instance, value): + print('Current volume level: %0.2f' % value) + + popup = XSlider(title='Volume', on_change=my_callback) + +Another example you can see in the demo app module. + +.. versionadded:: 0.2.3 + You can display the slider's value in the title using following parameter:: + + popup = XSlider(title_template='Volume: %0.2f', on_change=my_callback) + + NOTE: Be careful and use the only one formatting operator. + + +XTextInput and XNotes classes +============================= + +Subclasses of :class:`xpopup.XForm`. +Both classes are represents :class:`~kivy.uix.textinput.TextInput` in a popup. +The difference is that the class :class:`xpopup.XTextInput` is used to enter +one text line, and the class :class:`xpopup.XNotes` - for multiline text. + +Following example will create a :class:`~kivy.uix.textinput.TextInput` object +with the specified default text:: + + def my_callback(instance): + print('Your answer: ' + str(instance.get_value())) + + popup = XTextInput(title='What`s your mood?', + text='I`m in the excellent mood!', + on_dismiss=my_callback) + +NOTE: Pressing "Enter" key will simulate pressing "OK" on the popup. Valid for +the :class:`xpopup.XTextInput` ONLY. + +.. versionadded:: 0.2.3 + :class:`xpopup.XNotes` allows you to specify a list of strings as the + default value:: + + def my_callback(instance): + print('Edited text: ' + str(instance.lines)) + + popup = XNotes(lines=['1st row', '2nd row', '3rd row'], + on_dismiss=my_callback) + + +XAuthorization class +==================== + +Subclass of :class:`xpopup.XForm`. +This class is represents a simple authorization form. +Use :attr:`xpopup.XAuthorization.login` and +:attr:`xpopup.XAuthorization.password` to set default values for the login and +password:: + + def my_callback(instance): + print('Auth values: ' + str(instance.values)) + + XAuthorization(on_dismiss=my_callback, login='login', password='password') + +Also, you can set a default value for the checkbox "Login automatically" via +:attr:`xpopup.XAuthorization.autologin`. + + +.. versionadded:: 0.2.3 + Set :attr:`xpopup.XAuthorization.autologin` to None - checkbox will be + hidden. + + +To obtain the specific value, use following ids: + +* login - TextInput for the login + +* password - TextInput for the password + +* autologin - checkbox "Login automatically" + +""" + +from kivy import metrics +from kivy.factory import Factory +from kivy.lang.builder import Builder +from textwrap import dedent +from kivy.properties import NumericProperty, StringProperty, BooleanProperty,\ + ListProperty, OptionProperty, DictProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.checkbox import CheckBox +from kivy.uix.slider import Slider +from kivy.uix.switch import Switch +from kivy.uix.textinput import TextInput +from kivy.uix.widget import Widget +try: + from .tools import gettext_ as _ + from .xbase import XBase + from .notification import XError +except: + from tools import gettext_ as _ + from xbase import XBase + from notification import XError + +__author__ = 'ophermit' + + +class XForm(XBase): + """XForm class. See module documentation for more information. + """ + + buttons = ListProperty([XBase.BUTTON_OK, XBase.BUTTON_CANCEL]) + '''List of button names. Can be used when using custom button sets. + + :attr:`buttons` is a :class:`~kivy.properties.ListProperty` and defaults to + [Base.BUTTON_OK, Base.BUTTON_CANCEL]. + ''' + + values = DictProperty({}) + '''Dict of pairs : . Use it to get the data from + form fields. Supported widget classes: TextInput, Switch, CheckBox, Slider. + + :attr:`values` is a :class:`~kivy.properties.DictProperty` and defaults to + {}, read-only. + ''' + + required_fields = DictProperty({}) + '''Dict of pairs : . Use it to set required fields + in the form. If found blank widget with , its + appears in the error message. Supported widget classes: TextInput. + + .. versionadded:: 0.2.3 + + :attr:`values` is a :class:`~kivy.properties.DictProperty` and defaults to + {}. + ''' + + def __init__(self, **kwargs): + self._ui_form_container = BoxLayout() + super(XForm, self).__init__(**kwargs) + self._ui_form_container.add_widget(self._get_form()) + + def _get_body(self): + return self._ui_form_container + + def _on_click(self, instance): + """Pre-dismiss method. + Gathers widget values. Checks the required fields. + Ignores it all if the "Cancel" was pressed. + """ + try: + value = instance.id + except Exception: + value = instance.text + + if value != self.BUTTON_CANCEL: + self.values = {} + required_errors = [] + for widget in self._ui_form_container.walk(restrict=True): + t_id = widget.id + if t_id is not None: + if isinstance(widget, TextInput): + t_value = widget.text + if self.required_fields and\ + t_id in self.required_fields.keys()\ + and not t_value: + required_errors.append(self.required_fields[t_id]) + elif isinstance(widget, Switch)\ + or isinstance(widget, CheckBox): + t_value = widget.active + elif isinstance(widget, Slider): + t_value = widget.value + else: + t_value = 'Not supported: ' + widget.__class__.__name__ + + self.values[t_id] = t_value + + if required_errors: + XError(text=_('Following fields are required:\n') + + ', '.join(required_errors)) + return + + super(XForm, self)._on_click(instance) + + def _get_form(self): + raise NotImplementedError + + def get_value(self, ps_id=''): + """Obtain values from the widgets on the form. + + :param ps_id: widget id (optional) + If omit, method returns a first value from the values dictionary + :return: value of widget with specified id + """ + assert len(self.values) > 0 + if ps_id == '': + return self.values.get(list(self.values.keys())[0]) + else: + return self.values.get(ps_id) + + +class XSlider(XForm): + """XSlider class. See module documentation for more information. + + :Events: + `on_change`: + Fired when the :attr:`~kivy.uix.slider.Slider.value` is changed. + """ + __events__ = ('on_change', ) + + buttons = ListProperty([XForm.BUTTON_CLOSE]) + '''Default button set for the popup + ''' + + min = NumericProperty(0.) + max = NumericProperty(1.) + value = NumericProperty(.5) + orientation = OptionProperty( + 'horizontal', options=('vertical', 'horizontal')) + '''Properties that are binded to the same slider properties. + ''' + + title_template = StringProperty('') + '''Template for the formatted title. Use it if you want display the + slider's value in the title. See module documentation for more information. + + .. versionadded:: 0.2.3 + + :attr:`title_template` is a :class:`~kivy.properties.StringProperty` and + defaults to ''. + ''' + + def __init__(self, **kwargs): + super(XSlider, self).__init__(**kwargs) + self._update_title() + + def _update_title(self): + if self.title_template: + self.title = self.title_template % self.value + + def _get_form(self): + slider = Slider( + min=self.min, max=self.max, + value=self.value, orientation=self.orientation, + ) + slider.bind(value=self.setter('value')) + bind = self.bind + bind(min=slider.setter('min')) + bind(max=slider.setter('max')) + bind(value=slider.setter('value')) + bind(orientation=slider.setter('orientation')) + + def on_value(self, instance, value): + self._update_title() + self.dispatch('on_change', value) + + def on_change(self, value): + pass + + +class XTextInput(XForm): + """XTextInput class. See module documentation for more information. + """ + + text = StringProperty('') + '''This property represents default text for the TextInput. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to + ''. + ''' + + def _get_form(self): + layout = BoxLayout(orientation='vertical', spacing=5) + text_input = TextInput( + multiline=False, text=self.text, + on_text_validate=self._on_text_validate, + # DON`T UNCOMMENT OR FOUND AND FIX THE ISSUE + # if `focus` set to `True` - TextInput will be + # inactive to edit + # focus=True, + size_hint_y=None, height=metrics.dp(33)) + layout.add_widget(Widget()) + layout.add_widget(text_input) + layout.add_widget(Widget()) + return layout + + def _on_text_validate(self, instance): + self._on_click(Button()) + + +class XNotes(XForm): + """XNotes class. See module documentation for more information. + """ + + size_hint_x = NumericProperty(.9, allownone=True) + size_hint_y = NumericProperty(.9, allownone=True) + '''Default size properties for the popup + ''' + + text = StringProperty('') + '''This property represents default text for the TextInput. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to + ''. + ''' + + lines = ListProperty() + '''This property represents default text for the TextInput as list of + strings. + + .. versionadded:: 0.2.3 + + :attr:`lines` is a :class:`~kivy.properties.ListProperty` and defaults to + []. + ''' + + def _get_form(self): + t = self.text + if self.lines: + t = '\n'.join(self.lines) + return TextInput(text=t) + + def _on_click(self, instance): + try: + value = instance.id + except Exception: + value = instance.text + + if value != self.BUTTON_CANCEL: + for widget in self._ui_form_container.walk(restrict=True): + if widget.id == 'text': + self.lines = widget.text.split('\n') + + super(XForm, self)._on_click(instance) + + +class XAuthorization(XForm): + """XAuthorization class. See module documentation for more information. + """ + BUTTON_LOGIN = _('Login') + + login = StringProperty(u'') + '''This property represents default text in the `login` TextInput. + For initialization only. + + :attr:`login` is a :class:`~kivy.properties.StringProperty` and defaults to + ''. + ''' + + password = StringProperty(u'') + '''This property represents default text in the `password` TextInput. + For initialization only. + + :attr:`password` is a :class:`~kivy.properties.StringProperty` and defaults + to ''. + ''' + + autologin = BooleanProperty(False, allownone=True) + '''This property represents default value for the CheckBox + "Login automatically". For initialization only. + + .. versionadded:: 0.2.3 + + If None - checkbox is hidden. + + :attr:`autologin` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False. + ''' + + title = StringProperty(_('Authorization')) + '''Default title for the popup + ''' + + buttons = ListProperty([BUTTON_LOGIN, XForm.BUTTON_CANCEL]) + '''Default button set for the popup + ''' + + size_hint_x = NumericProperty(None, allownone=True) + size_hint_y = NumericProperty(None, allownone=True) + width = NumericProperty(metrics.dp(350)) + height = NumericProperty(metrics.dp(200)) + '''Default size properties for the popup + ''' + + def _get_form(self): + layout = BoxLayout(orientation='vertical', spacing=5) + layout.add_widget(Widget()) + lbl = Factory.XLabel + + pnl = BoxLayout(size_hint_y=None, height=metrics.dp(28), spacing=5) + pnl.add_widget( + lbl(text=_('Login:'), halign='right', size_hint_x=None, width=metrics.dp(80)) + ) + pnl.add_widget(TextInput(multiline=False, font_size=metrics.sp(14), text=self.login)) + layout.add_widget(pnl) + + pnl = BoxLayout(size_hint_y=None, height=metrics.dp(28), spacing=5) + pnl.add_widget( + lbl(text=_('Password:'), halign='right', size_hint_x=None, width=metrics.dp(80)) + ) + pnl.add_widget( + TextInput(multiline=False, font_size=14, password=True, text=self.password) + ) + layout.add_widget(pnl) + + if self.autologin is not None: + pnl = BoxLayout(size_hint_y=None, height=metrics.dp(28), spacing=5) + pnl.add_widget( + CheckBox(size_hint_x=None, width=metrics.dp(80), active=self.autologin) + ) + pnl.add_widget(lbl(text=_('Login automatically'), halign='left')) + layout.add_widget(pnl) + + layout.add_widget(Widget()) + return layout diff --git a/designer/uix/xpopup/main.py b/designer/uix/xpopup/main.py new file mode 100644 index 0000000..b256132 --- /dev/null +++ b/designer/uix/xpopup/main.py @@ -0,0 +1,8 @@ +import kivy +kivy.require('1.9.1') +from kivy.base import runTouchApp + +__author__ = 'ophermit' + +from demo_app import XPopupDemo +runTouchApp(XPopupDemo()) diff --git a/designer/uix/xpopup/notification.py b/designer/uix/xpopup/notification.py new file mode 100644 index 0000000..73db70d --- /dev/null +++ b/designer/uix/xpopup/notification.py @@ -0,0 +1,377 @@ +""" +Module notification.py +====================== + +This module contains the base class for all notifications. Also +subclasses which implement some the base notifications functionality. + +Classes: + +* XNotifyBase: Base class for all notifications. + +* XMessage: Notification with predefined button set (['Ok']) + +* XError: XMessage with predefined title + +* XConfirmation: Notification with predefined button set (['Yes', 'No']) + +* XNotification: Notification without buttons. Can autoclose after few + seconds. + +* XProgress: Notification with ProgressBar + + +XNotifyBase class +================= + +Subclass of :class:`xpopup.XBase`. +The base class for all notifications. Also you can use this class to create +your own notifications:: + + XNotifyBase(title='You have a new message!', text='What can i do for you?', + buttons=['Open it', 'Mark as read', 'Remind me later']) + +Or that way:: + + class MyNotification(XNotifyBase): + buttons = ListProperty(['Open it', 'Mark as read', 'Remind me later']) + title = StringProperty('You have a new message!') + text = StringProperty('What can i do for you?') + popup = MyNotification() + +.. note:: :class:`XMessage` and :class:`XError` classes were created in a + similar manner. Actually, it is just a subclasses with predefined default + values. + +Similarly for the :class:`XConfirmation` class. The difference - it has +:meth:`XConfirmation.is_confirmed` which checks which button has been +pressed:: + + def my_callback(instance): + if instance.is_confirmed(): + print('You are agree') + else: + print('You are disagree') + popup = XConfirmation(text='Do you agree?', on_dismiss=my_callback) + +.. versionadded:: 0.2.1 + :attr:`XNotifyBase.dont_show_value` + :attr:`XNotifyBase.dont_show_text` + See documentation below. + + +XNotification class +=================== + +Subclass of :class:`xpopup.XNotifyBase`. +This is an extension of :class:`XNotifBase`. It has no buttons and can +be closed automatically:: + + XNotification(text='This popup will disappear after 3 seconds', + show_time=3) + +If you don't want that, you can ommit :attr:`XNotification.show_time` and +use :meth:`XNotification.dismiss`:: + + popup = XNotification(text='To close it, use the Force, Luke!') + def close_popup(): + popup.dismiss() + + +XProgress class +=============== + +Subclass of :class:`xpopup.XNotifyBase`. +Represents :class:`~kivy.uix.progressbar.ProgressBar` in a popup. Properties +:attr:`XProgress.value` and :attr:`XProgress.max` is binded to an +appropriate properties of the :class:`~kivy.uix.progressbar.ProgressBar`. + +How to use it? Following example will create a `XProgress` object which has +a title, a text message, and it displays 50% of progress:: + + popup = XProgress(value=50, text='Request is being processed', + title='Please wait') + +There are two ways to update the progress line. +First way: simply assign a value to indicate the current progress:: + + # update progress to 80% + popup.value = 80 + +Second way: use :meth:`XProgress.inc`. This method will increase current +progress by specified number of units:: + + # reset progress + popup.value = 0 + # increase by 10 units + popup.inc(10) + # increase by 1 unit + popup.inc() + +By the way, if the result value exceeds the maximum value, this method is +"looping" the progress. For example:: + + # init progress + popup = XProgress(value=50) + # increase by 60 units - will display 10% of the progress + popup.inc(60) + +This feature is useful when it is not known the total number of iterations. +Also in this case, a useful method is :meth:`XProgress.complete`. It sets the +progress to 100%, hides the button(s) and automatically closes the popup +after 2 seconds:: + + # init progress + popup = XProgress(value=50) + # complete the progress + popup.complete() + +.. versionadded:: 0.2.1 + You can change the text and time-to-close using following parameters:: + + popup.complete(text='', show_time=0) + + In that case, the popup will be closed immediately. + +.. versionadded:: 0.2.1 + :meth:`XProgress.autoprogress` starts infinite progress increase in the + separate thread i.e. you don't need to increase it manually. Will be + stopped automatically when the :meth:`XProgress.complete` or + :meth:`XProgress.dismiss` is called. + + +XLoading class +=============== + +.. versionadded:: 0.3.0 + +Subclass of :class:`xpopup.XBase`. +Shows a 'loading.gif' in the popup. + +Following example will create a `XLoading` object using custom title and +image:: + + popup = XLoading(title='Your_title', gif='/your_path_to/loading.gif') + +""" + +from os.path import join +from kivy import metrics, kivy_data_dir +from kivy.clock import Clock +from kivy.factory import Factory +from kivy.properties import ListProperty, StringProperty, NumericProperty,\ + BoundedNumericProperty, BooleanProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.checkbox import CheckBox +from kivy.uix.image import Image +from kivy.uix.progressbar import ProgressBar +try: + from .tools import gettext_ as _ + from .xbase import XBase +except: + from tools import gettext_ as _ + from xbase import XBase + +__author__ = 'ophermit' + + +class XNotifyBase(XBase): + """XNotifyBase class. See module documentation for more information. + """ + + text = StringProperty('') + '''This property represents text on the popup. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to + ''. + ''' + + dont_show_text = StringProperty(_('Do not show this message again')) + '''Use this property if you want to use custom text instead of + 'Do not show this message'. + + :attr:`text` is a :class:`~kivy.properties.StringProperty`. + ''' + + dont_show_value = BooleanProperty(None, allownone=True) + '''This property represents a state of checkbox 'Do not show this message'. + To enable checkbox, set this property to True or False. + + .. versionadded:: 0.2.1 + + :attr:`dont_show_value` is a :class:`~kivy.properties.BooleanProperty` and + defaults to None. + ''' + + def __init__(self, **kwargs): + self._message = Factory.XLabel(text=self.text) + self.bind(text=self._message.setter('text')) + super(XNotifyBase, self).__init__(**kwargs) + + def _get_body(self): + if self.dont_show_value is None: + return self._message + else: + pnl = BoxLayout(orientation='vertical') + pnl.add_widget(self._message) + + pnl_cbx = BoxLayout( + size_hint_y=None, height=metrics.dp(35), spacing=5) + cbx = CheckBox( + active=self.dont_show_value, size_hint_x=None, + width=metrics.dp(50)) + cbx.bind(active=self.setter('dont_show_value')) + pnl_cbx.add_widget(cbx) + pnl_cbx.add_widget( + Factory.XLabel(text=self.dont_show_text, halign='left')) + + pnl.add_widget(pnl_cbx) + return pnl + + +class XNotification(XNotifyBase): + """XNotification class. See module documentation for more information. + """ + + show_time = BoundedNumericProperty(0, min=0, max=100, errorvalue=0) + '''This property determines if the pop-up is automatically closed + after `show_time` seconds. Otherwise use :meth:`XNotification.dismiss` + + :attr:`show_time` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + ''' + + def open(self, *largs): + super(XNotification, self).open(*largs) + if self.show_time > 0: + Clock.schedule_once(self.dismiss, self.show_time) + + +class XMessage(XNotifyBase): + """XMessageBox class. See module documentation for more information. + """ + + buttons = ListProperty([XNotifyBase.BUTTON_OK]) + '''Default button set for class + ''' + + +class XError(XMessage): + """XErrorBox class. See module documentation for more information. + """ + + title = StringProperty(_('Something went wrong...')) + '''Default title for class + ''' + + +class XConfirmation(XNotifyBase): + """XConfirmation class. See module documentation for more information. + """ + + buttons = ListProperty([XNotifyBase.BUTTON_YES, XNotifyBase.BUTTON_NO]) + '''Default button set for class + ''' + + title = StringProperty(_('Confirmation')) + '''Default title for class + ''' + + def is_confirmed(self): + """Check the `Yes` event + + :return: True, if the button 'Yes' has been pressed + """ + return self.button_pressed == self.BUTTON_YES + + +class XProgress(XNotifyBase): + """XProgress class. See module documentation for more information. + """ + + buttons = ListProperty([XNotifyBase.BUTTON_CANCEL]) + '''Default button set for class + ''' + + max = NumericProperty(100.) + value = NumericProperty(0.) + '''Properties that are binded to the same ProgressBar properties. + ''' + + def __init__(self, **kwargs): + self._complete = False + self._progress = ProgressBar(max=self.max, value=self.value) + self.bind(max=self._progress.setter('max')) + self.bind(value=self._progress.setter('value')) + super(XProgress, self).__init__(**kwargs) + + def _get_body(self): + layout = BoxLayout(orientation='vertical') + layout.add_widget(super(XProgress, self)._get_body()) + layout.add_widget(self._progress) + return layout + + def complete(self, text=_('Complete'), show_time=2): + """ + Sets the progress to 100%, hides the button(s) and automatically + closes the popup. + + .. versionchanged:: 0.2.1 + Added parameters 'text' and 'show_time' + + :param text: text instead of 'Complete', optional + :param show_time: time-to-close (in seconds), optional + """ + self._complete = True + n = self.max + self.value = n + self.text = text + self.buttons = [] + Clock.schedule_once(self.dismiss, show_time) + + def inc(self, pn_delta=1): + """ + Increase current progress by specified number of units. + If the result value exceeds the maximum value, this method is + "looping" the progress + + :param pn_delta: number of units + """ + self.value += pn_delta + if self.value > self.max: + # create "loop" + self.value = self.value % self.max + + def autoprogress(self, pdt=None): + """ + .. versionadded:: 0.2.1 + + Starts infinite progress increase in the separate thread + """ + if self._window and not self._complete: + self.inc() + Clock.schedule_once(self.autoprogress, .01) + + +class XLoading(XBase): + """XLoading class. See module documentation for more information. + + .. versionadded:: 0.3.0 + """ + gif = StringProperty(join(kivy_data_dir, 'images', 'image-loading.gif')) + '''Represents a path to an image. + ''' + + title = StringProperty(_('Loading...')) + '''Default title for class + ''' + + size_hint_x = NumericProperty(None, allownone=True) + size_hint_y = NumericProperty(None, allownone=True) + width = NumericProperty(metrics.dp(350)) + height = NumericProperty(metrics.dp(200)) + '''Default size properties for the popup + ''' + + def _get_body(self): + return Image(source=self.gif, anim_delay=.1) diff --git a/designer/uix/xpopup/screenshot.png b/designer/uix/xpopup/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca0debe3536caf27051526184e2229cceaf6a35 GIT binary patch literal 10728 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, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2016-09-13 14:06+FLE Daylight Time\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: ENCODING\n" +"Generated-By: pygettext.py 1.5\n" + + +#: file.py:216 +msgid "Icons" +msgstr "" + +#: file.py:219 +msgid "List" +msgstr "" + +#: file.py:222 file.py:231 +msgid "New folder" +msgstr "" + +#: file.py:230 +msgid "Input folder name" +msgstr "" + +#: file.py:241 +msgid "Folder \"%s\" is already exist. Maybe you should enter another name?" +msgstr "" + +#: file.py:271 +msgid "Save" +msgstr "" + +#: file.py:272 +msgid "Maybe you should enter a filename?" +msgstr "" + +#: file.py:278 +msgid "Save file" +msgstr "" + +#: file.py:324 +msgid "Open" +msgstr "" + +#: file.py:325 +msgid "Maybe you should select a file?" +msgstr "" + +#: file.py:327 +msgid "Open file" +msgstr "" + +#: file.py:351 +msgid "Select" +msgstr "" + +#: file.py:352 +msgid "Maybe you should select a folders?" +msgstr "" + +#: file.py:354 +msgid "Choose folder" +msgstr "" + +#: form.py:255 +msgid "" +"Following fields are required:\n" +msgstr "" + +#: form.py:409 +msgid "Login" +msgstr "" + +#: form.py:439 +msgid "Authorization" +msgstr "" + +#: form.py:460 +msgid "Login:" +msgstr "" + +#: form.py:468 +msgid "Password:" +msgstr "" + +#: form.py:480 +msgid "Login automatically" +msgstr "" + +#: notification.py:174 +msgid "Do not show this message again" +msgstr "" + +#: notification.py:248 +msgid "Something went wrong..." +msgstr "" + +#: notification.py:261 +msgid "Confirmation" +msgstr "" + +#: notification.py:299 +msgid "Complete" +msgstr "" + +#: notification.py:346 +msgid "Loading..." +msgstr "" + +#: xbase.py:103 +msgid "Ok" +msgstr "" + +#: xbase.py:104 +msgid "Cancel" +msgstr "" + +#: xbase.py:105 +msgid "Yes" +msgstr "" + +#: xbase.py:106 +msgid "No" +msgstr "" + +#: xbase.py:107 +msgid "Close" +msgstr "" + diff --git a/designer/uix/xpopup/xpopup.py b/designer/uix/xpopup/xpopup.py new file mode 100644 index 0000000..5080ce8 --- /dev/null +++ b/designer/uix/xpopup/xpopup.py @@ -0,0 +1,122 @@ +""" +https://github.com/kivy-garden/garden.xpopup.git + +XPopup class +============ + + The :class:`XPopup` is the extension for the :class:`~kivy.uix.popup.Popup` +class. Implements methods for limiting minimum size of the popup and fit popup +to the app's window. + + By default, the minimum size is not set. It can be changed via setting value +of an appropriate properties (see documentation below). + +.. warning:: + * Normalization is applied once (when using the :meth:`XPopup.open`) + + * The first normalization is performed on the minimum size, and then - fit + to app's window. In this case, if the specified minimum size is greater + than the size of the app's window - it will be ignored. + + +Examples +-------- +This example creates a simple popup with specified minimum size:: + + popup = XPopup(size_hint=(.4, .3), min_width=400, min_height=300) + popup.open() + +If actual size of popup less than minimum size, :attr:`size_hint` will be +normalized. For example: assume the size of app window is 500x500, in this case +popup will had size 200x150. But we set a minimum size, so :attr:`size_hint` +for this popup will be recalculated and set to (.8, .6) + + By default, if you set the popup size in pixels, which will exceed the size +of the app window, the popup will go out of app's window bounds. +If you don't want that, you can set :attr:`fit_to_window` to True (popup will +be normalized to the size of the app window):: + + popup = XPopup(size=(1000, 1000), size_hint=(None, None), + fit_to_window=True) + popup.open() +""" + +from kivy.properties import NumericProperty, BooleanProperty +from kivy.uix.popup import Popup + +__author__ = 'ophermit' + + +class XPopup(Popup): + """XPopup class. See module documentation for more information. + """ + + min_width = NumericProperty(None, allownone=True) + '''Minimum width of the popup. + + :attr:`min_width` is a :class:`~kivy.properties.NumericProperty` and + defaults to None. + ''' + + min_height = NumericProperty(None, allownone=True) + '''Minimum height of the popup. + + :attr:`min_height` is a :class:`~kivy.properties.NumericProperty` and + defaults to None. + ''' + + fit_to_window = BooleanProperty(False) + '''This property determines if the pop-up larger than app window is + automatically fit to app window. + + :attr:`fit_to_window` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False. + ''' + + def _norm_value(self, pn_value, pn_hint, pn_min, pn_max): + """Normalizes one value + + :param pn_value: original value (width or height) + :param pn_hint: original `size hint` (x or y) + :param pn_min: minimum limit for the value + :param pn_max: maximum limit for the value + :return: tuple of normalized parameters (value, `size hint`) + """ + norm_hint = pn_hint + norm_value = pn_value + + if pn_min is not None and norm_value < pn_min: + norm_value = pn_min + norm_hint = pn_min / float(pn_max) + + if self.fit_to_window: + if norm_value > pn_max: + norm_value = pn_max + if norm_hint is not None and norm_hint > 1: + norm_hint = 1. + + return norm_value, norm_hint + + def _norm_size(self): + """Applies the specified parameters + """ + win_size = self.get_root_window().size[:] + popup_size = self.size[:] + + norm_x = self._norm_value(popup_size[0], self.size_hint_x, + self.min_width, win_size[0]) + norm_y = self._norm_value(popup_size[1], self.size_hint_y, + self.min_height, win_size[1]) + self.width = norm_x[0] + self.height = norm_y[0] + self.size_hint = (norm_x[1], norm_y[1]) + + # DON`T REMOVE OR FOUND AND FIX THE ISSUE + # if `size_hint` is not specified we need to recalculate position + # of the popup + if (norm_x[1], norm_y[1]) == (None, None) and self.size != popup_size: + self.property('size').dispatch(self) + + def open(self, *largs): + super(XPopup, self).open(*largs) + self._norm_size() diff --git a/designer/uix/xpopup/xpopup_ru.mo b/designer/uix/xpopup/xpopup_ru.mo new file mode 100644 index 0000000000000000000000000000000000000000..702421ebdaa02089e290900bb5c8ac1988c8834d GIT binary patch literal 1962 zcmb7@&ud&&6vwYM{)&xgE0$Q%OQ6L5|Lb)mRiR_Sd4k_#{2snTR$HH)JCy2S*#qx}d$V;Pyo>cy@Lu=~+zIpWaX1VI;Yldk zSE1P7gb%^*U>E!qcEjJY^FQEj)_*~X_u!pM^}#(*baL7HH7I^>Lrm3$j5APlFT(ra zM;Yg{{VUn}8z}nM;U2gF`Ks@EJPCh-kH8&>NnO2gFWe6wfP-)sd>K9p3-D=p2G-y* z9DxTYTKrGL{qTIYz6zz^Z76m82oJzt;Qg?NQBvkC&>Gsl$trE=P%ui&0%C#Zs&%O&kXkMh6o?sjQB81>LtV z?$aCwVbuhMDQ!-b;$%R-6imHg^i)(6|5{kkrkt3n4p1z@axiJ~A`wRQQh7p;mrPhd zq-u24yjd$%O(EB`7KGtc zx|dV`l?`!2ljd@ZNo+zhmgvCSi6d$1A4nA6`R^1aVxC zs)d{y4c;>TksgftP---qG&Gua)ajP4SEIDNS4}K8ld#!;Vya?tIx(k`LzR$gcwUbc zgKBJ&!>^n;(*JC?vLD(d*RUNoZCiHLF1q(@%grhK6`MC~({9*BWiPoIdrfS#UB{$h z7j4^i?6RDF#$w$~yBXJTv#IH~oVD>3?~bxxvs}Sx$u@~{-LCk9mTf9KZ=2eRIkI+@ zC`%&mX0`i3lG5%S&N}v*dw z)i!!7sf7kUd={GhX||TJC%Klr;pXznE^NBEi-aKgH1D>X(>}pD-@FF0Z8vK-+>EA9 ze@at*iJKjvz09ipoS+@@S+^~^Ce6X2MdF)5|E1Rp2(^4q)5=Duu{| z`y<5nua95BF;@~CK=^L z@@2Z+EInF4<(%ZtHF9$@{@b@?{AEzK3-~u_vt(tj5W3SZ4?j-i!+WLi!%GTU_m}xG G@7do!o=Ft| literal 0 HcmV?d00001 diff --git a/designer/utils/__init__.py b/designer/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/designer/utils/constants.py b/designer/utils/constants.py new file mode 100644 index 0000000..43f720d --- /dev/null +++ b/designer/utils/constants.py @@ -0,0 +1,8 @@ +import os + + +DIR_NEW_TEMPLATE = 'new_templates' +NEW_PROJECT_DIR_NAME_PREFIX = 'designer_' +NEW_TEMPLATE_IMAGE_PATH = os.path.join(DIR_NEW_TEMPLATE, 'images') +DIR_PROFILES = 'profiles' +DESIGNER_CONFIG_FILE_NAME = 'config.ini' diff --git a/designer/utils/toolbox_widgets.py b/designer/utils/toolbox_widgets.py new file mode 100644 index 0000000..9b7a3e8 --- /dev/null +++ b/designer/utils/toolbox_widgets.py @@ -0,0 +1,48 @@ +#: Describe the widgets to show in the toolbox, +#: and anything else needed for the +#: designer. The base is a list, because python dict don't preserve the order. +#: The first field is the name used for Factory. +#: The second field represent a category name +#: The third field represents initial widget values +#: The fourth field are extra parameters used to display the widget while dragging +toolbox_widgets = [ + ('Button', 'base', {'text': 'Button'}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('Carousel', 'base'), + ('CheckBox', 'base', {'active': True}, {'size_hint': (None, None), 'size': ('50sp', '50sp')}), + ('Image', 'base', {'source': 'data/logo/kivy-icon-64.png'}, {'size_hint': (None, None), 'size': (64, 64)}), + ('Label', 'base', {'text': 'Label'}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('ProgressBar', 'base', {}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('Screen', 'base'), + ('ScreenManager', 'base'), + ('Slider', 'base', {}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('Switch', 'base', {'active': True}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('TextInput', 'base', {'text': 'Text Content'}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('ToggleButton', 'base', {'text': 'Toggle Button'}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('Video', 'base'), + + ('AnchorLayout', 'layout'), + ('BoxLayout', 'layout'), + ('FloatLayout', 'layout'), + ('GridLayout', 'layout', {'cols': 2}), + ('PageLayout', 'layout'), + ('RelativeLayout', 'layout'), + ('ScatterLayout', 'layout'), + ('StackLayout', 'layout'), + + ('ActionButton', 'complex'), + ('ActionPrevious', 'complex', {}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('Bubble', 'complex', {}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('DropDown', 'complex'), + ('FileChooserListView', 'complex', {}, {'size_hint': (None, None), 'size': ('200sp', '160sp')}), + ('FileChooserIconView', 'complex', {}, {'size_hint': (None, None), 'size': ('200sp', '160sp')}), + ('ListView', 'complex', {'item_strings': ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']}, {'size_hint': (None, None), 'size': ('100sp', '140sp')}), + ('Popup', 'complex'), + ('Spinner', 'complex', {'text': 'Spinner'}, {'size_hint': (None, None), 'size': ('150sp', '40sp')}), + ('TabbedPanel', 'complex'), + ('VideoPlayer', 'complex', {}, {'size_hint': (None, None), 'size': ('200sp', '150sp')}), + + ('ScrollView', 'behavior'), + ('Scatter', 'behavior'), + ('StencilView', 'behavior'), +] + diff --git a/designer/utils/utils.py b/designer/utils/utils.py new file mode 100644 index 0000000..8e328e1 --- /dev/null +++ b/designer/utils/utils.py @@ -0,0 +1,243 @@ +'''This file contains a few functions which are required by more than one + module of Kivy Designer. +''' +import functools +import inspect +import os +import sys + +from kivy.app import App +from kivy.event import EventDispatcher +from kivy.factory import Factory +from kivy.properties import BooleanProperty, ListProperty, StringProperty +from kivy.uix.label import Label +from kivy.uix.popup import Popup +from kivy.uix.widget import Widget +from files import init_file + +class FakeSettingList(EventDispatcher): + '''Fake Kivy Setting to use SettingList + ''' + + items = ListProperty([]) + '''List with default visible items + :attr:`items` is a :class:`~kivy.properties.ListProperty` and defaults + to []. + ''' + + allow_custom = BooleanProperty(False) + '''Allow/disallow a custom item to the list + :attr:`allow_custom` is a :class:`~kivy.properties.BooleanProperty` + and defaults to False + ''' + + group = StringProperty(None) + '''CheckBox group name. If the CheckBox is in a Group, + it becomes a Radio button. + :attr:`group` is a :class:`~kivy.properties.StringProperty` and + defaults to '' + ''' + + desc = StringProperty(None, allownone=True) + '''Description of the setting, rendered on the line below the title. + + :attr:`desc` is a :class:`~kivy.properties.StringProperty` and defaults to + None. + ''' + + +def get_indent_str(indentation): + '''Return a string consisting only indentation number of spaces + ''' + i = 0 + s = '' + while i < indentation: + s += ' ' + i += 1 + + return s + + +def get_line_end_pos(string, line): + '''Returns the end position of line in a string + ''' + _line = 0 + _line_pos = -1 + _line_pos = string.find('\n', _line_pos + 1) + while _line < line: + _line_pos = string.find('\n', _line_pos + 1) + _line += 1 + + return _line_pos + + +def get_line_start_pos(string, line): + '''Returns starting position of line in a string + ''' + _line = 0 + _line_pos = -1 + _line_pos = string.find('\n', _line_pos + 1) + while _line < line - 1: + _line_pos = string.find('\n', _line_pos + 1) + _line += 1 + + return _line_pos + + +def get_indent_level(string): + '''Returns the indentation of first line of string + ''' + lines = string.splitlines() + lineno = 0 + line = lines[lineno] + indent = 0 + total_lines = len(lines) + while line < total_lines and indent == 0: + indent = len(line) - len(line.lstrip()) + line = lines[lineno] + line += 1 + + return indent + + +def get_indentation(string): + '''Returns the number of indent spaces in a string + ''' + count = 0 + for s in string: + if s == ' ': + count += 1 + else: + return count + + return count + + +def get_config_dir(): + '''This function returns kivy-designer's config dir + ''' + user_dir = os.path.join(App.get_running_app().user_data_dir, + '.kivy-designer') + if not os.path.exists(user_dir): + os.makedirs(user_dir) + return user_dir + + +def get_kd_dir(): + '''Return kivy designer source/binaries folder + ''' + _dir = os.path.dirname(init_file) + if isinstance(_dir, bytes): + _dir = _dir.decode(get_fs_encoding()) + return _dir + + +def get_kd_data_dir(): + '''Return kivy designer's data path + ''' + return os.path.join(get_kd_dir(), 'data') + + +def show_alert(title, msg, width=500, height=200): + lbl_message = Label(text=msg) + lbl_message.padding = [10, 10] + popup = Popup(title=title, + content=lbl_message, + size_hint=(None, None), + size=(width, height)) + popup.open() + + +def show_message(*args, **kwargs): + '''Shortcut to display a message on status bar + ''' + d = get_designer() + d.statusbar.show_message(*args, **kwargs) + + +def update_info(*args, **kwargs): + '''Shortcut to display an update on status info + ''' + d = get_designer() + d.statusbar.update_info(*args, **kwargs) + + +def show_error_console(text, append=False): + '''Shows a text on Error Console. + :param text: error message + :param append appends the new text at the bottom of console + ''' + d = get_designer() + error_console = d.ui_creator.error_console + if append: + text = error_console.text + text + error_console.text = text + + +def get_designer(): + '''Return the Designer instance + ''' + return App.get_running_app().root + + +def get_current_project(): + '''Returns the current project in project_manager. + ''' + d = get_designer() + return d.project_manager.current_project + + +def ignore_proj_watcher(f): + '''Function decorator to makes project watcher ignores file modification + ''' + @functools.wraps(f) + def wrapper(*args, **kwargs): + watcher = get_designer().project_watcher + watcher.pause_watching() + f(*args, **kwargs) + return watcher.resume_watching() + return wrapper + + +def get_app_widget(target, **default_args): + '''Creates a widget instance by it's name and module + :param target: instance of designer.project_manager.AppWidget + ''' + d = get_designer() + if target.is_dynamic: + name = target.name.split('@')[0] + with d.ui_creator.playground.sandbox: + return Factory.get(name)(**default_args) + elif target.is_root: + return target.instance + else: + classes = inspect.getmembers(sys.modules[target.module_name], + inspect.isclass) + for klass_name, klass in classes: + if issubclass(klass, Widget) and klass_name == target.name: + with d.ui_creator.playground.sandbox: + return klass(**default_args) + + return None + + +def widget_contains(container, child): + '''Search recursively for child in container + :param container: container widget + :param child: item to search + ''' + if container == child: + return True + for w in container.children: + if widget_contains(w, child): + return True + return False + + +def get_fs_encoding(): + encoding = sys.getfilesystemencoding() + if not encoding: + encoding = sys.stdin.encoding + if not encoding: + encoding = sys.getdefaultencoding() + return encoding diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..cfe13a2 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/KivyDesigner.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/KivyDesigner.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/KivyDesigner" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/KivyDesigner" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/doc-requirements.txt b/docs/doc-requirements.txt new file mode 100644 index 0000000..4dc7455 --- /dev/null +++ b/docs/doc-requirements.txt @@ -0,0 +1 @@ +# documentation requirements diff --git a/docs/source/buildozer.rst b/docs/source/buildozer.rst new file mode 100644 index 0000000..4fa6c3a --- /dev/null +++ b/docs/source/buildozer.rst @@ -0,0 +1,44 @@ +Buildozer Spec Editor +===================== + +Kivy Designer provides a GUI editor to Buildozer Spec files. + +.. image:: img/kd_buildozer_editor.png + +Settings +~~~~~~~~ + +You can edit Buildozer Settings at ``File -> Settings -> Buildozer``. +You may see the following keys: + +* **Buildozer Path** - indicates the path of Buildozer executable. Kivy Designer finds it automatically if it's on the system path. + +Creating a new Buildozer Project +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to make your project compatible with Buildozer, creates a new specification file on ``Tools -> Buildozer init`` +This command will create a basic buildozer.spec file on the root folder of your project. + +Editing +~~~~~~~ + +To open Buildozer Spec editor, just click on the ``buildozer.spec`` file in the **Project Tree**. + +GUI +--- + +You will see the Editor with the project's settings. + +You can edit the specifications using the GUI editor. You can find some shortcuts to usual settings. + +You'll see the default values for Android Permissions, Garden requirements and Python module dependencies; and you'll be able to add your own value if necessary. + +Raw Editor +---------- + +If you prefer, you can edit the spec in text mode. Just open the **buildozer.spec** tab in the Editor and you'll see a text input to edit it. + +Save your new specification +--------------------------- + +If you are using the GUI, it'll auto save your spec. If using the raw editor, you need to press **Apply modifications** to save it; or **Cancel modifications** to restore the last saved .spec diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..635c3e1 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# +# Kivy Designer documentation build configuration file, created by +# sphinx-quickstart on Wed Jul 8 01:52:44 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex + + + +import sphinx_rtd_theme + + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Kivy Designer' +copyright = u'2015, Kivy\'s Developers and Contributors' +author = u'Kivy' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.9' +# The full version, including alpha/beta/rc tags. +release = '0.9' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# html_theme = 'alabaster' +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'KivyDesignerdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'KivyDesigner.tex', u'Kivy Designer Documentation', + u'Kivy', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'kivydesigner', u'Kivy Designer Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'KivyDesigner', u'Kivy Designer Documentation', + author, 'Kivy\'s Developers', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/source/contribute.rst b/docs/source/contribute.rst new file mode 100644 index 0000000..db3c76d --- /dev/null +++ b/docs/source/contribute.rst @@ -0,0 +1,37 @@ +Contribute +========== + +About +----- + +Kivy Designer is an IDE under development, and we hope to provide with it an easy-to-use multiplatform Python development workspace. So, we just need your help to keep it growing :) + + * Have you found a bug? + * Is there something missing on Kivy Designer? + * Should it be different? + * Do you have an idea?? + * PS: We love ideas. Share it with us :) + * Any kind of contribution are always welcome :) + +If you can help us with something listed above, or anything else, `reach us on IRC `_ or `create a new issue `_. + +Contributing +------------ + +We love pull requests and discussing novel ideas. Check out our +`contribution guide `_ and +feel free to improve Kivy Designer. + +The following mailing list and IRC channel are used exclusively for +discussions about developing the Kivy framework and its sister projects: + +* Dev Group : https://groups.google.com/group/kivy-dev +* Email : kivy-dev@googlegroups.com + +IRC channel: + +* Server : irc.freenode.net +* Port : 6667, 6697 (SSL only) +* Channel : #kivy-dev + +Read this doc about Kivy Contributing. http://kivy.org/docs/contribute.html \ No newline at end of file diff --git a/docs/source/img/kd_bug_reporter.png b/docs/source/img/kd_bug_reporter.png new file mode 100644 index 0000000000000000000000000000000000000000..546187910a655766bec99218a230aaa6ea9a595f GIT binary patch literal 99172 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 literal 0 HcmV?d00001 diff --git a/docs/source/img/kd_build_profiles.png b/docs/source/img/kd_build_profiles.png new file mode 100644 index 0000000000000000000000000000000000000000..43b20e38940af28ce26f45417abd193e2961c3c3 GIT binary patch literal 43070 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⋙`=SPh!XI&&gy;@0G}Nt*yi8p?z}h6{?aU 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

Q`dKrfQL<7zzMw7>ZTIU(;Jq~3jZKc%C@^nv4i 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

Ob|>R?o4<_`D3S_$zvpzeS_?h6 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

S__~~YsEPn$3eb;i2i^F%L**_wl-|s19s#7?{ zi%ipMOI;;y9V!0HW=PrDxXOQ+(%q>L;~haj%pDyi#adWl?qyJXuxyo6rKmb2E#2Y1 zsRd`{())n^kIjV@$xEO3x=d*#+mHH0r1@sY(q?(m4k%*JSw3{?&HE-Z?HDe`@O{6= z`4w*DN7R4u?KI_wJh+vy#R-k+Xa1bcXy)=Ou2rV1`>Mw9a};Mq8tMB$H|1+e78l_G z$M!F9n(0;-8u zJn}v3Km75>_{^VvXQpz-pli<8wjDHl+nGro{g8l=HR3{Ua@u8-)LZ4z!K45j_!${y z)YA`~x)m*f22^Z9*NZ z*>t+pyvQQ`Jl<#EF;H7t6(K;%2v2yHgYq`>+rNWZMwlo)t%F%}^u5lDB{IW@mUm%` zJgq$GI3pO(Dl7cuLFFvv3{R%M<&cz>*O`N`jnm3w?DQ#>H3SlHN2|QFKQDC#7ff_j zmRKST80Y;rZ-X0o<+70fu0+ZAa&=rIK!fc6W&vjNvIRGjlOCG;Q+mD=5RAGAPc(4L zVp>o>gY&tHT(pb)=y#w>em}N>f70k6(LrsK`O~on#791#HG6I6eF#XEm7Q~d^G42eK*KWlM5Mo_I?k!4!OuDPQu^Lb zj7_nOeWfNpgdYs7MkM!UBh$du#I*fzQvTuN4eMm8pCS9IZL6lP_kFG``hW9y__}Hh z(Yb8J+EHE8UOw`qy~4)qcKh+p*5n0qjnjsBU~bbyf}>zmZxp`0gh%PQQFN{AnZs}q z6Qr-*jmkH)L~b(^*$Gs?e3+w4zMhMTxt*}>`@ma94hI}(ox_~LABTn{9j;&ej0A#I zESJKfLU&?D>z=kfz_qDJewCbIlAqMnFH%(&rzmUsV*2>3hRa^2sn%1hrRc5?kT1&!j&_oxy2*H_)_)d&9Y&QID7WqPsV*@{m!FjEZ2Zsc zD~&Tt3CZBJA-T^@22WYm2F#;X$E=>~)iNA&T%DYfIyUXs{T?MPCx;>P6EIJEGHAENJoGN5;jWd^5?X&E))=ZC=GdPdhJ7oGoZqXmIunT@ zRFF=<31i;&S>gk)BXee&V&L3x>+pLR08h zQyb>kv%M?-=&fEp>``%#wy5qyj;9Vc z!6kWVMuB)z%<0c5bY3Rrb*#MF`zpg`WFq7e__Bz;cUG4a(_Iub4Spk(W3MzNAj`RVh} z4!0C(kr&c@s$<--%*P5L5kE>wSr4X`OZ$}hOBlElI_X7dgF{r5|3^cHt_v39-uR(<*s z&Q%>16i&~3>%s{m3E$dmAy&}0AFktd5Yjr%D?`WwVFh@!z9s*~5q4{qo+KUfO65n3 z`9}dX=m?i$fOlQxOA(T{%=jT;_#D-&LJb_WKYtI_A2hCZyo#1`6LU+WRPqQ~Z@3o8VP%9LRKU)h|#yu6tgg z+fyF!5MvkpssLCEWT|^E3)%!vIUimNxBM6w8*76y3MRfOc|kjB7;yPD;iJ#(Fz1&7 zt&bQam2B@_{ydh6S!c2Gm%MTDLGEzAI!n(Zcg={DB3*}NEN^}xBp4ZQo?lQ+3VPFDANREr(V%AOr^gFzB`6MaNhdbODu5fAS`)PPNY+ zVr3pVPIwS1W`9p@VxTyqbRfr*&-1 zqW##ZdctF!VPm4?^GANy-Sf+dy#~^rk&(Y4n1X+d*=!cS(*58y!VpLZ-Q;!HnUy3( zTZ`r~xC42W*8W;o_a?M3AQHHsQ7g4bEiH8r8dDcu zQo<#RNuvHOf5T1`Aqitc$M?Cc>^zb5b*Hr0-`a>KAAlW7E#W`Zy#+U)oJD)hJTLe+ zs0)-(F?`tiO?w@SARtMDmFW#jc82IOD42lv$)Q1${;tpzvJqeU;qaukB|`3RNcgQi zbGBaF9<tjcMNxgWHn5d13_c^s?@0fU``wz<9jEtNV?QTt^t~mD3dx3Y3NV)1uhSf#8 ze9$2i`Ez)^!sXOv!h+Ke!}E@ij0}dQ<>bp>O?^h{hT)kll`mh06j=!iI8=GVL+mjt zv^q7Iu3vQ6UmGiNs+rp1sC>xRk`>z@7?#B@%vxUuo%`|J!ZLIb?=I>vfOW!QZ2$7@VaNTZfBQf) zz56{r2DYuOirtimz^VQt)8X8P<$Ww%;--z!<+lnNC=beA`WJ0jJg4}YgFBy5@>GuB zTJrKnqhVyEWM}^lP0}6`;Ew8@+8^5UH?wn?(%UX~=$wZT=S=VLKuKokc<}Ht*|Q`v z4O?hs#$oBKCa>-;58b`GM~a>A3gkxu0S9e20$1pf zG4Mr8Ko2D)CEffGOvq^ivv7wh{pf&%8 z;pqwg>Ft)p;bY!Bs2m>F`8B%Z#0R)S+s6yHd!M|d?jIK4U%Yr5`}&_^2Mq;OYFzAR zN=6~~))+8iR{AtfPXWW}zeJQ*pK~p9PgwvA;<4jFsnH}Y&uG-=K%O~v%vDRBpH)%q z{uIk-6uRhIGsQ&OJb2jlI=$alCLPLnk+;S1@13|Bht|&RonP_~4HAd9`ebDx4_pz; zBmKqA)=jw^Z&b3r{4dQB5E!TnZF``0y8seM&>Kv|z+mx9<1;kD|I#Pg_$%C8g4fj# zvIivqIFSq`ZrD7sY88Lf+tVYYq0wEocHBIF2;JjE-`U|nV;BAi&kZMaRbK{(L?i-Fr|=gnW{lG2hxWu^3L0#<73 z5%2D8BVi8#m}FeeAJ1xn%)qk@=?U^Rq8Jy#goA@x9v`kPQ1sG3Ut2$RGWy2VY3v;SL5I74mXj z8daJ3mNT)oeR5v#2x#fD-zXdg&8=nT4Gk$GLH+DscvAuX&)=%4wHMB}fGyXtwDk=c zPm`6+X{$)(B6KSu{g+C4!pIl`b?-$mR|wPzj+kK2%XO`bgLFXyfCNM%lu=}2r1hPM zZV_YnZjL8M@sCTXj@9#3?Aeie5=lvXirk$?TgsZ{cYXHWWbKE%RJwdX;9COTAyLJl z8Ld?Mf7#SVZGw&LM;iYhP)AS2m;Tk1yuA2`hzQ2%^~?*XV|Un`n$$B(aKGY)A7^F6 zt)(!LCMjshH~5jd^JO@SV;sE06uC!%(8KOQce_0(Pu@SDzK(}ZV;3HRqobo>Pg&#l zsU|+PdleM)K$nn+=C#556L#U7LG7cp{DB>>n6N1iJjUBYh~;2thBmaWs^DMh_LNB} zX8TAZufAfZ{XB!5@~mQ$|LutX1``?!t++-VE>Ps0HQgwdfDIx$HnqHqQ27;Vx2f z4tK8frl(PjlreoRFAq5ZNaKyd3+a6p`>eY)SMRtbl3$95wAi$^V&E|p?e|K`3EO{< zc$}HVGGphEqvY6+0x(FyxV^r<6PH7W|70E+O>4`=X-MKCEL1=lDe34w)5xbCOs)K9 zg0Z?c9UP-nwd-3ivgSyg&0nUh_ArfIpqt=9YAq{_&xgGTXEF6jxw7jW(qGF-|`d9R{!%ky0A=H@039|?+r?}9$5PIbQjCl!yJG=D=eP`!}0>y8%(G|g|?tw#>-h+a%79jYh2`qI%H3><+h zD-u)<@MmRN9S%mRTZ{9SO_`w7n+Y6}cTh-=m7nPHB28a=NR$=ks33Z~$not6kpZF- zLd3j#K23#9u)4fe6@%Q4iyNg}Ssc9WIW1wH>vg^*t#^4Ag3qgiTb^FDsNs=u;@$l+TsW^S=~vKahj~-Zp}7IH>;W)mREOrfsFxcVW9_Kd|utX zmX;RVCS0ZGP^aB#&h#IuVI;Byu#$JY2hkVUeY#>hr!$ww$rW&|fj1-7b(eQrBmtKt z<0vJgIU6&zR<{d_qsuo#e!@T5F6(LXvzqVO8yPpZmLeVvNtTj*yo>&*PFh3TmU9>P z;otqs^2${lAu$$3*0a7xLK?oll)$ym*XNdmwh`sIu=@~ z5;k^i|6?>(^1HGXC^Big3=!N>&~0{hEF9dZADrE-G&ICY z8EGjeO&NB#p*bf3`N0a^e$ zK=csa(~A50)*nks4kuQ|D3Y!KLHFPvy(`i47`)3=Y`a~PPV%foN0fejxMkMHUj3=d ztgnN^b_m!pviI*b$Hwie_g)fFhL2Gc@|b)J&7wtSVBk4$**QmlMKg0q>s04P`E5FX zW}k*Dj@UcBHAR;#i(Y+%xy96@);ooqw6oI%kmTt7u3vs`O&Q&ii5I0D#7#|p|x!9mm z@#^YS{duIuP*=Y!mE2oh{pNd*=oE{0njTSJZC}r|b{+Kq8`T~E!lJ5kEVVsRg2*eIG@K3fBx;goYGO4E z;{1|Qvt66UXn&Zh7cy8bE;a$GkOJbfm{=mrP`VN~nkaV$$c0o$uZ!fw;L&e0O|(4WB)GhevO9 zt1T}l2*COmiF#M^Pj*~dzdGpZzFX>q=^zT&F`O+{D_^3(TxTn?v$~ejy&0}{mYJKo z5N38%w|B{Uz1;0+wcsD7QT0p?=f$f7+e!OedX+vrWy7XG#RC^=MvkBTzc)r@nuC9V zZO5c_9b@B^lKQ_`d+VSm+y4!eP!uIZ6hX=ai$*#u1SAFNZs~3oP!uFrK)O^)8l-dS z4yBgvkQSEC^I7%%&Tr=Yb!N`a_>M2KyU+97_x;K1b6v+m#Y#_}-~ma9&QO8z(<<{n zd%PZv*6t{!GUn3bqB@hzj2p~Ae(z&k+|ew5+^|ZgV7=I;fv!U8#7@SgOP5X|{-lgi z%gbg!6ArxDOP|q#>Y-*}sE;>O%b3@e?0(miZ#`aY{EU)`d?FI*@B7XYPL z6V_Xh`8O!RPfo=ktr*ppsGp)TBx8B?-o0snfe@C}$}9PU4@k2v8l48J>;{lm@mu@3 z$`f`5Yqsme?Cdy;M{LeRakF94ipZYluTwvJ{h8RS%XWT2zalBWCVqunqVpVNnm%=d z$-;l)*C#J8F_-4q_CL}apC|OF*Xwfo)=heA{Si+5|@8LP|fE%ez%mmp8H8IU3*?twV(5+ z@d%V(^}yx%cFc7#Efok6r${|^@qs|_>7}TD+1FXJtEa`&{qlX%(*?JU{>0P(C8Qw{ z{ox8IMr4y$havT*)6B$w7b$7ew>X%}QF;%c3vh)2tiV;0n3iVzV~KY4OqE~JIjD;Q zfXN1+V_ETvhPOI@93DZdhiyS}=Ju?y>hW|LvgJdow%hV92eC?iO=bq;mm(yk(31sW zpJ)=Oc|t$3Sj&DpmBt5MvpuvD8{@{DcvE!eJ_!?n%gInrt`CD!Q)JfX3$$b%1Hbb0 z8}y0A5R?50+rie;OAFAIgzE7l(FCOL2Oafxp2lZLLRXF3MHhpL=}0R&gE|c54wG`x z9J1bZ>qmCqQT~&XAsI#qlkVdTb!LDd-57f*bJi(Lyxm;Z_#XyrMvla z-y6f3=NNG&n)LgzD;%+# zMVeY?zHBDUBZa#1wFHLO_zvWy0Ywm2uDBt+#ed}Yc*%kO*U^%Uh{x3WQ)K>XJs;O6 z%ZadVC8yP9+t}CqY})qT@>(mlMM2b4?MId^CmTy%D8o@-`%D(CE>zHoKPuud4tip_ zXa#*HUF^^K^MVd`M(KC?wUd5z9X}kI3O>>x@(#9=W~T&_-;$mA9joK7BEPowIhg19 zftKD8g`t&`m#d|W<4R>roJlz3r#}7ywTCAk$X0FJ=G|^iQ{!AxJy&`Ulg`|hDU=(% zl%K=M$ecR9HOf6L^ndYm(w+TV-6pIDCOj1YT7w=Xwg<;OJMykJD(;!_=&ILGAzRUo z*}Op{fVBW%*|~_H!L}Hf8DHq~2t=BzhUS;5JbYJYZ_lpJR4}gZV~gLD>vc%{-@i(c z-i7hE^}?;J_Zx8BEDv^SRkyV%Ee_CD{-Pg(O%}o=fTY@}$%kG}-j;oNmip!m<&Oz6 zpl)#5b$bG{*ZD0rO+Ez*E(7J3^qidUcw4JZUzB?ipdDwM=^zSw_yd`aL>~ptxvck+ z;*quC=}Z8SfUaMtOpNetmb*yD^zoY1eXCs>fRo^jxA?YQ=Vs>qRsqe2d&3aTi2FGqiV^U3AO5qSsv)C1n}L2cF9cR>{t1MaU#}8`}Y|LoJ@=R4gI>R0!+|# z#Wwt4lXm3U-7{xOPOm%%IKL#JkUTa$X!3LY-aAe?`t`%wVx#oU4hc7XO@#w^6Mhnp|j!O$0bbrXy21K@!itY!3p48Z9`)qba< zZ{zBd3!t{tmRQsws*#=j4r{G@!d&APIO=T~ju=S7r+ZNV}6v%tu;_p4fn=j z%pla)RJ3TW9^y_24NSnx!2d(zD0+x7cG{alwXtlhNGUEcsOv#OFrd3SUy7>gU-kQn zg$@kHwwJcd6NKy@(KgkpM1%{a7zp$Zc~4&a!rwP93Wyc^{yu?&hI|2mrO8Jlh^+E( z>%L5Gz5W+CpTSl-?`Y1mm3h*Zz@}upy zF#llzHpJO)<06nN3-fPK#h53A z-whDHAup+#0#LPp`>r(>r2w|#(w&{G)?zFag;}iJ^Q^|Zlw&N^s`O^p%OSZJ6>Ran z&94B}Ert}hoD*)Io(y6`q4Y9LPJS|rLOM!E%;82%q;6x&CUdiTEVj@DS}F&?K|!KpUej6>ArJ>;8T( zO@`{ez~-pO{`aF92NF)&iBgfSm-FAI<41RL*S`l!1|&3NL=*n?HerjZ|J|ZhMd;Ly zgpD>q)QnH5u)brR1(DwyfEwM&3;Qu;Jn7MOq7@?5qC@q+ZAye*pOU;C6Jos|3vicB zm&z1-G+*|%NZWC;ff*s1jXCx}$@2Ns@z^i3nKuYhtTo(N$r~pDlE#*uz0t|>^~Kvc zA^I=6IfYCISw5dkr&GG&TU&&D{8)WCLhHHvv74+8dUez-7r`%`>JKeu;*7o*a=I6x z3i?v{RefVI7jUijn5$sBMrci2;+H>P)-%>)HhVlK_07$TXI%v}El^t&4y!{VHSv{@ zY+3A0k^&=_KG<7ON-4EzxeVigXA-)awrTlyeKxb$^R%jXzh@=b# z6(m~PVje$5;LE2*b8uCuEUl| z8Pe=nBRz?aK%&~8a?p3v$b2dM=wQh&9Dk@McT=7hy2Y;NR}BEGY!PYoR2jVl&V9h$ zhM%<4?W3~R5@kZ05SqOl=pTDIr9yX`M-2yxa!TZ9Q! zvn#S_62_YVB+1Zt!Aoh&B#5z-vI+V<6)Nit_rEJ$_~Ku^;+UUiLq6WV&+yXw&Lg!G z=fp!|q0FO?eff$Ug4TQhm0s8EaD7bpl47+fqOEHo%oU~eH5`z68&+qRFeKt&dQFTi=MX5LLy;JrMxN!&-w^3I+hh{(AE(;J>wX z51#=B;m~OV`LI}~q>-4ZAi!R=D57Uxza{cE)+M)U+%MIsC3d^Nq~>-|X^DBib)F)b z>D(|6MQ2&TW}a2S8v*Y7x}j94j4j?32A_4e)j z{QTET1fiv1K)FfaaCzAfdLBXv5m{=*AW+t7Fs>}Ap&_U?<_mu)~uo_ z9`5*3mw@*%G_-D5t+?APZ<7jmZ0E6^%ug+$dk~jem8m~awW9-Vh`NXb`zwi~Iz~U8 z=U*y1yxrd?V6L3G$RHW{sO9HHP#7xR{pvrUA&c`vPpz`l`ldVTq=1+b+Kd99Bm+28 zk&n~UzxFJY#Z^fDiGr@zL_UJWgnbDBYT5a3dJrssR%@OLT*m<>ivAccA^uVD$LhRu zFL31A@4^7dqc6#*H4i|G1vxmm8#w&jM=} zY;C^nUBJ=PYus??8@?WAHP@3Oy`zd_FmEF#5;w>*jmg1 zUJLglU6}Njdp@#@Q_Zx zLcFjZg~=@)xnnvuAF?SSD_e5w=%TypZgGK&s&C+j)7mS(g;)r3c#z7~*9l?rN^)b0 zbazB0JXSLhPZp>F%X?XB15Dgp zK=e~_i6-RBE+!X}vnP4w9Q_=$N)652Y}^qd^Gq|LG7fnTqkL{c^xzHJ;}OgXeF`}L zn)wa?|F(%Czip4<_W@GVbl49*+o`~FPqiDY+XU)grh z)zhUEbm1&rad@SWoSuHQeDtTD+QQcUMEzpKTe`!W^=JEkBFpJq|8yR|+&Kr*E~_@x zckkby9>~jC3+=>(ff6f7OcQz$Z`0Gwvc_5&w-U5viQsQnBQ53bCyr5W@-f`-=CV{1 zW9tA!&CYjJQ%b8;l1NJM>lgZF?aCBM56-5=PAdKb-acyDB#HxD_M-OF$38Cj(M_1&um*ER+g5WiE>(-Q{<529{*8$=~xvSlQ3 zjgnadrS5CYDI_(Zn$Kj*X4&?xqgjqy$rA)?FjoYSDwh5I^Qd0=lb<(QK0R~M=L3vB zz`(Is&-R;V=)Qm>f_?b>RU=i{a7+!QQlVK+)i*nJCh~0S2eI~BgH4gG+XJArOZ z-D`H84?p^ixYr**pB!c>WD|p2>qS z6ub2Bd0&p4hDP150#49j_?Diji_~EO@dQIV)vNYfTL6dp>K?_j!kofvE936`W>G0e zIqXyh&oFjoL#I}seH{*s98h5QI4_>dEe6RD(*)(C_6@4FSE#zqvN))Hz3@OQzE1ZeKPke4C+HO&(xe54u!4H!^3jxi6&Ohg<$AxjxMXnbbqh zr0)df*~bM)Kd=VTho9V5qixV>T`!+9V;T}!{*qkp!_SI}ey2;9vb(C{8HEuc^mE4_ zcVw#QyP0a@bZ_VaD(&%F+?cm&v9Z?d>q3ZxVR{NeSy|otxYzFU`8#He6W>zQyQvEU zK##{W9D92fjSZfRy!*V?4ENPfh#6?K3l*1`v*fnshsyfy zz36+PVVy=jgy(lwOc@X0Ku?}*ziWwaxs`u(e3pt(B~tquq1$*4y1=k=#5RF z-6fokijAyRJ5hc9+>n7W-s#5l)U-;eiGmM@rJAk@d9S+!?wv395DJ*}a>v6Wj)3x$ z?aw!CWC3BrO5_DHNpFBtx)-y%J#0x@u})?&_R(2@bL%>u8(xV=>yZHKEG}LuPRImC1lyG^h zCwvd@tt!>OZ&=r8ajbOEVse*0prmZ4<%|e)<&x`{^+pZS3y0E|7M3n&5057EIG|Sy z^?!*qzDFh|SPDM~q)5M-p^D(8zRc(Mb|3Er^@}{d%$fdKIc2SZ=&sClmO?^{IEDtsxZ8_Bi##s7Q=@_<4VsG79opa+x)%~NO z=SGN|YedL-#--o$^cr#}`;h-}ki~Fus+@v? z6!5?T*_(Fr$LsC!yig3cc_G1H^$Nw|;N$i2Hh9Mdo_DxjOIOzX$%IRwQ*O;Tjsbo2Pf@?%gln zzb`&2AMcVOR0wV9tq_`Z_f-_!{1u1{Umw~4zFOhHNr&fUhM9hw7l9ADM4cI{jn-$f zvS8&BrkyY`$HK$l(YQq|swhSArRodpL7f6ylNgTIBnk5#o#o|Yx0`96*=ZLTX^2@S zDI^E0_EjhvAb6hvVM*`p#04vTkQ-R z%f_zRL}sI(b0QB~)N4!Jr@vXGr?e~1sYsAkEXi9g@?P-os5NXejF)yP>l>|-p?164 zl`1G>D8)cXMySrVc+TO~n)X6xasUaf6r3#LS!w}pxj`kPqgAN`yPW~$_+rRTr0nG% zj)%E7g(c=Bc|7u{E@vCpxOkJiWiX`fp~ITX406Tu$VZ{qjvmb` zST?3}H$`yIhp#T?)H%CtHB8e+8^v_evn4dBQP_@uIuPJCG@I;L9}DwfZ2oDEY^X(z zJ4fVqMmHgLM!a+=d;9v<7d(U#bAL^QtV6pr*2?W7`fwjzvAGk$pDE19=yO@iqa(^F zwsT7KSW;z=uJ+*lv>jhsX697o&iH&lkBrezuZ}jt%a7#{id(V3iM@NLQO*gV_ z^Wd0pI{+*aH1_(6MsgPSjUH9wbVW2meqQ(1r!cx94{~SNX0aD- z&*2EK?gc9}WHje=F>>9Z06Ptqe+E5oYt!CxYX21bEvK_yUIh6W7FSdrs`T8K zGgH}%M;MmUb|UPR4xa6a22)0fSwCePd^q@uTfuh3bGpQ7ra{Bc$PAUoGeW|i&T*$W zjN`8EMft>(?yemArY%b$!>{;>zQ6sif#D4CzDRI}cy9Mi6Zs_l&y*8>S1mXxZ1?Og z!6}{ToxAXhy!oMC&U)tTT-K~}Tg<^tCstzxltC~zxz%K zqGG{6D9B%XTwB^ts0A|?9V-QoOzFQ7Z*|6F}WT+^vXXsjTgd!E?R$u^r`)&s`r(rkOJ=RR`el`mua z6a254uUB2yM;qEsRg?P?`>ljylpW!4vCVbt<=#)Y0+nWPqbVX%dg{A8IR|MX;!g?l z9_P`J+Y|k$kT#LjCNl0bj-^4c9~*k)jFof$-=nr4u#`RBMvYi^vV1A4RUPifB*3#( z!=-f1+17n~dz^68S~IM!i+M)a|)7{&EHO6!a zBdN2snoony&ffe(VZ)ACah;cB{Os&49UXe}oe!WZ*N{Cj;;>Y#nsw>&mC%?NQAKGI z5LTKje>k3Rt;Y@`WS1^c$jQo9uO5z?$w=sJE%wbU554a&xUd8-no}r`j3FAwkgXOiR5;Lz6j}?y|Wj+Qyq!Ru&C-g!AQfdp*uZfTbMuIErc} zW7c-N_>!Zm=GCY7&TSro5wfKo`yZNc)IAf1->+Qha4cjxJUF85ckWj)vK}@s&I8FC zkUm|fic3x^}b*WiaHhALVle8p%yCE8_m3@4h?J=8x^$58jt>1@c zvd8m%iJ^Tu$?b}b_${O#DWjU>2pzZW#JbN@c|QgS^m&R0+RLsD$j zEp_eJ9H~NwMdWMEXz~(Uju-L$lsxwHb$J|yI0;DUX2dgb0I9-dKK4XKWw0y#KrA5R zeGZb;D3&Y2hyIuY{|t%Q>G}V@GK~@56!wH0Vx78O7@W z6i<*g`tLiMAHTFw2)eWqsExh4U^tj>;Ln%TI-m!~zV=i=Bom5;`Gc8B=`>U$t6YN$ z@SCS_iifjA8btmqL(@XJ_;`P}J&BJOh*e55gkH?Bc?Nf2-YEqSUMwL5Vr~Wb%b_BxNh!W!;VLBLazTM|!l`WIsT&j*Q(Yn$h8Y@x@0AMKc0mydS%DkSnw z-s?t}7*`#Rvl78=0L=fS`kG8Bd@@kDFqevFNgAwvF2ym+8R=y4KKkl&bCHhJ!sVlc zsAg*ixy9;&tmoJjB=*y3Xh|BD8+E+&;AkyvyCfq3yS|+FYSWJL#%Skg=Sl`V?qEe1 zQ=2W!Vb_~E{l(qi>u&KUyx?6VgQ6v0i1-=xn@F3JfJYpl;le}UQPHu-PF}AQ+=4;l zN1nVmslzRF!*r6~zHn2_s7AHh#JBa)`ttMN>bho{K7W23KuGP?!it;w)#@}!gcpG* zK6teF9p032%Eotr>%IAvHvEb{Vb^P(4yfdlL)@9&`03AxO-T_82S(nv} z?YP(x)plLB;m^-6C@l02E}h*Z)K~t^`rJEc^OGw%viN{cwjD=7N+C%pS$_0YAZjon z_{&WyGWM@+D-W;?-iYxpW%_Q8I_uZdl!oTlmfENw24dnKe~UsTgyVg@`L;zLhK9mV zD;0)wxeBpe0Z2PAEgD;L-><(LC=krXG z`^I6$$}1hFMX~&cBZ|I#9`lDWOyNwi4>uotH~Id}I9=z}z#W;84T#pO%_5!G>#qn7 zKNv0jWTVDw+2MBH)35Q|Si4K;C%;c~%98Gzn5Cwz>1?A{zwYEruN!KO)vNcss^jNf z%Y~)kN+ssD(8beJrbPYz95R4?kll`V_Z()dN=G9*48dfdB_UB9ZzZc?4VX)2QqOA2 z#~%1*2;6wrDln8hR@a1tgg}65M@+Yn+l75WoNHyUUVb=XswIhWP`2vLd{6v5{B@n< zp;mvWYLs7}&Mfz|`%^n@PW}u>o8kT8NC-aei)t;TWkA}h#xPR*;Rl&np4ntq%t`z2 zkCP)j$BoRDbB~G?Ec76hKRl{Ogqa&j zhxRz9BJqcMBa|Y1dRAt_35y{V@vB?;B}eV9uzPuJ%_RZIGTJ_UyZedr({HENY3!3P zmMN=jTX#y#k5sypJ#jR$Q|hsC6mXti6jlmpf%wFoKQ?oGcZa_B7;9OcC8d@Mnr4)# zGsB2gPE5(o?#ru{!W1Ry*<8Yx?qE&v?%p05Eo}z~nF3#IDyVyz@;k;ZN#o+;R$w~$ zX^;H|hJ_A`6wGecUai@vorP_dto2kl{K|u!PiX!{ssWylJKR-BC`!yakf~9$`I+B; zeYN+LClk6+HsMQBL~^J!ACC2V>r7?TThE`kny5zV@+AjyEy^zRX|4P*OTou;*PfR@ z*Rxi0LaZ!hX;Oz#iAbA}v~e?A7H7Zxiub%f@K)}D0sMquerouU~b zJ?su8s;*`w>LT{|yn{epT7TYlx$;10+dwjkT_hp)hQ=jUA>VIN^V;`XYb$0^@uveu zDNBs^C~>b$%sc2Dh+rNx)uK!(;<2`HlbJEr3U6|967)fDD<&(k5`mVrB;+f?hrjW? zJTPZ*9^Vh2V2@%QVIHCU5i&8rZy{xIMT#)rx;}{EAcORd=8>^X<@>~F2nA9*;_C2s z#zn2HtYmy{ZsZr%YR9`>xmFiMX8t;OwLg3A*|V7o?A5l@_ii^O=Nv3NHv=Dry>ECm z>-=C>Q^;3LW-%>wzPu8kCr?@reMP zC>U`lhAChEbB)*~dm^sDd+~K+`ri-#KoeG5SI0ZO*(3HMn#5vPXOvGdTlIZMe{=0p zHuNq0nm>qnlhQvoQN5aQJkJ5ts6$!zZ{2#glwIka(#UY*MlCj&P%BRP-JqleVR93^ ztzp*gA{AB3jwu>=OC6+zoC%s3Z%n+s1#cW7TVjxjA=;zT!)^zn*i8lxAj@2 zTb4ubpfFO9$ui*Bm@cL@r*gj}ZauU9f7hAk=H^zi8R9P<$?03LX}2up(8+Ve>DDfE z2=WlvFRCf=YsMC@F+{RTVauRy4)WP|QjB`aPsaCPqR} zo_sv#D1JILtnx^r+lyE_gjpwzrUX7bAsdJ<95{VCoslA&COstI~zfMiKf16 zYl-Ti&8-Mc7dDuGGLw;7$u;Yl>k$0mr=bs)0g<3XSu(mRt(EwHF1s;^s!p6SzItc; zq}r9<&9i6W)*YS?dO=Yy9_!$G8Z{d0B?c}u(KdaUtUF6cV!@p^)PC)@0W}O1otF0T zdV^2EYCUxdl+t=U%cYpuzB4yR7J>{_4t~=8l z414ywL7QT~J6mOR8pw)RR8$s z6Bs@kci*9fPaRmC7#i^b( z;ScQ&EU%;Pv#TE;rw78uo>{Xkx6n%8vNAp>(=mdUt|wCHIC5k}07R<>gM%N7i{FKh zyCoTo&C-w!ay4^IYJ3k4+*gLYEd1*le@JD_G@?Od~qO!YrAI6EoKnI})4 zWYn90^f<^M<4m-3K)gurvCNq_6vtdLfoIqm+hSfk>Y%)hre4ld-reC}v*}mCl&r?-SgNTHEkG0-kh;{88jFe;mka5dMln8K4`xqd^+t_hb^%VBMce>Na z{%=8J`%Hj!=&UE`5`$c*(^j8Slz_8MLS$N0M1)U=p&kg(#@{jqE!&2Mh5%ZA3SnVk z&=QB@Ef^`kho=2FRwqY0JNg5nKoHvDP47`Jr=!z6Q3GFo?!15VVBz`l(N2!8<1qOA z*v`4OT~KP|&KopScNk2Tl$JCLdj+Ya?w%+;Zmw~UA>^e2IXU#8wytkr)B{H&T?hs{ z!LGoP8~uh)?J#^HA<@#=l2TL@BqMZ0YHYml?D_K_z50wsXl9^B1~}7lZf4GaBSN7= zHSA{C|Gdi*$p3*2BpfKGNNLfZsy)x54~nr6ssu+xwSYV`f5j&tSLX0Ic7x>+fBpK7 zv$J!>(e`jsYO1RyIgGx(J$+tmBu1dExp`s>t(6LLgPu?Y2SxPe-ZXhAM@|9>@hYn( zTrNmT1D$8UynQI6%pCl54*O*zCDutvNvviAtcOQOEzQk1L(Do>pAp{g-dT;>quC5v zFM&*{N}ld%(1mBfmSR9zcDW!f{DI07S=kFXpx-lHIv=mMGF13XR1}k;b>wF~-3ThE zz+*cHrv@a}=_JuxFJPBIJ{=k!K9*uOy1$imJoerG{*RGAO5s)^jK+u-XN2Fs2yn1Z23T?V z+(-d%p>Fq6SJ zma(zWlZx-uCk;Yh;37H<@uSH#z0Qyt!rqE+Yu9pFxQ#Vt8cnqC7Z;a?AJAVjr}}Jw zlhifjI8roHkuLau4CBt6f|925Z5uhM7jeJSzmO@hqK?M!`h6E+qjdT*t3yFe{}-aITG%lBf2x zM6&6LNlNN3jY+i1%gHH&HV}MOeM7@K>f|VAbcM8_D>5>YYgRh7M}Dl&!Nl0daU2l= zyXsv={32;bHsFt6~p#OwyahuhMvcTNcDM{&=lvD;~Iy*j@P}5T|tphUB4J3Tl6}qE{Rs?BILr8>1X_lUjA~YM zX4y}|w-nUCxnTp<=H_2^2ly)(@M}hsweRCMEUWgs!AQ6==L^a512!@UmfQxmI+m$9 z&tjAhDFc%0&=rsncXY${xOh`ze z2g>ny)XkeWohRR4!RpJaG6`DUf=<~o1leD>k4}4jzARxCB}r*<#bR4!&C0U7bH?H4 zCsu0F<=5X)`<_#M3EQuJi9XcQqVJ64j}WDE2D?GoC3pD-LPODkD>cELz`mcL%l5zI z_C~kph|_c+v>fwbf4LzLrOV&jprL)8gCiem!noD%-qcNgh8Q5DZ%BYxTz)?Z$stJ) zb~(&EUg^KcDha7vWJZNGj~@Fy2_U!wYIVN{^pN1RUHLf4aLfV&Z&|*Wt2&^sW{qaN zRP5M8A6)g>;IjYR!?&Y=Qqqm|c#(SVU$zYw1RiD`LP@A^B3i4DU!K}M*s`M(4QyS! z+RAkK)hk7Or|?dWPby(T-}-hRR+~qv2I~JWeV+Nyr=9;(J0JTFD*V*8!f{(w(ntH> zD@}RViVZ<%?q9xaQ=gudfz8rq&L*qn|0`fa)N2>~zh8mh)PUOizx?>a2eB)6|GP%` zr*4t|yaP?ch{b4{nhRr~6xqUxBy&VYs@{bK`4J1p4AR{op~Wp39PCOjtPs_K9O;xD zpDW*`U`0>^0nzL_R)MYs{_qc*TtZ+l!E?e2xbjg1Yj(}r011j`+v;xtqY1+kq*G#_ zmT=f0Es0zEoZ<=3pZfTMIQT($epB=F=$pk{KsB;zeLHsQKXHZzZ_JgFy#*i zdNJU*F-q1$<=R_u68mkzV>Po5lMZ*OZdb~9RI^^Im_2y4tUcmQi0@cFUP)9;or)r5 zj{Z&0^&uEq8_Ls{gAS+q59K>ZTV#%u4Q>7Gi*H zOKQBY2Vq2V&S19`txa;4Z}}j6o7Y@%L&%vnwJ;_1DfZ#P_b0}74OyaQ-(osz6qWo7 z%T#?6y!0sv81j8{A9);?>o(|&qxN(47GtV1QV%TJvt+cn-%l3A*gt;BqK#u1N*Zp> zF}SxJCrJBO4)X)etQ(FQG4ZGHPoU&Nvgxw$4vCaO7Jk{!wZ(o0<3T5-Q2aT5p3%kf zv05`q$~s1D!Ts-RcftY8i7Q{w&9`-vuPr2s%bl-B)`=jT9%0|wv`>;-UX!HKSn1cZ z(m1=gaOYoX=aJ(NxDlb}Ef#<8X~FrwJ%rt4{~xxV=rdh=h_fR*<{aAS+;?^6I^$d; zZ}Rck^{;m4lZcLgJ0T5B!+-jljY^QV5qB$$=U;J&B#imCV#EWLTBr+P&nzg_%?69o zO-AYk?F~MLVgjwYRn2J}Y*!wu(~#AAvIfG~dzg9GW0Ldd^#;EiY_5&dYiVg&RBkEi z>gjnn(2UhPCPfN+2*C_+O;=aflKHl|&#K7|Sc3+R6iwHahjcE#u0Ic=*~?#CUUGRl zehA^+EFicnF^9&fag7kdyhEW*t_!d3e_bNiRYr*4I5u39$&%@yX|NB~#re$<0y6ZR@7(!SMCo3{T(mGb2EkN&^h19T?F|w>-t`(Xa4d3di^N9TpdU7|15sL6cK*{v z>Kv>}3&b@SFJ0~kV!YEXvb=88QA3}7V&vybPCoTmE`h_(i?40Uc#i5; z^p%F)9R!NAT6Km{T(*bbvqth6BF;~JEioJMOweGlvyA<>@eqBFSx#eF&VW+`nAxyh zI8ShXtEdzAaAu8Gc^ScmyQ#5L&InNhSdd9D!m}X~%sRlrq;FuL!xcj%-8%~Y29 zv#@Ou@Vq!v)6>BGf)9G2`9RYI5%Ia|Uq2I_m-CvOz$2GHYDh>#v{`@Q$`y3_GQ_+a z56CY-)f+mYO^{Ave-FwemfTgV6x=SHZDTH>NefN(uA{VoKCE7Gz)bqkv%79DD;hwG zKMzC994CkK!p<<-8_8vU^+yryw&hOHJ~umi2(X2-Ij*W7L;V35#B&Ow5fv;k9gW4~ z?h+u)MMy%jU5|-C9lpobTc9}`QeR)MxTOh-G1wR=e!V*`Xs!xwBoT_0SeL#nY&G$y zF@UNL9@WrtV&e3C86X58c-ZL8d;RmP;?YSxxlF;3sN;ffm4qK*9r2-_8sR)L$V7pH zo~(JvD8OLFAarjccD^`!D6nnTA=D&qqhs}63enkhvxD+9 zFJ))z8|!3mhzQr`lPeEY9Qt`uh+Ye!nfY{h9O?M$<YWV}zg0s-u#b)>17BoL% zm5-ow1y2@ps}H+ZWMnMKLvwOiJa+5oci~@3_vT{u;6ziges=Z_hY-eb#9B1td$>er zham@~Dcq3lU}Fqe_LIpV{?%)&thrKdqgmfdBHQEm_s}tx8?*BX)^kxt+LS&HqqV%Y za}aF_=R){}UB1tcy*%fHD&e}Y6##D39x=8;wYy^Jf!!AT=02VK#SI_GZkE7rIKziv zjS;rFPp+h`eGOY~&6gHT8T{8bj^7br(!D11akRoQ+?IyjN#Zz=i?Zw1pOtyS!^!yx zP9Oxd{VOX2X+_s+}ksByi9oHMMc-|7UlLC>5J;1>sWkiGUuKfXT*JZz1jEX z2jXqLx$?W2_rA>~XEmtj-^f=$xEqppC0rGC5y3ZN9`BqYE-%#^x)7rKR4~UENi_4l z%kj65rLA)0g|>;0MGFy;drB)G{S>Arfq4r zP5fqA|F3@*NAO#lZy*0!MNfX?BDDUU57(PuI6)7d!KqWHes}V(0oNSt4LwNYVKs|; zzHdXUO-@gbgdGAI7|6PIn2l9L0r}Ac>_u270S?8TJ{yiWD2eL9TcxE9A&`G$%iiDA zl&YWtf|)qvjMBI6-1)FG?vVhHwEj$$r}p-RjHRx3RI(K5Ai|9L_^~+ztvOw!W-;mW zaGc$x_uJF&sN7su;OlD~Dl|>Y$}#|2#`vgU$gQ9VE3`L54g~ZAP~5bvSZ~Bd?{B7f zL@=p6hq5xjGN6cEtQwPVAXy2?I@ZX8#i7Q51AtE|P`%ifVGM(V0LM9X`ZO68)xzL* z2lu!>943gi?2R~Z7h@DM6xrMlooef1SWJZ%l0uaWO%!4fb`FP0DJfX)I%Q=_ti=Vj z@XaMO5*Qg8k@PZ#h76GWjP|}2<{d#?nOv)DzkZ|~tR}9I=(TmbQ2hd$-ZLpuR^4LP z$mFhKdFa}?3}^OI9AVpe-?0U@>!ZqfMs)Ws>Ix!F&;7HVPB_A&qR z@=eZ!YzyG%kb$x$LyBZnEj%AdX=%)rNhmqDwzWY)Ivx#NY3xAKCfzm!6}!ffQd=rv zcLAv2!m|gJ2nyz^UEQgvDIk0Jnwgo|;Q-|l1Bx?dNxHeEI3~Hrn)7PM;o~9hrkmE5 z%A8#4<+ADi%-S`6(7#~k<(#nNG@MRTbw6`8_c+{K@9p7--#e2{_fv}QAxSpsW-Z)V zY`x!48*$}w@(if-{V)+8s~?Pip6VZS9MO=&@XU5JiP8bhwXvM?R!k)CzvS=OQK{ znHwuVX~s_Y&UHSBciI4G%wk6NZKCpAluEuK+v^|K;8OY*_wuFFB6{!-e6_@2qouuY@i1V08K&V@A~2qi)K#|J;_xng(jDc z;_NAcgM%}*P*tOY$!GZ7fq}9X>Q+?z4iB*8Ca_S>m^ql^Z2EynQ7C-yqD36Cq(kf#RJqV<1^ScpVS*_A|}Y_n;f@XiL~3JuUOyQBEd5PLxI@bP_-$oYaEY2fq< ziwl2@3CtxRq4^aR{2EFdt*uif(?K_Rc?a`R9WmS-i2Y3mw9;uHyr~E3(qj-Kq@--l zYoc2!O?zp`jnsOLgL9fl{MqAIh75q5I?r5EEzf1LW>E8|ku&Ab!T=Vy#_NnN@DRH) zzZny*!e4w6)7V(g>e73M8D)cO)XonTs>AO~E%Yoq++hEp#)gfWOQ9Mph=gjE%f){1 zq%ctJ&0$l~vD_l$z8&dg~@Bi6or0%oin}=n)7qC%NMF z^iXwlg78{tL6A#%C^fkrxI_+vM`y1fX{$vw+Z|T?qzvqFMj|OCKIB9NAK@vxb)Vfo z!-Y%^XeGEB#|%Q_4N9fv&R(kGf$G1 z@-1rcbIU^Gs*Q6<{y+CxsSy!@H-LtX53Rs?Y|9PI=M1dVG!UUrHeAt4f_8)Bn6nSS zIq~dNi%k0NVb2tsb~O&RHuahJI;VEEr=o8Jthp^T9UOS{`YehuFxh2ON6c*nq@8jI%$1)j#=X9DqQg`wOFJ!vQRTi{KgT1s4A>1w38x@5IIx@Y~;Ex$ay zL$cc=Ibyw&rICzkqK@f)e(6EU7xjb-5>b7fv>CR|Ip$R}N7H8Kk)4z^pD%b=>|2H# zxt6zebey!!#VW3;6_ z%2l;8fyL&2B-=N7ku7sH`|1N}&xR#TDPl<5UD*6Ay!lQs7di&Q{8u*DJ%p@zj_?sJ zSw-tj4@>W4{l$~qK)nCSa-&snr&ndHt8uvwNk=CIj+m>)pF&kci8 z@UatY;u^6C3Ku%$?d3)&AvbARH_}eDH&zY_bZmA@m!+1=rkCSMp8hrHK{mqE(Ti0^ zknTY-@-#GSrTJGlp_K$V>aT^|zrTbv{K!2O3Ea(x5ax1Wp|#L&1OeN1Fy}8dM%Oj+ z|AV4Ddp2bI>b9c1ygc;1TwVBx{$>{N--T((>g(ld+dsdh4#^>$wEvVnhGY?F{xq$> zUz2o&6&DMVw&HQ&3m58|wAvi###^lGOtak2j$h_CGN8&vSU52Xb{P~;XFgb2VEuP> ztotI{jt6`D7wE=FJ%mr{5Irk5y@!eapY{6ohJ=P*PO<;vY4SDkI4Cguzs= 1.9.1 +pygments >= 2.1 +docutils >= 0.12 +watchdog >= 0.8 +jedi >= 0.9 +gitpython >= 1.0 +six >= 1.10.0 +kivy-garden \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1431da0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[nosetests] +verbosity=2 +nologcapture=1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9a82ca0 --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +import codecs +import os + +import re + +import io +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) + + +def find_version(*file_paths): + # Use codecs.open for Python 2 compatibility + with codecs.open(os.path.join(here, *file_paths), 'r', 'utf-8') as f: + version_file = f.read() + + # The version line must have the form + # __version__ = 'ver' + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + +curdir = os.path.dirname(__file__) +with io.open(os.path.join(curdir, "README.md"), encoding="utf-8") as fd: + readme = fd.read() + + +setup( + name='kivy-designer', + version=find_version('designer', '__init__.py'), + url='https://github.com/kivy/kivy-designer', + description='UI designer for Kivy', + long_description=readme, + author='Kivy\'s Developers and Contributors.', + packages=find_packages(exclude=('tests', 'tests.*')), + package_data={'designer': ['*.rst', '*.kv', '*.ini', 'data/icons/*', + 'data/new_templates/*', + 'data/new_templates/images/*', + 'data/profiles/*', + 'data/settings/*', + 'tools/ssh-agent/*']}, + license='MIT', + install_requires=[ + 'kivy >= 1.9.1', + 'pygments >= 2.1', + 'docutils >= 0.12', + 'watchdog >= 0.8', + 'jedi >= 0.9', + 'gitpython >= 1.0', + 'six >= 1.10.0', + 'kivy-garden'], + + entry_points={ + 'gui_scripts': [ + 'kivydesigner=designer.__main__:main', + ] + } +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/buildozer.spec b/tests/buildozer.spec new file mode 100644 index 0000000..e56bd6d --- /dev/null +++ b/tests/buildozer.spec @@ -0,0 +1,193 @@ +[app] + +# (str) Title of your application +title = TestCase + +# (str) Package name +package.name = test + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.kivy + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas + +# (list) Source files to exclude (let empty to not exclude anything) +source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +source.exclude_dirs = tests, bin + +# (list) List of exclusions using pattern matching +source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +version.regex = __version__ = ['"](.*)['"] +version.filename = %(source.dir)s/main.py + +# (str) Application versioning (method 2) +# version = 1.2.0 + +# (list) Application requirements +# comma seperated e.g. requirements = sqlite3,kivy +requirements = kivy + +# (str) Custom source folders for requirements +# Sets custom source for any requirements with recipes +# requirements.source.kivy = ../../kivy + +# (list) Garden requirements +#garden_requirements = + +# (str) Presplash of the application +#presplash.filename = %(source.dir)s/data/presplash.png + +# (str) Icon of the application +#icon.filename = %(source.dir)s/data/icon.png + +# (str) Supported orientation (one of landscape, portrait or all) +orientation = landscape + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = 1 + + +# +# Android specific +# + +# (list) Permissions +android.permissions = INTERNET + +# (int) Android API to use +android.api = 14 + +# (int) Minimum API required (8 = Android 2.2 devices) +#android.minapi = 8 + +# (int) Android SDK version to use +#android.sdk = 21 + +# (str) Android NDK version to use +#android.ndk = 9c + +# (bool) Use --private data storage (True) or --dir public storage (False) +#android.private_storage = True + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) +#android.p4a_dir = + +# (list) python-for-android whitelist +#android.p4a_whitelist = + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (str) python-for-android branch to use, if not master, useful to try +# not yet merged features. +#android.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in tag +#android.manifest.intent_filters = + +# (list) Android additionnal libraries to copy into libs/armeabi +#android.add_libs_armeabi = libs/android/*.so +#android.add_libs_armeabi_v7a = libs/android-v7/*.so +#android.add_libs_x86 = libs/android-x86/*.so +#android.add_libs_mips = libs/android-mips/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# +# iOS specific +# + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: ()" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 1 + +# (int) Display warning if buildozer is run as root (0 = False, 1 = True) +warn_on_root = 1 + + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +#[app] +#source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +#[app:source.exclude_patterns] +#license +#data/audio/*.wav +#data/images/original/* +# + + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +#[app@demo] +#title = My Application (demo) +# +#[app:source.exclude_patterns@demo] +#images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +#buildozer --profile demo android debug diff --git a/tests/test_apps.py b/tests/test_apps.py new file mode 100644 index 0000000..b2582c2 --- /dev/null +++ b/tests/test_apps.py @@ -0,0 +1,23 @@ +''' +This file is responsible for testing Apps inside the Kivy Designer project. +''' + +import unittest + +from kivy.clock import Clock +from kivy.properties import partial + + +class AppsTest(unittest.TestCase): + + def test_designer_app(self): + from designer.app import DesignerApp + d = DesignerApp() + d.bind(started=partial(d.stop)) + d.run() + + def test_bug_reporter(self): + from designer.tools.bug_reporter import BugReporterApp + b = BugReporterApp(traceback='Exception message') + Clock.schedule_once(b.stop, 1) + b.run() diff --git a/tests/test_buildozer_spec_editor.py b/tests/test_buildozer_spec_editor.py new file mode 100644 index 0000000..8c3176e --- /dev/null +++ b/tests/test_buildozer_spec_editor.py @@ -0,0 +1,22 @@ +''' +File responsible for testing BuildozerSpecEditor +''' +import os +import unittest + +from nose.tools import assert_equal + +from designer.components.buildozer_spec_editor import BuildozerSpecEditor + + +class BuildozerSpecEditorTest(unittest.TestCase): + + def setUp(self): + self.spec = BuildozerSpecEditor() + self.spec.load_settings(os.path.realpath('tests')) + + def test_config_parser(self): + c = self.spec.config_parser + assert_equal(c.getdefault('app', 'title', ''), 'TestCase') + assert_equal(c.getdefault('app', 'package.name', ''), 'test') + assert_equal(c.getdefault('app', 'requirements', ''), 'kivy') diff --git a/tests/test_kv_lang_area.py b/tests/test_kv_lang_area.py new file mode 100644 index 0000000..84c2dca --- /dev/null +++ b/tests/test_kv_lang_area.py @@ -0,0 +1,60 @@ +import unittest + +from kivy.uix.button import Button +from kivy.uix.floatlayout import FloatLayout +from nose.tools import assert_equal + +from designer.components.kv_lang_area import KVLangArea +from designer.components.playground import Playground +from designer.core.project_manager import Project +from designer.uix.sandbox import DesignerSandbox + + +class KVLangAreaTest(unittest.TestCase): + + def setUp(self): + self.play = Playground() + self.play.sandbox = DesignerSandbox() + self.project = Project() + self.kv = KVLangArea(playground=self.play) + + def test_get_widget_path(self): + p = self.kv.get_widget_path + # level 0 + float_layout = FloatLayout() + assert_equal(p(float_layout), []) + + # level 1 + btn1 = Button() + float_layout.add_widget(btn1) + assert_equal(p(float_layout), []) + assert_equal(p(btn1), [0]) + + btn2 = Button() + float_layout.add_widget(btn2) + assert_equal(p(float_layout), []) + assert_equal(p(btn1), [0]) + assert_equal(p(btn2), [1]) + + # level 2 + btn1_btn1 = Button() + btn1.add_widget(btn1_btn1) + assert_equal(p(btn1_btn1), [0, 0]) + + btn2_btn1 = Button() + btn2.add_widget(btn2_btn1) + assert_equal(p(btn2_btn1), [0, 1]) + + btn2_btn2 = Button() + btn2.add_widget(btn2_btn2) + assert_equal(p(btn2_btn2), [1, 1]) + + btn2_btn3 = Button() + btn2.add_widget(btn2_btn3) + assert_equal(p(btn2_btn3), [2, 1]) + + # level 3 + + btn2_btn3_btn1 = Button() + btn2_btn3.add_widget(btn2_btn3_btn1) + assert_equal(p(btn2_btn3_btn1), [0, 2, 1]) diff --git a/tests/test_playground.py b/tests/test_playground.py new file mode 100644 index 0000000..167aaa7 --- /dev/null +++ b/tests/test_playground.py @@ -0,0 +1,25 @@ +import unittest + +from nose.tools import assert_equal + +from designer.components.playground import Playground + + +class PlaygroundTest(unittest.TestCase): + + def setUp(self): + self.play = Playground() + + def test_generate_kv_from_args(self): + g = self.play.generate_kv_from_args + tests = [ + ['Test', {'text': 'Test'}, 'Test:\n text: \'Test\''], + ['Test', {'size': [1, 1]}, 'Test:\n size: [1, 1]'], + ['Test', {'size': [0.1, 0]}, 'Test:\n size: [0.1, 0]'], + ['Test', {}, 'Test:'], + ['Test', {'pos_hint': {'center_x': 0}}, + 'Test:\n pos_hint: {\'center_x\': 0}'], + ['Test', {'active': True}, 'Test:\n active: True'], + ] + for t in tests: + assert_equal(g(t[0], t[1]), t[2]) diff --git a/tests/test_uix.py b/tests/test_uix.py new file mode 100644 index 0000000..36df0de --- /dev/null +++ b/tests/test_uix.py @@ -0,0 +1,66 @@ +''' +This file is responsible for testing custom UIX from designer/uix/* +''' + +import unittest + +from nose.tools import assert_equal +from nose.tools import assert_not_equal + +from designer.components.kivy_console import KivyConsole +from designer.uix.action_items import ActionCheckButton +from designer.uix.settings import SettingListCheckItem + + +class UIXTest(unittest.TestCase): + + def setUp(self): + pass + + def test_ActionCheckButton(self): + btn = ActionCheckButton( + text='TestCase', + desc='Testing', + checkbox_active=False, + allow_no_selection=False, + group='Test') + + assert_equal(btn.text, 'TestCase') + assert_equal(btn.desc, 'Testing') + assert_equal(btn.checkbox_active, False) + assert_equal(btn.allow_no_selection, False) + assert_equal(btn.group, 'Test') + + def test_SettingSettingListCheckItem(self): + check1 = SettingListCheckItem( + item_text='TestCase 1', + active=True, + group='Test') + check2 = SettingListCheckItem( + item_text='TestCase 2', + active=False, + group='Test') + + assert_equal(check1.item_text, 'TestCase 1') + assert_equal(check1.active, True) + assert_equal(check2.active, False) + assert_equal(check1.group, 'Test') + + assert_not_equal(check1.active, check2.active) + check2.item_check._toggle_active() + assert_not_equal(check1.active, check2.active) + + def test_KivyConsole(self): + kc = KivyConsole() + h = kc.txtinput_history_box + i = kc.txtinput_command_line + + # check the kc initialization + assert_equal(h.text, '') + assert_equal(i.text, '') + assert_equal(kc.command_status, 'closed') + assert_not_equal(kc.prompt(), '') + + # running commands + kc.run_command('echo 1') + kc.run_command(['echo 1', 'echo 2', 'echo 3']) diff --git a/tools/pep8checker/pep8.py b/tools/pep8checker/pep8.py new file mode 100644 index 0000000..63a78e2 --- /dev/null +++ b/tools/pep8checker/pep8.py @@ -0,0 +1,1956 @@ +#!/usr/bin/env python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +r""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements +900 syntax error + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '1.3.3' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + from io import TextIOWrapper + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = 'E24' +if sys.platform == 'win32': + DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') +else: + DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or + os.path.expanduser('~/.config'), 'pep8') +MAX_LINE_LENGTH = 80 +REPORT_FORMAT = { + 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', + 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', +} + + +SINGLETONS = frozenset(['False', 'None', 'True']) +KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +BINARY_OPERATORS = frozenset([ + '**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', + '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS +WHITESPACE = frozenset(' \t') +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, + tokenize.INDENT, tokenize.DEDENT]) +BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') +COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type' + r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))') +KEYWORD_REGEX = re.compile(r'(?:[^\s])(\s*)\b(?:%s)\b(\s*)' % + r'|'.join(KEYWORDS)) +OPERATOR_REGEX = re.compile(r'(?:[^\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') +LAMBDA_REGEX = re.compile(r'\blambda\b') +HUNK_REGEX = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$') + +# Work around Python < 2.6 behaviour, which does not generate NL after +# a comment which is on a line by itself. +COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if '\t' in indent: + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1) + W291: spam(1)\s + W293: class Foo(object):\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip(' \t\v') + if physical_line != stripped: + if stripped: + return len(stripped), "W291 trailing whitespace" + else: + return 0, "W293 blank line contains whitespace" + + +#def trailing_blank_lines(physical_line, lines, line_number): +# r""" +# JCR: Trailing blank lines are superfluous. +# +# Okay: spam(1) +# W391: spam(1)\n +# """ +# if not physical_line.rstrip() and line_number == len(lines): +# return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + + Reports warning W292. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line, max_line_length): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + + Reports error E501. + """ + line = physical_line.rstrip() + length = len(line) + if length > max_line_length: + if hasattr(line, 'decode'): # Python 2 + # The line could contain multi-byte characters + try: + length = len(line.decode('utf-8')) + except UnicodeError: + pass + if length > max_line_length: + return (max_line_length, "E501 line too long " + "(%d > %d characters)" % (length, max_line_length)) + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + if previous_logical.startswith('@'): + if blank_lines: + yield 0, "E304 blank lines found after function decorator" + elif blank_lines > 2 or (indent_level and blank_lines == 2): + yield 0, "E303 too many blank lines (%d)" % blank_lines + elif logical_line.startswith(('def ', 'class ', '@')): + if indent_level: + if not (blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + yield 0, "E301 expected 1 blank line, found 0" + elif blank_lines != 2: + yield 0, "E302 expected 2 blank lines, found %d" % blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ': + # assert char in '([{' + yield found + 1, "E201 whitespace after '%s'" % char + elif line[found - 1] != ',': + code = ('E202' if char in '}])' else 'E203') # if char in ',;:' + yield found, "%s whitespace before '%s'" % (code, char) + + +def whitespace_around_keywords(logical_line): + r""" + Avoid extraneous whitespace around keywords. + + Okay: True and False + E271: True and False + E272: True and False + E273: True and\tFalse + E274: True\tand False + """ + for match in KEYWORD_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E274 tab before keyword" + elif len(before) > 1: + yield match.start(1), "E272 multiple spaces before keyword" + + if '\t' in after: + yield match.start(2), "E273 tab after keyword" + elif len(after) > 1: + yield match.start(2), "E271 multiple spaces after keyword" + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + yield index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + yield 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + yield 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + yield 0, "E113 unexpected indentation" + + +def continuation_line_indentation(logical_line, tokens, indent_level, verbose): + r""" + Continuation lines should align wrapped elements either vertically using + Python's implicit line joining inside parentheses, brackets and braces, or + using a hanging indent. + + When using a hanging indent the following considerations should be applied: + + - there should be no arguments on the first line, and + + - further indentation should be used to clearly distinguish itself as a + continuation line. + + Okay: a = (\n) + E123: a = (\n ) + + Okay: a = (\n 42) + E121: a = (\n 42) + E122: a = (\n42) + E123: a = (\n 42\n ) + E124: a = (24,\n 42\n) + E125: if (a or\n b):\n pass + E126: a = (\n 42) + E127: a = (24,\n 42) + E128: a = (24,\n 42) + """ + first_row = tokens[0][2][0] + nrows = 1 + tokens[-1][2][0] - first_row + if nrows == 1: + return + + # indent_next tells us whether the next block is indented; assuming + # that it is indented by 4 spaces, then we should not allow 4-space + # indents on the final continuation line; in turn, some other + # indents are allowed to have an extra 4 spaces. + indent_next = logical_line.endswith(':') + + row = depth = 0 + # remember how many brackets were opened on each line + parens = [0] * nrows + # relative indents of physical lines + rel_indent = [0] * nrows + # visual indents + indent = [indent_level] + indent_chances = {} + last_indent = (0, 0) + if verbose >= 3: + print((">>> " + tokens[0][4].rstrip())) + + for token_type, text, start, end, line in tokens: + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # this is the beginning of a continuation line. + last_indent = start + if verbose >= 3: + print(("... " + line.rstrip())) + + # record the initial indent. + rel_indent[row] = start[1] - indent_level + + if depth: + # a bracket expression in a continuation line. + # find the line that it was opened on + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + # an unbracketed continuation line (ie, backslash) + open_row = 0 + hang = rel_indent[row] - rel_indent[open_row] + visual_indent = indent_chances.get(start[1]) + + if token_type == tokenize.OP and text in ']})': + # this line starts with a closing bracket + if indent[depth]: + if start[1] != indent[depth]: + yield (start, 'E124 closing bracket does not match ' + 'visual indentation') + elif hang: + yield (start, 'E123 closing bracket does not match ' + 'indentation of opening bracket\'s line') + elif visual_indent is True: + # visual indent is verified + if not indent[depth]: + indent[depth] = start[1] + elif visual_indent in (text, str): + # ignore token lined up with matching one from a previous line + pass + elif indent[depth] and start[1] < indent[depth]: + # visual indent is broken + yield (start, 'E128 continuation line ' + 'under-indented for visual indent') + elif hang == 4 or (indent_next and rel_indent[row] == 8): + # hanging indent is verified + pass + else: + # indent is broken + if hang <= 0: + error = 'E122', 'missing indentation or outdented' + elif indent[depth]: + error = 'E127', 'over-indented for visual indent' + elif hang % 4: + error = 'E121', 'indentation is not a multiple of four' + else: + error = 'E126', 'over-indented for hanging indent' + yield start, "%s continuation line %s" % error + + # look for visual indenting + if parens[row] and token_type != tokenize.NL and not indent[depth]: + indent[depth] = start[1] + indent_chances[start[1]] = True + if verbose >= 4: + print(("bracket depth %s indent to %s" % (depth, start[1]))) + # deal with implicit string concatenation + elif token_type == tokenize.STRING or text in ('u', 'ur', 'b', 'br'): + indent_chances[start[1]] = str + + # keep track of bracket depth + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + if verbose >= 4: + print(("bracket depth %s seen, col %s, visual min = %s" % + (depth, start[1], indent[depth]))) + elif text in ')]}' and depth > 0: + # parent indents should not be more than this one + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth: + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + # allow to line up tokens + indent_chances[start[1]] = text + + last_token_multiline = (start[0] != end[0]) + + if indent_next and rel_indent[-1] == 4: + yield (last_indent, "E125 continuation line does not distinguish " + "itself from next logical line") + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + not keyword.iskeyword(prev_text)): + yield prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in OPERATOR_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E223 tab before operator" + elif len(before) > 1: + yield match.start(1), "E221 multiple spaces before operator" + + if '\t' in after: + yield match.start(2), "E224 tab after operator" + elif len(after) > 1: + yield match.start(2), "E222 multiple spaces after operator" + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + need_space = False + elif text == '>' and prev_text in ('<', '-'): + # Tolerate the "<>" operator, even if running Python 3 + # Deal with Python 3's annotated return value "->" + pass + else: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + if prev_text in '}])': + need_space = True + elif prev_type == tokenize.NAME: + if prev_text not in KEYWORDS: + need_space = True + elif prev_type not in SKIP_TOKENS: + need_space = True + if need_space and start == prev_end: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): + found = m.start() + 1 + if '\t' in m.group(): + yield found, "E242 tab after '%s'" % m.group()[0] + else: + yield found, "E241 multiple spaces after '%s'" % m.group()[0] + + +def whitespace_around_named_parameter_equals(logical_line, tokens): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + no_space = False + prev_end = None + for token_type, text, start, end, line in tokens: + if no_space: + no_space = False + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + elif token_type == tokenize.OP: + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + elif parens and text == '=': + no_space = True + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + prev_end = end + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + yield (prev_end, + "E261 at least two spaces before inline comment") + if text.startswith('# ') or not text.startswith('# '): + yield start, "E262 inline comment should start with '# '" + elif token_type != tokenize.NL: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if -1 < found: + yield found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + before.count('(') <= before.count(')') and # (Python 3 annotation) + not LAMBDA_REGEX.search(before)): # lambda x: x + yield found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + yield found, "E702 multiple statements on one line (semicolon)" + + +def explicit_line_join(logical_line, tokens): + r""" + Avoid explicit line join between brackets. + + The preferred way of wrapping long lines is by using Python's implied line + continuation inside parentheses, brackets and braces. Long lines can be + broken over multiple lines by wrapping expressions in parentheses. These + should be used in preference to using a backslash for line continuation. + + E502: aaa = [123, \\n 123] + E502: aaa = ("bbb " \\n "ccc") + + Okay: aaa = [123,\n 123] + Okay: aaa = ("bbb "\n "ccc") + Okay: aaa = "bbb " \\n "ccc" + """ + prev_start = prev_end = parens = 0 + for token_type, text, start, end, line in tokens: + if start[0] != prev_start and parens and backslash: + yield backslash, "E502 the backslash is redundant between brackets" + if end[0] != prev_end: + if line.rstrip('\r\n').endswith('\\'): + backslash = (end[0], len(line.splitlines()[-1]) - 1) + else: + backslash = None + prev_start = prev_end = end[0] + else: + prev_start = start[0] + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in ')]}': + parens -= 1 + + +def comparison_to_singleton(logical_line): + """ + Comparisons to singletons like None should always be done + with "is" or "is not", never the equality operators. + + Okay: if arg is not None: + E711: if arg != None: + E712: if arg == True: + + Also, beware of writing if x when you really mean if x is not None -- + e.g. when testing whether a variable or argument that defaults to None was + set to some other value. The other value might have a type (such as a + container) that could be false in a boolean context! + """ + match = COMPARE_SINGLETON_REGEX.search(logical_line) + if match: + same = (match.group(1) == '==') + singleton = match.group(2) + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) + if singleton in ('None',): + code = 'E711' + else: + code = 'E712' + nonzero = ((singleton == 'True' and same) or + (singleton == 'False' and not same)) + msg += " or 'if %scond:'" % ('' if nonzero else 'not ') + yield match.start(1), ("%s comparison to %s should be %s" % + (code, singleton, msg)) + + +def comparison_type(logical_line): + """ + Object type comparisons should always use isinstance() instead of + comparing types directly. + + Okay: if isinstance(obj, int): + E721: if type(obj) is type(1): + + When checking if an object is a string, keep in mind that it might be a + unicode string too! In Python 2.3, str and unicode have a common base + class, basestring, so you can do: + + Okay: if isinstance(obj, basestring): + Okay: if type(a1) is type(b1): + """ + match = COMPARE_TYPE_REGEX.search(logical_line) + if match: + inst = match.group(3) + if inst and isidentifier(inst) and inst not in SINGLETONS: + return # Allow comparison for types which are not obvious + yield match.start(1), "E721 do not compare types, use 'isinstance()'" + + +def python_3000_has_key(logical_line): + r""" + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead. + + Okay: if "alph" in d:\n print d["alph"] + W601: assert d.has_key('alph') + """ + pos = logical_line.find('.has_key(') + if pos > -1: + yield pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + + Okay: raise DummyError("Message") + W602: raise DummyError, "Message" + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match and not RERAISE_COMMA_REGEX.match(logical_line): + yield match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + + Okay: if a != 'no': + W603: if a <> 'no': + """ + pos = logical_line.find('<>') + if pos > -1: + yield pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + + Okay: val = repr(1 + 2) + W604: val = `1 + 2` + """ + pos = logical_line.find('`') + if pos > -1: + yield pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + f = open(filename) + try: + return f.readlines() + finally: + f.close() + + isidentifier = re.compile(r'[a-zA-Z_]\w*').match + stdin_get_value = sys.stdin.read +else: + # Python 3 + def readlines(filename): + f = open(filename, 'rb') + try: + coding, lines = tokenize.detect_encoding(f.readline) + f = TextIOWrapper(f, coding, line_buffering=True) + return [l.decode(coding) for l in lines] + f.readlines() + except (LookupError, SyntaxError, UnicodeError): + f.close() + # Fall back if files are improperly declared + f = open(filename, encoding='latin-1') + return f.readlines() + finally: + f.close() + + isidentifier = str.isidentifier + stdin_get_value = TextIOWrapper(sys.stdin.buffer, errors='ignore').read +readlines.__doc__ = " Read the source code." + + +def expand_indent(line): + r""" + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + if '\t' not in line: + return len(line) - len(line.lstrip()) + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + # String modifiers (e.g. u or r) + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Triple quotes + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def parse_udiff(diff, patterns=None, parent='.'): + rv = {} + path = nrows = None + for line in diff.splitlines(): + if nrows: + if line[:1] != '-': + nrows -= 1 + continue + if line[:3] == '@@ ': + row, nrows = [int(g) for g in HUNK_REGEX.match(line).groups()] + rv[path].update(list(range(row, row + nrows))) + elif line[:3] == '+++': + path = line[4:].split('\t', 1)[0] + if path[:2] == 'b/': + path = path[2:] + rv[path] = set() + return dict([(os.path.join(parent, path), rows) + for (path, rows) in list(rv.items()) + if rows and filename_match(path, patterns)]) + + +def filename_match(filename, patterns, default=True): + """ + Check if patterns contains a pattern that matches filename. + If patterns is unspecified, this always returns True. + """ + if not patterns: + return default + return any(fnmatch(filename, pattern) for pattern in patterns) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + for name, function in list(globals().items()): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(function.__doc__ or '') + yield name, codes, function, args + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename, lines=None, + options=None, report=None, **kwargs): + if options is None: + options = StyleGuide(kwargs).options + else: + assert not kwargs + self._io_error = None + self._physical_checks = options.physical_checks + self._logical_checks = options.logical_checks + self.max_line_length = options.max_line_length + self.verbose = options.verbose + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif lines is None: + try: + self.lines = readlines(filename) + except IOError: + exc_type, exc = sys.exc_info()[:2] + self._io_error = '%s: %s' % (exc_type.__name__, exc) + self.lines = [] + else: + self.lines = lines + self.report = report or options.report + self.report_error = self.report.error + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and line[:1] in WHITESPACE: + self.indent_char = line[0] + for name, check, argument_names in self._physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_row, end = previous[3] + start_row, start = token[2] + if end_row != start_row: # different row + prev_text = self.lines[end_row - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_row - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.strip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + self.build_tokens_line() + self.report.increment_logical_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if self.verbose >= 2: + print((self.logical_line[:80].rstrip())) + for name, check, argument_names in self._logical_checks: + if self.verbose >= 4: + print((' ' + name)) + for result in self.run_check(check, argument_names): + offset, text = result + if isinstance(offset, tuple): + orig_number, orig_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + orig_number = token[2][0] + orig_offset = (token[2][1] + offset - token_offset) + self.report_error(orig_number, orig_offset, text, check) + self.previous_logical = self.logical_line + + def generate_tokens(self): + if self._io_error: + self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) + tokengen = tokenize.generate_tokens(self.readline_check_physical) + try: + for token in tokengen: + yield token + except (SyntaxError, tokenize.TokenError): + exc_type, exc = sys.exc_info()[:2] + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + self.report_error(offset[0], offset[1], + 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), + self.generate_tokens) + generate_tokens.__doc__ = " Check if the syntax is valid." + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.report.init_file(self.filename, self.lines, expected, line_offset) + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + parens = 0 + for token in self.generate_tokens(): + self.tokens.append(token) + token_type, text = token[0:2] + if self.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print(('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], text))) + if token_type == tokenize.COMMENT or token_type == tokenize.STRING: + for sre in re.finditer(r"[:.;,] ?[A-Za-z]", text): + pos = sre.span()[0] + part = text[:pos] + line = token[2][0] + part.count('\n') + offset = 0 if part.count('\n') > 0 else token[2][1] + col = offset + pos - part.rfind('\n') + 1 + if sre.group(0)[0] == '.': + self.report_error(line, col, + 'E289 Too many spaces after period. Use only one.', + check=None) + elif sre.group(0)[0] == ',': + self.report_error(line, col, + 'E288 Too many spaces after comma. Use only one.', + check=None) + else: + self.report_error(line, col, + 'E287 Too many spaces after punctuation. ' + 'Use only one.', + check=None) + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in '}])': + parens -= 1 + elif not parens: + if token_type == tokenize.NEWLINE: + if self.blank_lines < blank_lines_before_comment: + self.blank_lines = blank_lines_before_comment + self.check_logical() + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + elif token_type == tokenize.NL: + if len(self.tokens) == 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + elif token_type == tokenize.COMMENT and len(self.tokens) == 1: + if blank_lines_before_comment < self.blank_lines: + blank_lines_before_comment = self.blank_lines + self.blank_lines = 0 + if COMMENT_WITH_NL: + # The comment also ends a physical line + self.tokens = [] + if self.blank_lines > 1: + self.report_error(token[2][0],0, + 'E389 File ends in multiple blank lines', + check=None) + + return self.report.get_file_results() + + +class BaseReport(object): + """Collect the results of the checks.""" + print_filename = False + + def __init__(self, options): + self._benchmark_keys = options.benchmark_keys + self._ignore_code = options.ignore_code + # Results + self.elapsed = 0 + self.total_errors = 0 + self.counters = dict.fromkeys(self._benchmark_keys, 0) + self.messages = {} + + def start(self): + """Start the timer.""" + self._start_time = time.time() + + def stop(self): + """Stop the timer.""" + self.elapsed = time.time() - self._start_time + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self.filename = filename + self.lines = lines + self.expected = expected or () + self.line_offset = line_offset + self.file_errors = 0 + self.counters['files'] += 1 + self.counters['physical lines'] += len(lines) + + def increment_logical_line(self): + """Signal a new logical line.""" + self.counters['logical lines'] += 1 + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = text[:4] + if self._ignore_code(code): + return + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print((self.filename)) + self.file_errors += 1 + self.total_errors += 1 + return code + + def get_file_results(self): + """Return the count of errors and warnings for this file.""" + return self.file_errors + + def get_count(self, prefix=''): + """Return the total count of errors and warnings.""" + return sum([self.counters[key] + for key in self.messages if key.startswith(prefix)]) + + def get_statistics(self, prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) + for key in sorted(self.messages) if key.startswith(prefix)] + + def print_statistics(self, prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in self.get_statistics(prefix): + print(line) + + def print_benchmark(self): + """Print benchmark numbers.""" + print(('%-7.2f %s' % (self.elapsed, 'seconds elapsed'))) + if self.elapsed: + for key in self._benchmark_keys: + print(('%-7d %s per second (%d total)' % + (self.counters[key] / self.elapsed, key, + self.counters[key]))) + + +class FileReport(BaseReport): + print_filename = True + + +class StandardReport(BaseReport): + """Collect and print the results of the checks.""" + + def __init__(self, options): + super(StandardReport, self).__init__(options) + self._fmt = REPORT_FORMAT.get(options.format.lower(), + options.format) + self._repeat = options.repeat + self._show_source = options.show_source + self._show_pep8 = options.show_pep8 + + def error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = super(StandardReport, self).error(line_number, offset, + text, check) + if code and (self.counters[code] == 1 or self._repeat): + print((self._fmt % { + 'path': self.filename, + 'row': self.line_offset + line_number, 'col': offset + 1, + 'code': code, 'text': text[5:], + })) + if self._show_source: + if line_number > len(self.lines): + line = '' + else: + line = self.lines[line_number - 1] + print((line.rstrip())) + print((' ' * offset + '^')) + if self._show_pep8 and check is not None: + print((check.__doc__.lstrip('\n').rstrip())) + return code + + +class DiffReport(StandardReport): + """Collect and print the results for the changed lines only.""" + + def __init__(self, options): + super(DiffReport, self).__init__(options) + self._selected = options.selected_lines + + def error(self, line_number, offset, text, check): + if line_number not in self._selected[self.filename]: + return + return super(DiffReport, self).error(line_number, offset, text, check) + + +class TestReport(StandardReport): + """Collect the results for the tests.""" + + def __init__(self, options): + options.benchmark_keys += ['test cases', 'failed tests'] + super(TestReport, self).__init__(options) + self._verbose = options.verbose + + def get_file_results(self): + # Check if the expected errors were found + label = '%s:%s:1' % (self.filename, self.line_offset) + codes = sorted(self.expected) + for code in codes: + if not self.counters.get(code): + self.file_errors += 1 + self.total_errors += 1 + print(('%s: error %s not found' % (label, code))) + if self._verbose and not self.file_errors: + print(('%s: passed (%s)' % + (label, ' '.join(codes) or 'Okay'))) + self.counters['test cases'] += 1 + if self.file_errors: + self.counters['failed tests'] += 1 + # Reset counters + for key in set(self.counters) - set(self._benchmark_keys): + del self.counters[key] + self.messages = {} + return self.file_errors + + def print_results(self): + results = ("%(physical lines)d lines tested: %(files)d files, " + "%(test cases)d test cases%%s." % self.counters) + if self.total_errors: + print((results % ", %s failures" % self.total_errors)) + else: + print((results % "")) + print(("Test failed." if self.total_errors else "Test passed.")) + + +class StyleGuide(object): + """Initialize a PEP-8 instance with few options.""" + + def __init__(self, *args, **kwargs): + # build options from the command line + parse_argv = kwargs.pop('parse_argv', False) + config_file = kwargs.pop('config_file', None) + options, self.paths = process_options(parse_argv=parse_argv, + config_file=config_file) + if args or kwargs: + # build options from dict + options_dict = dict(*args, **kwargs) + options.__dict__.update(options_dict) + if 'paths' in options_dict: + self.paths = options_dict['paths'] + + self.runner = self.input_file + self.options = options + + if not options.reporter: + options.reporter = BaseReport if options.quiet else StandardReport + + for index, value in enumerate(options.exclude): + options.exclude[index] = value.rstrip('/') + # Ignore all checks which are not explicitly selected + options.select = tuple(options.select or ()) + options.ignore = tuple(options.ignore or options.select and ('',)) + options.benchmark_keys = BENCHMARK_KEYS[:] + options.ignore_code = self.ignore_code + options.physical_checks = self.get_checks('physical_line') + options.logical_checks = self.get_checks('logical_line') + self.init_report() + + def init_report(self, reporter=None): + """Initialize the report instance.""" + self.options.report = (reporter or self.options.reporter)(self.options) + return self.options.report + + def check_files(self, paths=None): + """Run all checks on the paths.""" + if paths is None: + paths = self.paths + report = self.options.report + runner = self.runner + report.start() + for path in paths: + if os.path.isdir(path): + self.input_dir(path) + elif not self.excluded(path): + runner(path) + report.stop() + return report + + def input_file(self, filename, lines=None, expected=None, line_offset=0): + """Run all checks on a Python source file.""" + if self.options.verbose: + print(('checking %s' % filename)) + fchecker = Checker(filename, lines=lines, options=self.options) + return fchecker.check_all(expected=expected, line_offset=line_offset) + + def input_dir(self, dirname): + """Check all files in this directory and all subdirectories.""" + dirname = dirname.rstrip('/') + if self.excluded(dirname): + return 0 + counters = self.options.report.counters + verbose = self.options.verbose + filepatterns = self.options.filename + runner = self.runner + for root, dirs, files in os.walk(dirname): + if verbose: + print(('directory ' + root)) + counters['directories'] += 1 + for subdir in sorted(dirs): + if self.excluded(subdir): + dirs.remove(subdir) + for filename in sorted(files): + # contain a pattern that matches? + if ((filename_match(filename, filepatterns) and + not self.excluded(filename))): + runner(os.path.join(root, filename)) + + def excluded(self, filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + return filename_match(basename, self.options.exclude, default=False) + + def ignore_code(self, code): + """ + Check if the error code should be ignored. + + If 'options.select' contains a prefix of the error code, + return False. Else, if 'options.ignore' contains a prefix of + the error code, return True. + """ + return (code.startswith(self.options.ignore) and + not code.startswith(self.options.select)) + + def get_checks(self, argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name and which contain selected tests. + """ + checks = [] + for name, codes, function, args in find_checks(argument_name): + if any(not (code and self.ignore_code(code)) for code in codes): + checks.append((name, function, args)) + return sorted(checks) + + +def init_tests(pep8style): + """ + Initialize testing framework. + + A test file can provide many tests. Each test starts with a + declaration. This declaration is a single line starting with '#:'. + It declares codes of expected failures, separated by spaces or 'Okay' + if no failure is expected. + If the file does not contain such declaration, it should pass all + tests. If the declaration is empty, following lines are not checked, + until next declaration. + + Examples: + + * Only E224 and W701 are expected: #: E224 W701 + * Following example is conform: #: Okay + * Don't check these lines: #: + """ + report = pep8style.init_report(TestReport) + runner = pep8style.input_file + + def run_tests(filename): + """Run all the tests from a file.""" + lines = readlines(filename) + ['#:\n'] + line_offset = 0 + codes = ['Okay'] + testcase = [] + count_files = report.counters['files'] + for index, line in enumerate(lines): + if not line.startswith('#:'): + if codes: + # Collect the lines of the test case + testcase.append(line) + continue + if codes and index: + codes = [c for c in codes if c != 'Okay'] + # Run the checker + runner(filename, testcase, expected=codes, + line_offset=line_offset) + # output the real line numbers + line_offset = index + 1 + # configure the expected errors + codes = line.split()[1:] + # empty the test case buffer + del testcase[:] + report.counters['files'] = count_files + 1 + return report.counters['failed tests'] + + pep8style.runner = run_tests + + +def selftest(options): + """ + Test all check functions with test cases in docstrings. + """ + count_failed = count_all = 0 + report = BaseReport(options) + counters = report.counters + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None, options=options, report=report) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + checker.check_all() + error = None + if code == 'Okay': + if len(counters) > len(options.benchmark_keys): + codes = [key for key in counters + if key not in options.benchmark_keys] + error = "incorrectly found %s" % ', '.join(codes) + elif not counters.get(code): + error = "failed to find %s" % code + # Keep showing errors for multiple tests + for key in set(counters) - set(options.benchmark_keys): + del counters[key] + report.messages = {} + count_all += 1 + if not error: + if options.verbose: + print(("%s: %s" % (code, source))) + else: + count_failed += 1 + print(("%s: %s:" % (__file__, error))) + for line in checker.lines: + print((line.rstrip())) + return count_failed, count_all + + +def read_config(options, args, arglist, parser): + """Read both user configuration and local configuration.""" + config = RawConfigParser() + + user_conf = options.config + if user_conf and os.path.isfile(user_conf): + if options.verbose: + print(('user configuration: %s' % user_conf)) + config.read(user_conf) + + parent = tail = args and os.path.abspath(os.path.commonprefix(args)) + while tail: + local_conf = os.path.join(parent, '.pep8') + if os.path.isfile(local_conf): + if options.verbose: + print(('local configuration: %s' % local_conf)) + config.read(local_conf) + break + parent, tail = os.path.split(parent) + + if config.has_section('pep8'): + option_list = dict([(o.dest, o.type or o.action) + for o in parser.option_list]) + + # First, read the default values + new_options, _ = parser.parse_args([]) + + # Second, parse the configuration + for opt in config.options('pep8'): + if options.verbose > 1: + print((' %s = %s' % (opt, config.get('pep8', opt)))) + if opt.replace('_', '-') not in parser.config_options: + print(('Unknown option: \'%s\'\n not in [%s]' % + (opt, ' '.join(parser.config_options)))) + sys.exit(1) + normalized_opt = opt.replace('-', '_') + opt_type = option_list[normalized_opt] + if opt_type in ('int', 'count'): + value = config.getint('pep8', opt) + elif opt_type == 'string': + value = config.get('pep8', opt) + else: + assert opt_type in ('store_true', 'store_false') + value = config.getboolean('pep8', opt) + setattr(new_options, normalized_opt, value) + + # Third, overwrite with the command-line options + options, _ = parser.parse_args(arglist, values=new_options) + + return options + + +def process_options(arglist=None, parse_argv=False, config_file=None): + """Process options passed either via arglist or via command line args.""" + if not arglist and not parse_argv: + # Don't read the command line if the module is used as a library. + arglist = [] + if config_file is True: + config_file = DEFAULT_CONFIG + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.config_options = [ + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', + 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'] + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', default=True, action='store_true', + help="(obsolete) show all occurrences of the same error") + parser.add_option('--first', action='store_false', dest='repeat', + help="show first occurrence of each error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %default)") + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns " + "(default: %default)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error " + "(implies --first)") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--max-line-length', type='int', metavar='n', + default=MAX_LINE_LENGTH, + help="set maximum allowed line length " + "(default: %default)") + parser.add_option('--format', metavar='format', default='default', + help="set the error format [default|pylint|]") + parser.add_option('--diff', action='store_true', + help="report only lines changed according to the " + "unified diff received on STDIN") + group = parser.add_option_group("Testing Options") + group.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + group.add_option('--doctest', action='store_true', + help="run doctest on myself") + group.add_option('--benchmark', action='store_true', + help="measure processing speed") + group = parser.add_option_group("Configuration", description=( + "The project options are read from the [pep8] section of the .pep8 " + "file located in any parent folder of the path(s) being processed. " + "Allowed options are: %s." % ', '.join(parser.config_options))) + group.add_option('--config', metavar='path', default=config_file, + help="config file location (default: %default)") + + options, args = parser.parse_args(arglist) + options.reporter = None + + if options.testsuite: + args.append(options.testsuite) + elif not options.doctest: + if parse_argv and not args: + if os.path.exists('.pep8') or options.diff: + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport + + if options.filename: + options.filename = options.filename.split(',') + options.exclude = options.exclude.split(',') + if options.select: + options.select = options.select.split(',') + if options.ignore: + options.ignore = options.ignore.split(',') + elif not (options.select or + options.testsuite or options.doctest) and DEFAULT_IGNORE: + # The default choice: ignore controversial checks + # (for doctest and testsuite, all checks are required) + options.ignore = DEFAULT_IGNORE.split(',') + + if options.diff: + options.reporter = DiffReport + stdin = stdin_get_value() + options.selected_lines = parse_udiff(stdin, options.filename, args[0]) + args = sorted(options.selected_lines) + + return options, args + + +def _main(): + """Parse options and run checks on Python source.""" + pep8style = StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + if options.doctest: + import doctest + fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose) + fail_s, done_s = selftest(options) + count_failed = fail_s + fail_d + if not options.quiet: + count_passed = done_d + done_s - count_failed + print(("%d passed and %d failed." % (count_passed, count_failed))) + print(("Test failed." if count_failed else "Test passed.")) + if count_failed: + sys.exit(1) + if options.testsuite: + init_tests(pep8style) + report = pep8style.check_files() + if options.statistics: + report.print_statistics() + if options.benchmark: + report.print_benchmark() + if options.testsuite and not options.quiet: + report.print_results() + if report.total_errors: + if options.count: + sys.stderr.write(str(report.total_errors) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/tools/pep8checker/pep8base.html b/tools/pep8checker/pep8base.html new file mode 100644 index 0000000..e69ca6f --- /dev/null +++ b/tools/pep8checker/pep8base.html @@ -0,0 +1,70 @@ + + + Kivy Styleguide Check + + + +

+

Kivy Styleguide (PEP8) Check

+
+ diff --git a/tools/pep8checker/pep8kivy.py b/tools/pep8checker/pep8kivy.py new file mode 100644 index 0000000..8657bf6 --- /dev/null +++ b/tools/pep8checker/pep8kivy.py @@ -0,0 +1,111 @@ +import sys +from os import walk +from os.path import isdir, join, abspath, dirname +import pep8 +import time + +htmlmode = False + +pep8_ignores = ( + 'E125', # continuation line does not + # distinguish itself from next logical line + 'E126', # continuation line over-indented for hanging indent + 'E127', # continuation line over-indented for visual indent + 'E128') # continuation line under-indented for visual indent + +class KivyStyleChecker(pep8.Checker): + + def __init__(self, filename): + pep8.Checker.__init__(self, filename, ignore=pep8_ignores) + + def report_error(self, line_number, offset, text, check): + if htmlmode is False: + return pep8.Checker.report_error(self, + line_number, offset, text, check) + + # html generation + print('{0}{1}'.format(line_number, text)) + + +if __name__ == '__main__': + + def usage(): + print('Usage: python pep8kivy.py [-html] *') + print('Folders will be checked recursively.') + sys.exit(1) + + if len(sys.argv) < 2: + usage() + if sys.argv[1] == '-html': + if len(sys.argv) < 3: + usage() + else: + htmlmode = True + targets = sys.argv[-1].split() + elif sys.argv == 2: + targets = sys.argv[-1] + else: + targets = sys.argv[-1].split() + + def check(fn): + try: + checker = KivyStyleChecker(fn) + except IOError: + # File couldn't be opened, so was deleted apparently. + # Don't check deleted files. + return 0 + return checker.check_all() + + errors = 0 + exclude_dirs = ['/lib', '/coverage', '/pep8', '/doc', '/.designer', + '/.buildozer'] + exclude_files = ['kivy/gesture.py', 'osx/build.py', 'win32/build.py', + 'kivy/tools/stub-gl-debug.py', + 'kivy/modules/webdebugger.py', + 'kivy/modules/_webdebugger.py', + 'designer/utils/toolbox_widgets.py'] + for target in targets: + if isdir(target): + if htmlmode: + path = join(dirname(abspath(__file__)), 'pep8base.html') + print(open(path, 'r').read()) + print('''

Generated: %s

''' % (time.strftime('%c'))) + + for dirpath, dirnames, filenames in walk(target): + cont = False + for pat in exclude_dirs: + if pat in dirpath: + cont = True + break + if cont: + continue + for filename in filenames: + if not filename.endswith('.py'): + continue + cont = False + complete_filename = join(dirpath, filename) + for pat in exclude_files: + if complete_filename.endswith(pat): + cont = True + if cont: + continue + + if htmlmode: + print('' \ + % complete_filename) + errors += check(complete_filename) + + if htmlmode: + print('
%s
') + + else: + # Got a single file to check + for pat in exclude_dirs + exclude_files: + if pat in target: + break + else: + if target.endswith('.py'): + errors += check(target) + + # If errors is 0 we return with 0. That's just fine. + sys.exit(errors) diff --git a/tools/pep8checker/pre-commit.githook b/tools/pep8checker/pre-commit.githook new file mode 100644 index 0000000..926d663 --- /dev/null +++ b/tools/pep8checker/pre-commit.githook @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +''' + Kivy Git Pre-Commit Hook to Enforce Styleguide + ============================================== + + This script is not supposed to be run directly. + Instead, copy it to your kivy/.git/hooks/ directory, call it 'pre-commit' + and make it executable. + + If you attempt to commit, git will run this script, which in turn will run + the styleguide checker over your code and abort the commit if there are any + errors. If that happens, please fix & retry. + + To install:: + + cp kivy/tools/pep8checker/pre-commit.githook .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit +''' + +import sys, os +from os.path import dirname, abspath, sep, join +from subprocess import call, Popen, PIPE + +curdir = dirname(abspath(__file__)) +kivydir = sep.join(curdir.split(sep)[:-2]) +srcdir = kivydir +script = join(srcdir, 'tools', 'pep8checker', 'pep8kivy.py') +try: + with open(script): pass +except IOError: + # if this not the kivy project, find the script file in the kivy project + os.environ['KIVY_NO_CONSOLELOG'] = '1' + import designer + script = join(dirname(designer.__file__), 'tools', 'pep8checker', 'pep8kivy.py') + srcdir = '' + +# Only check the files that were staged +#proc = Popen(['git', 'diff', '--cached', '--name-only', 'HEAD'], stdout=PIPE) +#targets = [join(kivydir, target) for target in proc.stdout] + +# Correction: only check the files that were staged, but do not include +# deleted files. +proc = Popen(['git', 'diff', '--cached', '--name-status', 'HEAD'], stdout=PIPE) +proc.wait() + +# This gives output like the following: +# +# A examples/widgets/lists/list_simple_in_kv.py +# A examples/widgets/lists/list_simple_in_kv_2.py +# D kivy/uix/observerview.py +# +# So check for D entries and remove them from targets. +# +targets = [] +for target in proc.stdout: + parts = [p.strip() for p in target.split()] + if parts[0] != 'D': + targets.append(join(kivydir, target.decode(encoding='UTF-8'))) + +# Untested possibility: After making the changes above for removing deleted +# files from targets, saw also where the git diff call could be: +# +# git diff --cached --name-only --diff-filter=ACM +# (leaving off D) +# +# and we could then remove the special handling in python for targets above. + +call(['git', 'stash', 'save', '--keep-index', '--quiet']) +retval = call([sys.executable, script, srcdir] + targets) +call(['git', 'stash', 'pop', '--quiet']) + +if retval: + # There are styleguide violations + print("Error:", retval, "styleguide violation(s) encountered!") + print("Your commit has been aborted. Please fix the violations and retry.") + sys.exit(retval) + diff --git a/workspace.code-workspace b/workspace.code-workspace new file mode 100644 index 0000000..362d7c2 --- /dev/null +++ b/workspace.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file

TdU9i%B<@w5@hjd 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 literal 0 HcmV?d00001 diff --git a/docs/source/img/kd_interface.png b/docs/source/img/kd_interface.png new file mode 100644 index 0000000000000000000000000000000000000000..a2c1672579fdd9069356b16f4a993fe66a56d754 GIT binary patch literal 103510 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%0A+?SHJ^{yL1{MX=P+;l;wF^w;5 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 + installation + quickstart + buildozer + tools + contribute + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..0eff2e9 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,50 @@ +Installation +============ + + +Prerequisites +------------- + +- `Kivy 1.9+ `_ +- The following Python modules (available via pip): + - `watchdog `_ + - `Pygments `_ + - `docutils `_ + - `jedi `_ + - `gitpython `_ + - `six `_ + - `kivy-garden `_ +- The XPopup widget from the [Kivy garden](https://github.com/kivy-garden/garden.xpopup) + +Installation +------------ + +Download the Kivy Designer's source code: + +:: + + git clone http://github.com/kivy/kivy-designer/ + +or, download it manually from https://github.com/kivy/kivy-designer/archive/master.zip and extract to +`kivy-designer` + +Open the downloaded folder and install the required prerequisites: + +:: + + cd kivy-designer + pip install -Ur requirements.txt + +To install the XPopup enter a console (on Windows use kivy.bat in the kivy folder): + +:: + + garden install xpopup + +With the prerequisites installed, you can use the designer: + +:: + + python -m designer + +On OS X you might need to use `kivy` command instead of `Python` if you are using our portable package. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..d3ea91b --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,180 @@ +Quick-start +=========== + +Let's know more about Kivy Designer! + + +How it works +------------ + +Kivy Designer organizes some open source tools to help you to create Kivy UI easily, develop your applications and target multiple platforms. + +Creating a new project +~~~~~~~~~~~~~~~~~~~~~~ + +To create a new project, you can use: + * In **Start Page**, there is a ``New Project`` button. + * In the menu ``File -> New Project`` + +This is the **New Project** wizard: + +.. image:: img/kd_new_project.png + +Where you can select an initial template for your project. + +After creating it, you'll see the UI Creator. You can start editing the app UI, or edit the ``main.py``. + + +Kivy Designer Interface +----------------------- + +This is a list and overview of some Kivy Designer's components + +After opening a project, you will see following: + 1. **Project Tree** on the left side, shows files and folders inside the project's directory. + 2. **Toolbox** contains widgets which could be drag-drop to the required positions. + 3. **UI Creator** is place where you will be designing your project. + 4. **Widget Tree** shows the Widget hierarchy of the project. + 5. **Property Viewer** shows properties, their values and allows changing the values. + 6. **Events** shows the available events and their event handler. You can change/set an event handler and add an event. + 7. **KV Lang Area** shows what your kv file would be consisting. + 8. **Kivy Console** is a console just like xterm, GNOME Terminal. You can enter commands and execute them. + 9. **Python Shell** is an interactive Python Shell. + 10. **Error Console** shows errors which may occur in the user code, while opening a project or creating custom widget. + 11. **Playground Settings** you can change the playground screen size, orientation and zoom to help the UI development + 12. **Status Bar** The status bar helps you displaying the selected widget hierarchy and messages. + +.. image:: img/kd_interface.png + + +UI Creator +---------- + +You'll probably spend a big part of your time designing the app interface; so the UI creator is the right place for you :) + +When designing the UI, you can get Widget from **Widget Tree** or you insert the KV Lang code in **KV Lang Area** + +If you want to change the size or orientaiton of the emulated interface, you can set it on **Playground Settings** + +.. image:: img/kd_playground_settings.png + +Building +-------- + +To build, and run your project, you'll need to configure the Kivy Designer Builder. The Builder will help you to target your application to the desired platforms. +You can access Builder settings at ``Run -> Edit Profiles...`` + +.. _Builder: + +Builders +~~~~~~~~ +You can use the following tools to build your project: + + * **Desktop** - This is the default Python interpreter available in your system. (Desktop only) + * **Buildozer** - Use `Buildozer `_ to target mobile devices. (Android and iOS) + * **Hanga** - Use Hanga to target mobile devices. (Android) + +Build Profiles +~~~~~~~~~~~~~~ +You can select and configure your Builder using Build Profiles. + +.. image:: img/kd_build_profiles.png + +Kivy Designer already provides 3 defaults profiles: + + * Desktop + * Android - Buildozer + * iOS - Buildozer + +You can edit/delete these profiles and create new ones. To use a profile, click in the button ``Use this profile`` or select the profile from the menu ``Run -> Select Profile`` + +Editing a profile +~~~~~~~~~~~~~~~~~ + +Before edit a build profile, it's a good idea to know what you are editing :) Take a look on what each field represents + + * **Name** - Name of the profile. This name will be visible in the profiles list. + * **Builder** - Select which Builder_ do you want to use. + * **Target** - Select the target platform. IMPORTANT: Just make sure that the selected Builder_ supports the desired platform. + * **Mode** - Used by Buildozer and Hanga only. This sets the build mode, Debug or Release. + * **Install On Device** - If you are targeting a mobile device, this tool allows you to auto install the application every build. + * **Debug** - If activated and targeting Android, will show the logcat output on Kivy Console. + * **Verbose** - If activated, will run your Builder_ on verbose mode. + +Run +~~~ + +The ``Run`` menu provides you some options. Take a look in the table bellow to see how it works with each Builder_ + ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| | **Desktop** | **Buildozer** | **Hanga** | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Run** | Run *main.py* with Python interpreter | Build, install and run on target device | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Stop** | Stop the Python interpreter | Nothing | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Clean** | Removes all .pyc and __pycache__ | Clean the Buildozer build | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +| **Build** | Generate .pyc | Build the project. If ``Install On Device``| Not yet implemented | +| | | is set, install it on device. | | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ +|**Rebuild**| Run ``Clean`` and the ``Build`` | Run ``Clean`` and the ``Build`` | Not yet implemented | ++-----------+---------------------------------------+--------------------------------------------+------------------------------------------+ + +Modules +------- + +While developing your application, Kivy provides some `extra modules `_ to help you. + +Kivy Designer has an interface to some of `these modules `_ . + +To use Kivy Modules you must target Desktop, select the desired module at ``Run -> Run with module...``. + +Screen Emulation +~~~~~~~~~~~~~~~~ + +It's really important to see your application running in different screen sizes, dimensions and orientations. + +Kivy Designer provides a simple interface to the `Screen Module `_. + +This module provides some settings. You can change the ``Device``, ``Orientation`` and ``Scale``. And the just press ``Run`` to run your application with Screen Module. + +Touchring +~~~~~~~~~ + +The `Touchring Module `_ shows rings around every touch on the surface / screen. + +You can use this module to check that you don’t have any calibration issues with touches. + +Monitor +~~~~~~~ + +The `Monitor Module `_ is a toolbar that shows the activity of your current application. + +Inspector +~~~~~~~~~ + +.. note:: + `This module is highly experimental, use it with care.` + +The `Inspector Module `_ is a tool for finding a widget in the widget tree by clicking or tapping on it. + +After running your app, you can access the Inspector with: + + - "Ctrl + e": activate / deactivate the inspector view + - "Escape": cancel widget lookup first, then hide the inspector view + +Available inspector interactions: + + - tap once on a widget to select it without leaving inspect mode + - double tap on a widget to select and leave inspect mode (then you can manipulate the widget again) + +.. warning:: + Some properties can be edited live. However, due to the delayed usage of some properties, it might crash if you don’t handle all the cases. + +Web Debugger +~~~~~~~~~~~~ + +The `Web Debugger Module `_ starts a webserver and run in the background. You can see how your application evolves during runtime, examine the internal cache etc. + +To access the debugger, Kivy Designer will open http://localhost:5000/ \ No newline at end of file diff --git a/docs/source/tools.rst b/docs/source/tools.rst new file mode 100644 index 0000000..43bae7e --- /dev/null +++ b/docs/source/tools.rst @@ -0,0 +1,78 @@ +Kivy Designer's Tools +===================== + +This section explain how to use Kivy Designer's tools. Each tool tries to simplify a process of the development. + + +Create setup.py +--------------- + +This is a helper to auto create a setup.py file in the root of the project. + +You can access it in the menu ``Tools -> Create setup.py`` + +.. image:: img/kd_setup_y.png + + +Check PEP8 +---------- + +This tool will check the PEP8 of the current project. It's run on Kivy Console, so you need to check the Kivy Console to see the PEP8 checker status. + + +Export .PNG +----------- + +This is a helper to create a .png image from the Kivy Designer's Playground. While developing an application, if you want to save your current design in a image, use ``Tools -> Export .PNG`` + +If there is a selected widget on Playground, this widget will be exported. If there is no selected widget, the RootWidget will be exported. + +The .png will be saved in the root folder of the project, and the file name will be displayed on the Status Bar. + + +Git +--- + +Kivy Designer provides some Git shortcuts to help you with your project versioning. You can get Git tools on ``Tools -> Git`` + +If the project is not a git repo, you will see the ``Init`` button. Otherwise, you'll have the following tools available. + +Commit +~~~~~~ +Commit the current repository. + +Add +~~~ +Opens a list with untracked files. You can select the desired files to add to the Git repo. + +Branches +~~~~~~~~ +Opens a list with repo branches. You can select any branch to do a checkout. Or you can type a custom name and select it to create a new branch and do checkout to it. + +Diff +~~~~ +Shows the project's modification on a Code Input + +Pull/Push +~~~~~~~~~ + +To work with remote repositories, you'll need to generate and configure a SSH key. `Read more about it here. `_ + +If you have a SSH key working, you can pull and push data from remote repositories. These buttons will display a list of available remotes. Select a remote to pull or push data. + +.. note:: + + If you are using Windows, you may see a CMD window asking for SSH password before remote actions. + + +Bug Reporter +------------ +We hope that you never use this tool, but let's know about it. + +If Kivy Designer finds a bug, you'll see the following screen: + +.. image:: img/kd_bug_reporter.png + +The ``Copy to clipboard`` button copies the traceback message to the clipboard. + +And if you want to help us to fix it, the ``Report Bug`` button will open a Github page to submit this issue. \ No newline at end of file diff --git a/kivy_designer.png b/kivy_designer.png new file mode 100644 index 0000000000000000000000000000000000000000..32f023a7c2bffa5aa1b64d4642011da585379f7d GIT binary patch literal 99850 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