Repository: BillBillBillBill/Tickeys-linux Branch: master Commit: 2df31b866500 Files: 537 Total size: 4.6 MB Directory structure: gitextract_b93_15po/ ├── .gitignore ├── AUTHOURS ├── Changelog ├── LICENSE ├── MANIFEST.in ├── README.md ├── README_en_US.md ├── README_zh_CN.md ├── build.sh ├── build32.sh ├── deb.sh ├── deb32.sh ├── setup.py └── tickeys/ ├── CLI.py ├── GUI.py ├── Resources/ │ └── data/ │ ├── bubble/ │ │ └── license.txt │ ├── drum/ │ │ └── _readme_and_license.txt │ ├── mechanical/ │ │ └── license.txt │ └── schemes.json ├── __init__.py ├── build.py ├── config.py ├── keyboardHandler.py ├── kivy/ │ ├── __init__.py │ ├── _event.pxd │ ├── adapters/ │ │ ├── __init__.py │ │ ├── adapter.py │ │ ├── args_converters.py │ │ ├── dictadapter.py │ │ ├── listadapter.py │ │ ├── models.py │ │ └── simplelistadapter.py │ ├── animation.py │ ├── app.py │ ├── atlas.py │ ├── base.py │ ├── cache.py │ ├── clock.py │ ├── compat.py │ ├── config.py │ ├── context.py │ ├── core/ │ │ ├── __init__.py │ │ ├── audio/ │ │ │ ├── __init__.py │ │ │ ├── audio_ffpyplayer.py │ │ │ ├── audio_gi.py │ │ │ ├── audio_gstplayer.py │ │ │ ├── audio_pygame.py │ │ │ └── audio_pygst.py │ │ ├── camera/ │ │ │ ├── __init__.py │ │ │ ├── camera_gi.py │ │ │ ├── camera_opencv.py │ │ │ ├── camera_pygst.py │ │ │ └── camera_videocapture.py │ │ ├── clipboard/ │ │ │ ├── __init__.py │ │ │ ├── clipboard_android.py │ │ │ ├── clipboard_dbusklipper.py │ │ │ ├── clipboard_dummy.py │ │ │ ├── clipboard_gtk3.py │ │ │ ├── clipboard_nspaste.py │ │ │ ├── clipboard_pygame.py │ │ │ ├── clipboard_sdl2.py │ │ │ ├── clipboard_winctypes.py │ │ │ └── clipboard_xsel.py │ │ ├── gl/ │ │ │ └── __init__.py │ │ ├── image/ │ │ │ ├── __init__.py │ │ │ ├── img_dds.py │ │ │ ├── img_ffpyplayer.py │ │ │ ├── img_gif.py │ │ │ ├── img_pil.py │ │ │ ├── img_pygame.py │ │ │ ├── img_sdl2.py │ │ │ └── img_tex.py │ │ ├── spelling/ │ │ │ ├── __init__.py │ │ │ ├── spelling_enchant.py │ │ │ └── spelling_osxappkit.py │ │ ├── text/ │ │ │ ├── __init__.py │ │ │ ├── markup.py │ │ │ ├── text_layout.pxd │ │ │ ├── text_pil.py │ │ │ ├── text_pygame.py │ │ │ └── text_sdl2.py │ │ ├── video/ │ │ │ ├── __init__.py │ │ │ ├── video_ffmpeg.py │ │ │ ├── video_ffpyplayer.py │ │ │ ├── video_gi.py │ │ │ ├── video_gstplayer.py │ │ │ ├── video_null.py │ │ │ ├── video_pyglet.py │ │ │ └── video_pygst.py │ │ └── window/ │ │ ├── __init__.py │ │ ├── window_egl_rpi.py │ │ ├── window_pygame.py │ │ └── window_sdl2.py │ ├── data/ │ │ ├── glsl/ │ │ │ ├── default.fs │ │ │ ├── default.vs │ │ │ ├── header.fs │ │ │ └── header.vs │ │ ├── images/ │ │ │ └── defaulttheme.atlas │ │ ├── keyboards/ │ │ │ ├── azerty.json │ │ │ ├── de_CH.json │ │ │ ├── en_US.json │ │ │ ├── fr_CH.json │ │ │ ├── qwerty.json │ │ │ └── qwertz.json │ │ ├── settings_kivy.json │ │ └── style.kv │ ├── effects/ │ │ ├── __init__.py │ │ ├── dampedscroll.py │ │ ├── kinetic.py │ │ ├── opacityscroll.py │ │ └── scroll.py │ ├── event.py │ ├── ext/ │ │ └── __init__.py │ ├── extras/ │ │ ├── __init__.py │ │ └── highlight.py │ ├── factory.py │ ├── factory_registers.py │ ├── garden/ │ │ └── __init__.py │ ├── geometry.py │ ├── gesture.py │ ├── graphics/ │ │ ├── __init__.py │ │ ├── buffer.pxd │ │ ├── c_opengl.pxd │ │ ├── c_opengl_debug.pxd │ │ ├── common.pxi │ │ ├── compiler.pxd │ │ ├── config.h │ │ ├── config.pxi │ │ ├── context.pxd │ │ ├── context_instructions.pxd │ │ ├── fbo.pxd │ │ ├── img_tools.pxi │ │ ├── instructions.pxd │ │ ├── opcodes.pxi │ │ ├── opengl_utils.pxd │ │ ├── opengl_utils_def.pxi │ │ ├── shader.pxd │ │ ├── stencil_instructions.pxd │ │ ├── svg.pxd │ │ ├── tesselator.pxd │ │ ├── texture.pxd │ │ ├── transformation.pxd │ │ ├── vbo.pxd │ │ ├── vertex.pxd │ │ ├── vertex_instructions.pxd │ │ └── vertex_instructions_line.pxi │ ├── input/ │ │ ├── __init__.py │ │ ├── factory.py │ │ ├── motionevent.py │ │ ├── postproc/ │ │ │ ├── __init__.py │ │ │ ├── calibration.py │ │ │ ├── dejitter.py │ │ │ ├── doubletap.py │ │ │ ├── ignorelist.py │ │ │ ├── retaintouch.py │ │ │ └── tripletap.py │ │ ├── provider.py │ │ ├── providers/ │ │ │ ├── __init__.py │ │ │ ├── androidjoystick.py │ │ │ ├── hidinput.py │ │ │ ├── leapfinger.py │ │ │ ├── linuxwacom.py │ │ │ ├── mactouch.py │ │ │ ├── mouse.py │ │ │ ├── mtdev.py │ │ │ ├── probesysfs.py │ │ │ ├── tuio.py │ │ │ ├── wm_common.py │ │ │ ├── wm_pen.py │ │ │ └── wm_touch.py │ │ ├── recorder.py │ │ └── shape.py │ ├── interactive.py │ ├── lang.py │ ├── loader.py │ ├── logger.py │ ├── metrics.py │ ├── modules/ │ │ ├── __init__.py │ │ ├── _webdebugger.py │ │ ├── inspector.py │ │ ├── keybinding.py │ │ ├── monitor.py │ │ ├── recorder.py │ │ ├── screen.py │ │ ├── touchring.py │ │ └── webdebugger.py │ ├── multistroke.py │ ├── network/ │ │ ├── __init__.py │ │ └── urlrequest.py │ ├── parser.py │ ├── properties.pxd │ ├── resources.py │ ├── setupconfig.py │ ├── storage/ │ │ ├── __init__.py │ │ ├── dictstore.py │ │ ├── jsonstore.py │ │ └── redisstore.py │ ├── support.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── benchmark.py │ │ ├── extensions/ │ │ │ ├── __init__.py │ │ │ └── make-kivyext.py │ │ ├── generate-icons.py │ │ ├── highlight/ │ │ │ ├── __init__.py │ │ │ ├── kivy-mode.el │ │ │ └── kivy.vim │ │ ├── packaging/ │ │ │ ├── README.txt │ │ │ ├── __init__.py │ │ │ ├── factory.py │ │ │ ├── osx/ │ │ │ │ ├── Info.plist │ │ │ │ ├── InfoPlist.strings │ │ │ │ └── kivy.sh │ │ │ ├── pyinstaller_hooks/ │ │ │ │ ├── __init__.py │ │ │ │ ├── hook-kivy.py │ │ │ │ └── rt-hook-kivy.py │ │ │ └── win32/ │ │ │ └── README.txt │ │ ├── report.py │ │ ├── stub-gl-debug.py │ │ └── texturecompress.py │ ├── uix/ │ │ ├── __init__.py │ │ ├── abstractview.py │ │ ├── accordion.py │ │ ├── actionbar.py │ │ ├── anchorlayout.py │ │ ├── behaviors.py │ │ ├── boxlayout.py │ │ ├── bubble.py │ │ ├── button.py │ │ ├── camera.py │ │ ├── carousel.py │ │ ├── checkbox.py │ │ ├── codeinput.py │ │ ├── colorpicker.py │ │ ├── dropdown.py │ │ ├── effectwidget.py │ │ ├── filechooser.py │ │ ├── floatlayout.py │ │ ├── gesturesurface.py │ │ ├── gridlayout.py │ │ ├── image.py │ │ ├── label.py │ │ ├── layout.py │ │ ├── listview.py │ │ ├── modalview.py │ │ ├── pagelayout.py │ │ ├── popup.py │ │ ├── progressbar.py │ │ ├── relativelayout.py │ │ ├── rst.py │ │ ├── sandbox.py │ │ ├── scatter.py │ │ ├── scatterlayout.py │ │ ├── screenmanager.py │ │ ├── scrollview.py │ │ ├── selectableview.py │ │ ├── settings.py │ │ ├── slider.py │ │ ├── spinner.py │ │ ├── splitter.py │ │ ├── stacklayout.py │ │ ├── stencilview.py │ │ ├── switch.py │ │ ├── tabbedpanel.py │ │ ├── textinput.py │ │ ├── togglebutton.py │ │ ├── treeview.py │ │ ├── video.py │ │ ├── videoplayer.py │ │ ├── vkeyboard.py │ │ └── widget.py │ ├── utils.py │ ├── vector.py │ └── weakmethod.py ├── kivy_32/ │ └── kivy/ │ ├── __init__.py │ ├── _event.pxd │ ├── adapters/ │ │ ├── __init__.py │ │ ├── adapter.py │ │ ├── args_converters.py │ │ ├── dictadapter.py │ │ ├── listadapter.py │ │ ├── models.py │ │ └── simplelistadapter.py │ ├── animation.py │ ├── app.py │ ├── atlas.py │ ├── base.py │ ├── cache.py │ ├── clock.py │ ├── compat.py │ ├── config.py │ ├── context.py │ ├── core/ │ │ ├── __init__.py │ │ ├── audio/ │ │ │ ├── __init__.py │ │ │ ├── audio_ffpyplayer.py │ │ │ ├── audio_gi.py │ │ │ ├── audio_gstplayer.py │ │ │ ├── audio_pygame.py │ │ │ └── audio_pygst.py │ │ ├── camera/ │ │ │ ├── __init__.py │ │ │ ├── camera_gi.py │ │ │ ├── camera_opencv.py │ │ │ ├── camera_pygst.py │ │ │ └── camera_videocapture.py │ │ ├── clipboard/ │ │ │ ├── __init__.py │ │ │ ├── clipboard_android.py │ │ │ ├── clipboard_dbusklipper.py │ │ │ ├── clipboard_dummy.py │ │ │ ├── clipboard_gtk3.py │ │ │ ├── clipboard_nspaste.py │ │ │ ├── clipboard_pygame.py │ │ │ ├── clipboard_sdl2.py │ │ │ ├── clipboard_winctypes.py │ │ │ └── clipboard_xsel.py │ │ ├── gl/ │ │ │ └── __init__.py │ │ ├── image/ │ │ │ ├── __init__.py │ │ │ ├── img_dds.py │ │ │ ├── img_ffpyplayer.py │ │ │ ├── img_gif.py │ │ │ ├── img_pil.py │ │ │ ├── img_pygame.py │ │ │ ├── img_sdl2.py │ │ │ └── img_tex.py │ │ ├── spelling/ │ │ │ ├── __init__.py │ │ │ ├── spelling_enchant.py │ │ │ └── spelling_osxappkit.py │ │ ├── text/ │ │ │ ├── __init__.py │ │ │ ├── markup.py │ │ │ ├── text_layout.pxd │ │ │ ├── text_pil.py │ │ │ ├── text_pygame.py │ │ │ └── text_sdl2.py │ │ ├── video/ │ │ │ ├── __init__.py │ │ │ ├── video_ffmpeg.py │ │ │ ├── video_ffpyplayer.py │ │ │ ├── video_gi.py │ │ │ ├── video_gstplayer.py │ │ │ ├── video_null.py │ │ │ ├── video_pyglet.py │ │ │ └── video_pygst.py │ │ └── window/ │ │ ├── __init__.py │ │ ├── window_egl_rpi.py │ │ ├── window_pygame.py │ │ └── window_sdl2.py │ ├── data/ │ │ ├── glsl/ │ │ │ ├── default.fs │ │ │ ├── default.vs │ │ │ ├── header.fs │ │ │ └── header.vs │ │ ├── images/ │ │ │ └── defaulttheme.atlas │ │ ├── keyboards/ │ │ │ ├── azerty.json │ │ │ ├── de_CH.json │ │ │ ├── en_US.json │ │ │ ├── fr_CH.json │ │ │ ├── qwerty.json │ │ │ └── qwertz.json │ │ ├── settings_kivy.json │ │ └── style.kv │ ├── effects/ │ │ ├── __init__.py │ │ ├── dampedscroll.py │ │ ├── kinetic.py │ │ ├── opacityscroll.py │ │ └── scroll.py │ ├── event.py │ ├── ext/ │ │ └── __init__.py │ ├── extras/ │ │ ├── __init__.py │ │ └── highlight.py │ ├── factory.py │ ├── factory_registers.py │ ├── garden/ │ │ └── __init__.py │ ├── geometry.py │ ├── gesture.py │ ├── graphics/ │ │ ├── __init__.py │ │ ├── buffer.pxd │ │ ├── c_opengl.pxd │ │ ├── c_opengl_debug.pxd │ │ ├── common.pxi │ │ ├── compiler.pxd │ │ ├── config.h │ │ ├── config.pxi │ │ ├── context.pxd │ │ ├── context_instructions.pxd │ │ ├── fbo.pxd │ │ ├── img_tools.pxi │ │ ├── instructions.pxd │ │ ├── opcodes.pxi │ │ ├── opengl_utils.pxd │ │ ├── opengl_utils_def.pxi │ │ ├── shader.pxd │ │ ├── stencil_instructions.pxd │ │ ├── svg.pxd │ │ ├── tesselator.pxd │ │ ├── texture.pxd │ │ ├── transformation.pxd │ │ ├── vbo.pxd │ │ ├── vertex.pxd │ │ ├── vertex_instructions.pxd │ │ └── vertex_instructions_line.pxi │ ├── input/ │ │ ├── __init__.py │ │ ├── factory.py │ │ ├── motionevent.py │ │ ├── postproc/ │ │ │ ├── __init__.py │ │ │ ├── calibration.py │ │ │ ├── dejitter.py │ │ │ ├── doubletap.py │ │ │ ├── ignorelist.py │ │ │ ├── retaintouch.py │ │ │ └── tripletap.py │ │ ├── provider.py │ │ ├── providers/ │ │ │ ├── __init__.py │ │ │ ├── androidjoystick.py │ │ │ ├── hidinput.py │ │ │ ├── leapfinger.py │ │ │ ├── linuxwacom.py │ │ │ ├── mactouch.py │ │ │ ├── mouse.py │ │ │ ├── mtdev.py │ │ │ ├── probesysfs.py │ │ │ ├── tuio.py │ │ │ ├── wm_common.py │ │ │ ├── wm_pen.py │ │ │ └── wm_touch.py │ │ ├── recorder.py │ │ └── shape.py │ ├── interactive.py │ ├── lang.py │ ├── loader.py │ ├── logger.py │ ├── metrics.py │ ├── modules/ │ │ ├── __init__.py │ │ ├── _webdebugger.py │ │ ├── inspector.py │ │ ├── keybinding.py │ │ ├── monitor.py │ │ ├── recorder.py │ │ ├── screen.py │ │ ├── touchring.py │ │ └── webdebugger.py │ ├── multistroke.py │ ├── network/ │ │ ├── __init__.py │ │ └── urlrequest.py │ ├── parser.py │ ├── properties.pxd │ ├── resources.py │ ├── setupconfig.py │ ├── storage/ │ │ ├── __init__.py │ │ ├── dictstore.py │ │ ├── jsonstore.py │ │ └── redisstore.py │ ├── support.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── benchmark.py │ │ ├── extensions/ │ │ │ ├── __init__.py │ │ │ └── make-kivyext.py │ │ ├── generate-icons.py │ │ ├── highlight/ │ │ │ ├── __init__.py │ │ │ ├── kivy-mode.el │ │ │ └── kivy.vim │ │ ├── packaging/ │ │ │ ├── README.txt │ │ │ ├── __init__.py │ │ │ ├── factory.py │ │ │ ├── osx/ │ │ │ │ ├── Info.plist │ │ │ │ ├── InfoPlist.strings │ │ │ │ └── kivy.sh │ │ │ ├── pyinstaller_hooks/ │ │ │ │ ├── __init__.py │ │ │ │ ├── hook-kivy.py │ │ │ │ └── rt-hook-kivy.py │ │ │ └── win32/ │ │ │ └── README.txt │ │ ├── report.py │ │ ├── stub-gl-debug.py │ │ └── texturecompress.py │ ├── uix/ │ │ ├── __init__.py │ │ ├── abstractview.py │ │ ├── accordion.py │ │ ├── actionbar.py │ │ ├── anchorlayout.py │ │ ├── behaviors.py │ │ ├── boxlayout.py │ │ ├── bubble.py │ │ ├── button.py │ │ ├── camera.py │ │ ├── carousel.py │ │ ├── checkbox.py │ │ ├── codeinput.py │ │ ├── colorpicker.py │ │ ├── dropdown.py │ │ ├── effectwidget.py │ │ ├── filechooser.py │ │ ├── floatlayout.py │ │ ├── gesturesurface.py │ │ ├── gridlayout.py │ │ ├── image.py │ │ ├── label.py │ │ ├── layout.py │ │ ├── listview.py │ │ ├── modalview.py │ │ ├── pagelayout.py │ │ ├── popup.py │ │ ├── progressbar.py │ │ ├── relativelayout.py │ │ ├── rst.py │ │ ├── sandbox.py │ │ ├── scatter.py │ │ ├── scatterlayout.py │ │ ├── screenmanager.py │ │ ├── scrollview.py │ │ ├── selectableview.py │ │ ├── settings.py │ │ ├── slider.py │ │ ├── spinner.py │ │ ├── splitter.py │ │ ├── stacklayout.py │ │ ├── stencilview.py │ │ ├── switch.py │ │ ├── tabbedpanel.py │ │ ├── textinput.py │ │ ├── togglebutton.py │ │ ├── treeview.py │ │ ├── video.py │ │ ├── videoplayer.py │ │ ├── vkeyboard.py │ │ └── widget.py │ ├── utils.py │ ├── vector.py │ └── weakmethod.py ├── locale/ │ ├── en_US/ │ │ └── LC_MESSAGES/ │ │ └── tickeys.po │ ├── gen_mo.sh │ └── zh_CN/ │ └── LC_MESSAGES/ │ └── tickeys.po ├── logger.py ├── pyxhook.py ├── requirements.txt ├── run.py ├── run_with_CLI.py ├── soundPlayer.py ├── startupHandler.py ├── tickeys ├── tickeys_tray.py ├── tickeysui.kv └── windowManager.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ ================================================ FILE: AUTHOURS ================================================ Huang Xiongbiao ================================================ FILE: Changelog ================================================ 2016-08-18 version 0.2.6 1.启动不再需要root权限 2.更换键盘和音效实现 3.更加人性化 2016-08-02 version 0.2.5 1.修复小键盘的123不能响应热键的问题 2.修复隐藏按钮的一个交互问题 3.增加英语翻译,语言切换 2016-01-29 version 0.2.4 1.修复一个配置的bug 2.提供32位的deb包 2016-01-27 version 0.2.3 1.打包为deb包 2.软件字体更换为DroidSansFallbackFull 3.修复一些bug 2016-01-25 version 0.2.2 1.添加新音效(爆裂鼓手) 2.修改界面,现在显示中文 3.修改音效显示名称 4.修复一个键盘监测bug 5.再次运行程序自动打开设置界面 2015-10-31 version 0.2.1 1.修复错误弹出更新提示的bug 2015-10-13 version 0.2.0 1.打包成单独运行文件,制作了安装脚本,解决依赖问题 2.改为使用pyinstaller打包 2015-10-11 version 0.1.9 1.UI微调(字体,颜色等) 2015-10-10 version 0.1.8 1.修改了背景颜色,与mac版同步 2.同步了mac版的两款(Cherry)新音效 2015-09-05 version 0.1.7 1.增加了检查更新的功能 2015-08-31 version 0.1.6 1.实现将程序运行情况输出到log文件,以便调试 保存位置为/tmp/tickeys.log 2015-08-10 version 0.1.5 1.实现真正后台化,启动后会隐藏窗口 2.修改当前音效显示颜色 2015-08-10 version 0.1.4 1.打开程序后会出现气泡提醒 2015-08-04 version 0.1.3 1.修正因路径问题导致的配置无法正确显示的问题 2015-08-03 version 0.1.2 1.更新键盘检测方法(测试中) 2.增加配置自动保存功能 3.修改默认音效为mechanical 4.修改部分函数名称 2015-08-02 version 0.1.1a6 1.修改键盘检测方法,支持更多键盘类型(测试中) 2015-08-02 version 0.1.1a5 1.修改配色,使风格大致与mac版相同 2.修改布局,适应大小变化 3.加入开机自启动设置(测试中) 2015-07-31 version 0.1.0a4 1.加入changlog 2.修改setup,修改相关安装依赖 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Xiongbiao Huang 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. ================================================ FILE: MANIFEST.in ================================================ recursive-include tickeys/Resources/data * recursive-include tickeys/Resources/fonts * recursive-include tickeys/locale * include tickeys/requirements.txt include tickeys/tickeys include tickeys/tickeysui.kv include tickeys/tickeys.png include README.md include LICENSE include AUTHOURS include Changelog ================================================ FILE: README.md ================================================ # Tickeys-linux Instant audio feedback when typing. For Linux. ![Tickeys Icon](http://img.blog.csdn.net/20150802103616846) English Readme: [English Readme](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/README_en_US.md) 中文版Readme: [中文Readme](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/README_zh_CN.md) # Screenshot ## v0.2.5 ![Tickeys v0.2.5](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.2.5.png) ## v0.2.2 ![Tickeys v0.2.2](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.2.2.png) ## v0.1.9 ![Tickeys v0.1.9](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.1.9.png) ## v0.1.8 ![Tickeys v0.1.8](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.1.8.png) ## v0.1.1 ![Tickeys v0.1.1](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.1.1.png) ## v0.0.1 ![Tickeys v0.0.1](https://github.com/BillBillBillBill/Tickeys-linux/blob/master/screenshot/tickeys_v0.0.1.png) ================================================ FILE: README_en_US.md ================================================ [Downlod 32 bit version tickeys_0.2.5_i386.deb:http://pan.baidu.com/s/1c2BN3Pm](http://pan.baidu.com/s/1c2BN3Pm) [Downlod 64 bit version tickeys_0.2.5_amd64.deb:http://pan.baidu.com/s/1o8AhmwM](http://pan.baidu.com/s/1o8AhmwM) Bug or question? Please tell me in issue. [![Join the chat at https://gitter.im/BillBillBillBill/Tickeys-linux](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/BillBillBillBill/Tickeys-linux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Tickeys Icon](http://img.blog.csdn.net/20150802103616846) # Introduce Tickeys is an powerful keyboard sound effect tool。Tickeys now brings seven sound styles,include Bubble, Typewriter, Mechanical, Sword, etc. This tool have Windows and Mac version,Mac Version:([GitHub](https://github.com/yingDev/Tickeys)) # Project Website http://www.yingdev.com/projects/tickeys # PyPI https://pypi.python.org/pypi/tickeys # Tickeys Mac Version https://github.com/yingDev/Tickeys # Install Tickeys has not been fully tested to install in different distribution yet, some distribution may need installing related requirements. ## Download the packaged deb(suggest) * deb links: [Downlod 32 bit version tickeys_0.2.5_i386.deb:http://pan.baidu.com/s/1c2BN3Pm](http://pan.baidu.com/s/1c2BN3Pm) [Downlod 64 bit version tickeys_0.2.5_amd64.deb:http://pan.baidu.com/s/1o8AhmwM](http://pan.baidu.com/s/1o8AhmwM) * After install, find Tickeys on launcher and open it ## Compile(Build from source, need requirements): * next operation show **execute** `sudo apt-get install python-dev python-pip python-kivy xdotool gksu`first to meet the requirements for Tickeys to run. * install package(Notice Version): sudo pip install cython==0.20.2 notify2 pyinstaller==3.0 kivy==1.9.0 evdev #### Quick Compile Install: * execute `sudo apt-get install python-dev python-pip python-kivy xdotool && sudo easy_install tickeys`。 * execute `sudo easy_install tickeys` or `sudo pip install tickeys` * Use command `sudo tickeys` to open (`sudo tickeys -c` for CLI version) #### Running Problem * Couldn't hide window: Solve:execute `sudo apt-get install xdotool ` to install xdotool * no setuptools or pip Solve:execute `sudo apt-get install python-pip` to install * Python.h:no such file Solve:execute `sudo apt-get install python-dev` to install * ImportError: No module named Cython.Distutils Solve:execute `sudo easy_install cython` to install * ImportError: libSDL-1.2.so.0: cannot open shared object file: No such file or directory Solve:execute `yum install libSDL-1.2.so.0` to install * NotImplementedError: mixer module not available Solve:same with above Debian and Ubuntu User may try to install: * sudo apt-get install xdotool * sudo apt-get install libsdl1.2-dev * sudo apt-get install libsdl-mixer1.2 * sudo apt-get install libsdl-ttf2.0 # How to use Root permission is needed,implement the CLI and GUI version,GUI version is run by default,it will auto hide after open,press QAZ123 to show the window of tickeys. # How to Develop * ####Code Style: PEP8 * ####Application UI framework:Kivy * ####Open sources license: MIT License Install requirements: pip install -r requirements.txt # Project struct Tickeys-linux ``` ├── AUTHOURS ├── build.sh ├── build.spec pyinstaller ├── Changelog ├── deb.sh ├── dist │   ├── make_deb.sh script for deb ├── lib lib for run ├── LICENSE ├── MANIFEST.in ├── README.md ├── screenshot ├── setup.py ├── tickeys │   ├── locale translation │   ├── build.py cx_freeze(desperate) │   ├── CLI.py start CLI │   ├── config.py config controller │   ├── GUI.py start GUI │   ├── __init__.py │   ├── keyboardHandler.py handler keyboard events │   ├── logger.py │   ├── requirements.txt │   ├── Resources │   │   ├── data │   │   │   ├── bubble │   │   │   ├── Cherry_G80_3000 │   │   │   ├── Cherry_G80_3494 │   │   │   ├── drum │   │   │   ├── mechanical │   │   │   ├── sword │   │   │   └── typewriter │   │   │   ├── schemes.json │   │   └── fonts │   │   └── DroidSansFallbackFull.ttf │   ├── run.py entry │   ├── run_with_CLI.py entry with CLI │   ├── soundPlayer.py │   ├── startupHandler.py │   ├── tickeys │   ├── tickeys.png │   ├── tickeys_tray.py (not use) │   ├── tickeysui.kv (desperate) │   └── windowManager.py └── tickeys.egg-info ``` # Author Huang Xiongbiao billo@qq.com ================================================ FILE: README_zh_CN.md ================================================ [下载32位tickeys_0.2.5_i386.deb:http://pan.baidu.com/s/1c2BN3Pm](http://pan.baidu.com/s/1c2BN3Pm) [下载64位tickeys_0.2.5_amd64.deb:http://pan.baidu.com/s/1o8AhmwM](http://pan.baidu.com/s/1o8AhmwM) 如果有任何问题或者建议,请在issue中提出 [![Join the chat at https://gitter.im/BillBillBillBill/Tickeys-linux](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/BillBillBillBill/Tickeys-linux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Tickeys Icon](http://img.blog.csdn.net/20150802103616846) # 简介 Tickeys是一款很强大的键盘音效软件。Tickeys 自带了七种声音效果方案,有打字机、冒泡、机械键盘、剑气等。每天都听着键盘声音是不是很烦闷,现在有了这款神器你就可以瞬间帮助自己的键盘加上逼格特效。 这个软件之前发布了Windows和Mac版,Tickeys 是由 Nozama 所做的一个 Mac 平台的开源小项目 ([GitHub](https://github.com/yingDev/Tickeys)),Windows 版由黄飞实现。我使用了之后,觉得挺有意思的,因此用Python写了个Linux版的。 # 项目网站 http://www.yingdev.com/projects/tickeys PyPI: https://pypi.python.org/pypi/tickeys # Tickeys的Mac版本 https://github.com/yingDev/Tickeys # 安装说明 在不同发行版上可能会有因为文件的缺失或者环境不同导致无法使用,需要安装相关依赖。 ## 下载打包好的安装包安装(建议) * 下载deb安装包: [下载32位tickeys_0.2.4_i386.deb:http://pan.baidu.com/s/1geOYtdp](http://pan.baidu.com/s/1c2BN3Pm) [下载64位tickeys_0.2.4_amd64.deb:http://pan.baidu.com/s/1o8AhmwM](http://pan.baidu.com/s/1o8AhmwM) * 安装后,在启动器中找到Tickeys打开。 ## 编译安装(需要安装依赖): * 以下方法需要**先执行**`sudo apt-get install python-dev python-pip python-kivy xdotool gksu`来安装依赖,一般这样就可以满足运行条件了。 * 安装库(注意版本): sudo pip install cython==0.20.2 notify2 pyinstaller==3.0 kivy==1.9.0 evdev #### 快速编译安装:执行`sudo apt-get install python-dev python-pip python-kivy xdotool && sudo easy_install tickeys`。 ### 方法1.自动安装 * 执行`sudo easy_install tickeys` or `sudo pip install tickeys`安装 * 然后通过 `sudo tickeys` 来打开 (sudo tickeys -c 打开CLI版本) ### 方法2.半自动安装 * 下载 https://github.com/BillBillBillBill/Tickeys-linux/archive/master.zip ,解压后运行 `sudo python setup.py install` * 然后通过 `sudo tickeys` 来打开 (sudo tickeys -c 打开CLI版本) #### 错误解决方案: * 无法隐藏窗口: 解决方法:使用`sudo apt-get install xdotool `安装xdotool * 若没有setuptools or pip 解决方法:使用`sudo apt-get install python-pip` 安装 * Python.h:没有那个文件或目录 解决方法:使用`sudo apt-get install python-dev`安装 * ImportError: No module named Cython.Distutils 解决方法:使用`sudo easy_install cython`安装 * ImportError: libSDL-1.2.so.0: cannot open shared object file: No such file or directory 解决方法:使用`yum install libSDL-1.2.so.0`安装依赖 * NotImplementedError: mixer module not available 解决方法:同上 Debian and Ubuntu 用户则可以尝试安装: * sudo apt-get install xdotool * sudo apt-get install libsdl1.2-dev * sudo apt-get install libsdl-mixer1.2 * sudo apt-get install libsdl-ttf2.0 # 使用说明 需要以root权限才能启动,实现了CLI版本和GUI版本,默认启动GUI版本,GUI版本启动后会自动隐藏,按QAZ123唤出窗口。 调整参数后会自动保存,下次开启会使用该设置。 Open at startup是开启开机自启动功能选项,开关置为ON开启功能,开关置为OFF关闭功能。 # TODO 1.打开程序后出现气泡提醒(已实现) 2.使GUI真正后台化(已实现) 3.按最小化按钮或退出按钮隐藏GUI 4.程序运行情况输出到log文件中,以便调试(已实现) # 开发 * ####编码规范: PEP8 * ####应用UI框架:Kivy * ####开源许可证: MIT License 依赖安装: pip install -r requirements.txt 使用pyinstaller打包 命令:`pyinstaller build.spec` 播放音乐通过pygame的mixer实现。 键盘事件的获取通过evdev实现。 窗口的控制使用工具xdotool来实现。(另一方法是使用wmctrl来控制窗口) xdotool的使用: * 获取窗口ID: WID=`xdotool search "Tickeys" | head -1` * 激活窗口: xdotool windowactivate --sync $WID xdotool windowmap --sync $WID && xdotool windowactivate --sync $WID * 隐藏窗口实现: xdotool getactivewindow windowunmap ~~xdotool getactivewindow windowminimize~~ 或 ~~xdotool getactivewindow windowmove 999 0~~ # 项目结构 Tickeys-linux ``` ├── AUTHOURS ├── build.sh ├── build.spec pyinstaller打包用 ├── Changelog 版本变动说明 ├── deb.sh ├── dist │   ├── make_deb.sh 打包成deb包的脚本 ├── lib 运行所用的一些库 ├── LICENSE ├── MANIFEST.in ├── README.md ├── screenshot Tickeys截图文件 ├── setup.py 分发用 ├── tickeys │   ├── locale 翻译文件 │   ├── build.py cx_freeze打包,已不用 │   ├── CLI.py 启动CLI的模块 │   ├── config.py 处理配置保存和读取的模块 │   ├── GUI.py 启动GUI的模块 │   ├── __init__.py │   ├── keyboardHandler.py 处理键盘输入的函数 │   ├── logger.py 日志记录函数,调试时使用 │   ├── requirements.txt 开发模块依赖包 │   ├── Resources 程序资源,包括音效,字体等 │   │   ├── data │   │   │   ├── bubble │   │   │   ├── Cherry_G80_3000 │   │   │   ├── Cherry_G80_3494 │   │   │   ├── drum │   │   │   ├── mechanical │   │   │   ├── sword │   │   │   └── typewriter │   │   │   ├── schemes.json │   │   └── fonts │   │   └── DroidSansFallbackFull.ttf │   ├── run.py 程序入口 │   ├── run_with_CLI.py 程序入口,带CLI版(失败时运行CLI) │   ├── soundPlayer.py 播放声效的模块 │   ├── startupHandler.py 控制开机自启动的模块 │   ├── tickeys 启动tickeys的脚本,打包时放进打包后的文件夹使用 │   ├── tickeys.png │   ├── tickeys_tray.py 托盘,由于打包大小问题尚未放入 │   ├── tickeysui.kv kv的ui文件,现在已直接写入GUI.py中 │   └── windowManager.py 提供窗口控制的方法 └── tickeys.egg-info ``` # 文件说明 * build.py cx_freeze打包函数 * run.py 存放入口函数 * readme.txt 放进打包后程序文件夹的readme * requirements.txt * tickeys * CLI.py * GUI.py * config.py * tickeysui.kv * KeyboardHandler.py * logger.py * SoundPlayer.py * StartupHandler.py # 问题 * 目前在Fedora上好像无法使用GUI的,不知道是因为我用虚拟机开的问题还是Kivy这个框架的问题,因为提示The 3D features of the virtual machine will be disabled。 # 作者 Huang Xiongbiao billo@qq.com ================================================ FILE: build.sh ================================================ #!/bin/bash pyinstaller build.spec ================================================ FILE: build32.sh ================================================ #!/bin/bash pyinstaller build32.spec ================================================ FILE: deb.sh ================================================ #!/bin/bash sh build.sh cd dist sudo sh make_deb.sh ================================================ FILE: deb32.sh ================================================ #!/bin/bash sh build32.sh cd dist sudo sh make_deb32.sh ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages from tickeys import __version__, __author__, __email__ requirements = [ 'cython==0.20.2', 'kivy==1.9.0', 'evdev', 'sounddevice', 'soundfile', 'notify2', ] setup(name='tickeys', version=__version__, download_url='https://github.com/BillBillBillBill/Tickeys-linux', packages=['tickeys'], package_dir={'tickeys': 'tickeys'}, include_package_data=True, package_data={'tickeys': ['*.*']}, # data_files=[], classifiers=[ 'Intended Audience :: End Users/Desktop', 'Development Status :: 3 - Alpha', 'Environment :: X11 Applications', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX :: Linux', 'Natural Language :: Chinese (Simplified)', 'Programming Language :: Python :: 2.7', ], author=__author__, author_email=__email__, description='Instant audio feedback when typing. For Linux.', long_description=open('README.md').read(), keywords='keyboard typing audio feedback ', url='https://github.com/BillBillBillBill/Tickeys-linux', license='MIT', entry_points={ 'console_scripts': ['tickeys = tickeys:main'] }, install_requires=requirements, tests_require=requirements) ================================================ FILE: tickeys/CLI.py ================================================ #!/usr/bin/env python # coding: utf-8 import cmd from keyboardHandler import KeyboardHandler from __init__ import __version__ import sys reload(sys) sys.setdefaultencoding("utf-8") class CLI(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.intro = "Tickeys %s - Linux\nType 'help' for help" % __version__ self.prompt = ">>> " self.detecter = KeyboardHandler() self.detecter.start_detecting() self.volume = self.detecter.get_player_infor()['volume'] * 100.0 self.pitch = self.detecter.get_player_infor()['pitch'] * 10.0 self.style = self.detecter.get_player_infor()['style'] def default(self, line): print "Command '%s' is invalid, try 'help'" % line def help_setstyle(self): print "Set style, change the sound's effect" def do_setstyle(self, arg): style_index = raw_input( "Input the effect style number you want" "(0:bubble 1:mechanical 2:sword 3:typewriter 4:Cherry_G80_3000 5:Cherry_G80_3494 6:drum):") style_list = ['bubble', 'mechanical', 'sword', 'typewriter', 'Cherry_G80_3000', 'Cherry_G80_3494', 'drum'] try: style_index = int(style_index) assert(0 <= style_index <= 6) except Exception: print "Input must between 0~6!!" return self.style = style_list[style_index] self.detecter.set_style(self.style) def help_setvol(self): print "Set volume, input the volume you want" def do_setvol(self, arg): volume = raw_input("Input the volume(0~100) you want:") try: volume = float(volume) assert(0 <= volume <= 100) except Exception: print "Volume must between 0~100!!" return self.volume = volume self.detecter.set_volume(self.volume/100.0) # def help_getvol(self): # print "Get the volume" # def do_getvol(self, arg): # print self.volume def help_setpitch(self): print "Set pitch, input the pitch you want" def do_setpitch(self, arg): pitch = raw_input("Input the pitch(0~30, default 10) you want:") try: pitch = float(pitch) assert(0 <= pitch <= 30) except Exception: print "Pitch must between 0~30!!" return self.pitch = pitch self.detecter.set_pitch(self.pitch/10.0) # def help_getpitch(self): # print "Get the pitch" # def do_getpitch(self, arg): # print self.pitch def help_getinfo(self): print "Get tickeys' sound effect, volume and pitch" def do_getinfo(self, arg): print "Sound effect: %s Volume: %s Pitch: %s" \ % (self.style, self.volume, self.pitch) def do_quit(self, arg): try: self.detecter.stop_detecting() except Exception: pass finally: sys.exit(0) return True if __name__ == "__main__": cli = CLI() cli.cmdloop() ================================================ FILE: tickeys/GUI.py ================================================ #!/usr/bin/env python # coding: utf-8 from kivy.app import App from kivy.uix.spinner import Spinner from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.modalview import ModalView from kivy.uix.button import Button from kivy.uix.label import Label from keyboardHandler import KeyboardHandler from kivy.lang import Builder from startupHandler import add_startup_linux, check_startup_file, delete_startup_linux from logger import logger from config import configer from __init__ import __version__, debug_mode import sys import os import gettext from ast import literal_eval from threading import Thread import notify2 from windowManager import hide_GUI, save_GUI_window_id reload(sys) sys.setdefaultencoding("utf-8") # set font from kivy.core.text import Label as textLabel try: os.chdir(os.path.dirname(__file__)) except Exception: pass textLabel.register("DroidSans", "./Resources/fonts/DroidSansFallbackFull.ttf") t = gettext.translation('tickeys', 'locale', languages=[configer.lang], fallback=True) _ = t.ugettext Builder.load_string(('''
: pos: 0,0 padding: 50 rows: 6 row_force_default: True row_default_height: 50 spacing: 50 orientation: 'vertical' canvas: Color: rgba: 0.368, 0.384, 0.447, 0.8 Rectangle: pos: 0,0 size: self.size Label: bold: True text: 'Tickeys' font_size: 50 size_hint_y: None SpinnerRow AdjustVol AdjustPitch ExitAndSwitchRow InforRow Label: bold: True color: 1, 1, 1, 1 font_size: 33 size_hint_x: None width: 250 text: '%s' Slider: min: 0.0 max: 1.0 value: root.parent.configer.volume width: 300 on_value: root.setVolume(self.value) Label: bold: True color: 1, 1, 1, 1 font_size: 33 size_hint_x: None width: 250 text: '%s' Slider: min: 0.8 max: 2.5 value: root.parent.configer.pitch width: 300 on_value: root.setPitch(self.value) : Label: bold: True color: 1, 1, 1, 1 font_size: 33 size_hint_x: None text: "%s" width: 250 EffectSpinner: on_text: root.change_style() : bold: True font_size: 30 text: root.get_style_name() background_color: 3, 3, 3, 1 color: 0.1, 0.67, 0.93, 1 values: %s : Label: size_hint_x: None width: root.width/6.0 - 120 Label: size_hint_x: None color: 1, 1, 1, 1 font_size: 17 width: root.width/6.0 + 60 text: '%s:' Switch: size_hint_x: None width: 40 id: switcher active: root.have_added on_active: root.add_delete_startup_file(self.active) Label: size_hint_x: None width: root.width/6.0 - 85 Spinner: width: 40 bold: True font_size: 20 text: root.get_language_name() background_color: 3, 3, 3, 1 color: 0.1, 0.67, 0.93, 1 values: ['English', '简体中文'] on_text: root.set_language(self.text) Label: size_hint_x: None width: 20 Button: size_hint_x: None width: 150 font_size: 20 background_color: 3, 3, 3, 1 bold: True text: "%s" color: 0,0,0,1 on_press: root.Exit() Label: size_hint_x: None width: 20 Button: size_hint_x: None width: 150 font_size: 20 background_color: 3, 3, 3, 1 bold: True text: "%s" color: 0,0,0,1 on_release: root.Hide() : Label: color: 0.8, 0.8, 0.8, 1 font_size: 20 size_hint_x: None text: root.get_version() width: root.width/3.0 Label: color: 0.8, 0.8, 0.8, 1 font_size: 20 size_hint_x: None markup: True text: "[ref=%s]%s[/ref]" width: root.width/3.0 on_ref_press:root.open_project_website() Label: color: 0.8, 0.8, 0.8, 1 font_size: 20 size_hint_x: None text: "%s: Bill (billo@qq.com)" width: root.width/3.0 border: 1,1,1,1 on_touch_move: ''' % (_("Volume"), _("Pitch"), _("Sound Style"), _("Sound Style Items"), _("Start at startup"), _("Quit"), _("Hide"), _("Project Website"), _("Project Website"), _("Author"))).encode('utf-8')) def show_notify(notify_content=""): try: notify2.init('Tickeys') title = 'Tickeys' icon_file_path = os.getcwd() + '/tickeys.png' notify = notify2.Notification(title, notify_content, icon_file_path) notify.show() except Exception, e: logger.exception(e) logger.error("show notify fail") def show_startup_notify(): notify_content = _("Startup Notify") show_notify(notify_content) def check_update_and_notify(): try: import urllib import json logger.info("Version checking...") r = urllib.urlopen('http://billbill.sinaapp.com/tickeys') return_msg = json.loads(r.read()) print return_msg if return_msg["version"] <= __version__: logger.debug("Version checking success. It is the latest version...") else: # show update notify notify_content = _("Update Notify") % (return_msg["version"], return_msg["update"]) print notify_content show_notify(notify_content) except Exception, e: logger.exception(e) logger.error("Version checking fail:" + str(e)) class EffectSpinner(Spinner): def get_style_name(self): style_list = literal_eval(_("Sound Style Items")) style_display_name_map = { 'bubble': style_list[0], 'typewriter': style_list[1], 'mechanical': style_list[2], 'sword': style_list[3], 'Cherry_G80_3000': style_list[4], 'Cherry_G80_3494': style_list[5], 'drum': style_list[6] } name = self.parent.parent.configer.style return style_display_name_map.get(name, name) class SpinnerRow(BoxLayout): def change_style(self): style_list = literal_eval(_("Sound Style Items")) style_display_name_map = { style_list[0]: "bubble", style_list[1]: "typewriter", style_list[2]: "mechanical", style_list[3]: "sword", style_list[4]: "Cherry_G80_3000", style_list[5]: "Cherry_G80_3494", style_list[6]: "drum" } # for safe if self.children[0].text not in style_display_name_map: return style_name = style_display_name_map[self.children[0].text] self.parent.detecter.set_style(style_name) class AdjustVol(BoxLayout): def setVolume(self, volume): self.parent.detecter.set_volume(volume) class AdjustPitch(BoxLayout): def setPitch(self, pitch): self.parent.detecter.set_pitch(pitch) class SwitcherRow(BoxLayout): pass class ExitAndSwitchRow(BoxLayout): def Exit(self): os._exit(0) def Hide(self): self.parent.Hide() def add_delete_startup_file(self, active): if active: add_startup_linux() else: delete_startup_linux() def set_language(self, language): language_map = { "English": "en_US", "简体中文": "zh_CN" } self.parent.configer.lang = language_map.get(language, "en_US") self.parent.configer.save_config() # show popup hint view = ModalView(size_hint=(None, None), size=(0, 0)) view.add_widget(Label(text=_("This will take effect next time you start"), font_size=30)) view.open() def get_language_name(self): lang_display_name_map = { "en_US": "English", "zh_CN": "简体中文" } lang = self.parent.configer.lang return lang_display_name_map.get(lang, "English") @property def have_added(self): return check_startup_file() class InforRow(BoxLayout): def open_project_website(self, *args): from webbrowser import open_new open_new("https://github.com/BillBillBillBill/Tickeys-linux") def get_version(self): return 'Version: ' + __version__ class Main(GridLayout): def __init__(self, *args, **kwargs): self.configer = configer super(Main, self).__init__(**kwargs) save_GUI_window_id() self.Hide() # set up keyboard detecter self.detecter = KeyboardHandler() self.detecter.start_detecting() # show notify message Thread(target=show_startup_notify).start() # now not check update # Thread(target=check_update_and_notify).start() def Hide(self): if not debug_mode: hide_GUI() class TickeysApp(App): def __init__(self, *args, **kwargs): super(TickeysApp, self).__init__(**kwargs) def build(self): self.icon = 'tickeys.png' root = Main() return root def on_stop(self): os._exit(0) if __name__ == '__main__': TickeysApp().run() ================================================ FILE: tickeys/Resources/data/bubble/license.txt ================================================ Sound pack downloaded from Freesound.org ---------------------------------------- This pack of sounds contains sounds by Glaneur de sons ( http://www.freesound.org/people/Glaneur%20de%20sons/ ) You can find this pack online at: http://www.freesound.org/people/Glaneur%20de%20sons/packs/6686/ License details --------------- Sampling+: http://creativecommons.org/licenses/sampling+/1.0/ Creative Commons 0: http://creativecommons.org/publicdomain/zero/1.0/ Attribution: http://creativecommons.org/licenses/by/3.0/ Attribution Noncommercial: http://creativecommons.org/licenses/by-nc/3.0/ Sounds in this pack ------------------- * 104950__Glaneur_de_sons__bubbles_2_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104950/ * license: Attribution * 104949__Glaneur_de_sons__bubbles_1_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104949/ * license: Attribution * 104948__Glaneur_de_sons__bubble_9_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104948/ * license: Attribution * 104947__Glaneur_de_sons__bubble_8_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104947/ * license: Attribution * 104946__Glaneur_de_sons__bubble_7_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104946/ * license: Attribution * 104945__Glaneur_de_sons__bubble_6_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104945/ * license: Attribution * 104944__Glaneur_de_sons__bubble_5_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104944/ * license: Attribution * 104943__Glaneur_de_sons__bubble_4_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104943/ * license: Attribution * 104942__Glaneur_de_sons__bubble_3_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104942/ * license: Attribution * 104941__Glaneur_de_sons__bubble_2_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104941/ * license: Attribution * 104940__Glaneur_de_sons__bubble_1_.wav * url: http://www.freesound.org/people/Glaneur%20de%20sons/sounds/104940/ * license: Attribution ================================================ FILE: tickeys/Resources/data/drum/_readme_and_license.txt ================================================ Sound pack downloaded from Freesound.org ---------------------------------------- This pack of sounds contains sounds by Veiler ( http://www.freesound.org/people/Veiler/ ) You can find this pack online at: http://www.freesound.org/people/Veiler/packs/16053/ License details --------------- Sampling+: http://creativecommons.org/licenses/sampling+/1.0/ Creative Commons 0: http://creativecommons.org/publicdomain/zero/1.0/ Attribution: http://creativecommons.org/licenses/by/3.0/ Attribution Noncommercial: http://creativecommons.org/licenses/by-nc/3.0/ Sounds in this pack ------------------- * 261413__veiler__crash.wav * url: http://www.freesound.org/people/Veiler/sounds/261413/ * license: Creative Commons 0 * 261412__veiler__tom-2.wav * url: http://www.freesound.org/people/Veiler/sounds/261412/ * license: Creative Commons 0 * 261411__veiler__tom-3.wav * url: http://www.freesound.org/people/Veiler/sounds/261411/ * license: Creative Commons 0 * 261410__veiler__tom-4.wav * url: http://www.freesound.org/people/Veiler/sounds/261410/ * license: Creative Commons 0 * 261409__veiler__kick-bass-win.wav * url: http://www.freesound.org/people/Veiler/sounds/261409/ * license: Creative Commons 0 * 261408__veiler__snare-3.wav * url: http://www.freesound.org/people/Veiler/sounds/261408/ * license: Creative Commons 0 * 261407__veiler__tom-1.wav * url: http://www.freesound.org/people/Veiler/sounds/261407/ * license: Creative Commons 0 ================================================ FILE: tickeys/Resources/data/mechanical/license.txt ================================================ Sound pack downloaded from Freesound.org ---------------------------------------- This pack of sounds contains sounds by jim-ph ( http://www.freesound.org/people/jim-ph/ ) You can find this pack online at: http://www.freesound.org/people/jim-ph/packs/12363/ License details --------------- Sampling+: http://creativecommons.org/licenses/sampling+/1.0/ Creative Commons 0: http://creativecommons.org/publicdomain/zero/1.0/ Attribution: http://creativecommons.org/licenses/by/3.0/ Attribution Noncommercial: http://creativecommons.org/licenses/by-nc/3.0/ Sounds in this pack ------------------- * 194799__jim-ph__keyboard5.wav * url: http://www.freesound.org/people/jim-ph/sounds/194799/ * license: Creative Commons 0 * 194798__jim-ph__vintage-keyboard-4.wav * url: http://www.freesound.org/people/jim-ph/sounds/194798/ * license: Creative Commons 0 * 194797__jim-ph__vintage-keyboard-3.wav * url: http://www.freesound.org/people/jim-ph/sounds/194797/ * license: Creative Commons 0 * 194796__jim-ph__vintage-keyboard-2.wav * url: http://www.freesound.org/people/jim-ph/sounds/194796/ * license: Creative Commons 0 * 194795__jim-ph__vintage-keyboard-1.wav * url: http://www.freesound.org/people/jim-ph/sounds/194795/ * license: Creative Commons 0 ================================================ FILE: tickeys/Resources/data/schemes.json ================================================ [ { "name":"bubble", "display_name":"冒泡", "files":["1.wav","2.wav","3.wav","4.wav","5.wav","6.wav","7.wav","8.wav", "enter.wav"], "non_unique_count":8, "key_audio_map":{"36":8} }, { "name":"typewriter", "display_name":"打字机", "files":["key-new-05.wav","key-new-04.wav","key-new-03.wav","key-new-02.wav","key-new-01.wav","space-new.wav","scrollUp.wav","scrollDown.wav","backspace.wav", "return-new.wav"], "non_unique_count":5, "key_audio_map":{"36":9, "49":5, "51":8} }, { "name": "mechanical", "display_name":"机械键盘", "files":["1.wav", "2.wav", "3.wav", "4.wav", "5.wav"], "non_unique_count":4, "key_audio_map":{"36":4} }, { "name": "sword", "display_name": "剑气", "files": ["1.wav", "2.wav", "3.wav", "4.wav", "5.wav", "6.wav", "back.wav", "enter.wav", "space.wav"], "non_unique_count": 6, "key_audio_map":{"36": 7,"49":8, "51":6} }, { "name": "Cherry_G80_3000", "display_name": "Cherry G80-3000", "files": ["G80-3000.wav", "G80-3000_fast1.wav", "G80-3000_slow1.wav", "G80-3000_fast2.wav","G80-3000_slow2.wav"], "non_unique_count": 5, "key_audio_map":{"36": 4, "49": 4} }, { "name": "Cherry_G80_3494", "display_name": "Cherry G80-3494", "files": ["G80-3494.wav", "G80-3494_fast1.wav", "G80-3494_slow1.wav", "G80-3494_enter.wav", "G80-3494_space.wav", "G80-3494_backspace.wav"], "non_unique_count": 3, "key_audio_map":{"36": 3, "49": 4, "51": 5} }, { "name": "drum", "display_name": "爆裂鼓手", "files": ["1.wav", "2.wav", "3.wav", "4.wav", "space.wav", "backspace.wav", "enter.wav"], "non_unique_count": 4, "key_audio_map":{"36": 6, "49": 4, "51": 5} } ] ================================================ FILE: tickeys/__init__.py ================================================ __author__ = 'Huang Xiongbiao' __email__ = 'billo@qq.com' __version__ = '0.2.6' debug_mode = False from run import main def tickeys(): main() ================================================ FILE: tickeys/build.py ================================================ import shutil import os from cx_Freeze import setup, Executable from __init__ import __version__ executable_filename = "tickeys" if __name__ == '__main__': buildOptions = dict( compressed=True, include_files=['kivy', 'tickeys', 'Resources'],) setup( name=executable_filename, version=__version__, description='Instant audio feedback when typing. For Linux.', options=dict(build_exe=buildOptions), executables=[Executable("run.py")]) # print "Move resources file..." # shutil.copytree("../Resources", os.getcwd() + "/build/Resources") print "Add readme file..." shutil.copyfile("../README.md", os.getcwd() + "/build/README.md") print "Add some docs..." shutil.copyfile("../AUTHOURS", os.getcwd() + "/build/AUTHOURS") shutil.copyfile("../Changelog", os.getcwd() + "/build/Changelog") shutil.copyfile("../LICENSE", os.getcwd() + "/build/LICENSE") ================================================ FILE: tickeys/config.py ================================================ import ConfigParser import os from logger import logger class Configer(): """docstring for Configer""" def __init__(self, *arg): try: os.chdir(os.path.dirname(__file__)) except Exception: pass self.config_path = os.environ["HOME"] + "/.tickeys/tickeys.conf" self.cf = ConfigParser.ConfigParser() self.read_config() def init_config(self): self.style = 'mechanical' self.volume = 1.0 self.pitch = 1.0 self.lang = 'en_US' self.autostart = False self.save_config() def read_config(self): try: if not os.path.exists(self.config_path): self.init_config() else: self.cf.read(self.config_path) self.volume = self.cf.getfloat('options', 'volume') self.pitch = self.cf.getfloat('options', 'pitch') self.style = self.cf.get('options', 'style') self.autostart = self.cf.get('options', 'autostart') self.lang = self.cf.get('options', 'lang') except Exception, e: self.init_config() logger.debug(e) def save_config(self): if not self.cf.sections(): self.cf.add_section('options') self.cf.set('options', 'volume', self.volume) self.cf.set('options', 'pitch', self.pitch) self.cf.set('options', 'style', self.style) self.cf.set('options', 'lang', self.lang) self.cf.set('options', 'autostart', self.autostart) with open(self.config_path, 'w') as f: self.cf.write(f) @property def volume(self): return self.volume @property def pitch(self): return self.pitch @property def style(self): return self.style @property def lang(self): return self.lang @property def autostart(self): return self.autostart configer = Configer() ================================================ FILE: tickeys/keyboardHandler.py ================================================ #!/usr/bin/env python # coding: utf-8 import threading from soundPlayer import SoundPlayer from logger import logger from pyxhook import HookManager from windowManager import show_GUI __author__ = 'Huang xiongbiao(billo@qq.com)' class KeyboardHandler(): def __init__(self): logger.debug("Keyboard deteccter created.") self.hm = HookManager() self.hm.KeyDown = self.key_down self.hm_thread = None self.input_record_list = [] self.hot_key_list = [24, 38, 52, 10, 11, 12] # QAZ123 self.hot_key_list2 = [24, 38, 52, 87, 88, 89] # QAZ123 with 123 in side keyboard self.sp = SoundPlayer() def set_style(self, style): self.sp.set_style(style) def set_volume(self, volume): self.sp.set_volume(volume) def set_pitch(self, pitch): self.sp.set_pitch(pitch) def get_player_infor(self): return self.sp.get_infor() def key_down(self, event): self.sp.play(event.ScanCode) self.check_show_window(event.ScanCode) # logger.debug(str(event)) # check input if satisfy the hotkey def check_show_window(self, key_code): if self.input_record_list and key_code == self.input_record_list[-1]: return input_record_length = len(self.input_record_list) next_key_code = self.hot_key_list[input_record_length] next_key_code2 = self.hot_key_list2[input_record_length] if key_code == next_key_code or key_code == next_key_code2: self.input_record_list.append(key_code) if input_record_length == 5: show_GUI() self.input_record_list = [] else: # clear the record if not satisfy self.input_record_list = [] def start_detecting(self): self.hm_thread = threading.Thread(target=self.hm.start, args=()) self.hm_thread.start() # kill all threads def stop_detecting(self): self.hm_thread._Thread__stop() self.hm.cancel() if __name__ == '__main__': detecter = KeyboardHandler() detecter.start_detecting() ================================================ FILE: tickeys/kivy/__init__.py ================================================ ''' Kivy framework ============== Kivy is an open source library for developing multi-touch applications. It is completely cross-platform (Linux/OSX/Win) and released under the terms of the MIT License. It comes with native support for many multi-touch input devices, a growing library of multi-touch aware widgets and hardware accelerated OpenGL drawing. Kivy is designed to let you focus on building custom and highly interactive applications as quickly and easily as possible. With Kivy, you can take full advantage of the dynamic nature of Python. There are thousands of high-quality, free libraries that can be integrated in your application. At the same time, performance-critical parts are implemented in the C language. See http://kivy.org for more information. ''' __all__ = ( 'require', 'kivy_configure', 'kivy_register_post_configuration', 'kivy_options', 'kivy_base_dir', 'kivy_modules_dir', 'kivy_data_dir', 'kivy_shader_dir', 'kivy_icons_dir', 'kivy_home_dir', 'kivy_userexts_dir', 'kivy_config_fn', 'kivy_usermodules_dir', ) __version__ = '1.9.0' import sys import shutil from getopt import getopt, GetoptError from os import environ, mkdir from os.path import dirname, join, basename, exists, expanduser from kivy.logger import Logger, LOG_LEVELS from kivy.utils import platform # internals for post-configuration __kivy_post_configuration = [] if platform == 'macosx' and sys.maxsize < 9223372036854775807: r = '''Unsupported Python version detected!: Kivy requires a 64 bit version of Python to run on OS X. We strongly advise you to use the version of Python that is provided by Apple (don't use ports, fink or homebrew unless you know what you're doing). See http://kivy.org/docs/installation/installation-macosx.html for details. ''' Logger.critical(r) def require(version): '''Require can be used to check the minimum version required to run a Kivy application. For example, you can start your application code like this:: import kivy kivy.require('1.0.1') If a user attempts to run your application with a version of Kivy that is older than the specified version, an Exception is raised. The Kivy version string is built like this:: X.Y.Z[-tag[-tagrevision]] X is the major version Y is the minor version Z is the bugfixes revision The tag is optional, but may be one of 'dev', 'alpha', or 'beta'. The tagrevision is the revision of the tag. .. warning:: You must not ask for a version with a tag, except -dev. Asking for a 'dev' version will just warn the user if the current Kivy version is not a -dev, but it will never raise an exception. You must not ask for a version with a tagrevision. ''' def parse_version(version): # check for tag tag = None tagrev = None if '-' in version: l = version.split('-') if len(l) == 2: version, tag = l elif len(l) == 3: version, tag, tagrev = l else: raise Exception('Revision format must be X.Y.Z[-tag]') # check x y z l = version.split('.') if len(l) != 3: raise Exception('Revision format must be X.Y.Z[-tag]') return [int(x) for x in l], tag, tagrev # user version revision, tag, tagrev = parse_version(version) # current version sysrevision, systag, systagrev = parse_version(__version__) # ensure that the required version don't contain tag, except dev if tag not in (None, 'dev'): raise Exception('Revision format must not have any tag except "dev"') if tag == 'dev' and systag != 'dev': Logger.warning('Application requested a -dev version of Kivy. ' '(You have %s, but the application requires %s)' % ( __version__, version)) # not tag rev (-alpha-1, -beta-x) allowed. if tagrev is not None: raise Exception('Revision format must not contain any tagrevision') # finally, checking revision if sysrevision < revision: raise Exception('The version of Kivy installed on this system ' 'is too old. ' '(You have %s, but the application requires %s)' % ( __version__, version)) def kivy_configure(): '''Call post-configuration of Kivy. This function must be called if you create the window yourself. ''' for callback in __kivy_post_configuration: callback() def kivy_register_post_configuration(callback): '''Register a function to be called when kivy_configure() is called. .. warning:: Internal use only. ''' __kivy_post_configuration.append(callback) def kivy_usage(): '''Kivy Usage: %s [OPTION...]:: -h, --help Prints this help message. -d, --debug Shows debug log. -a, --auto-fullscreen Force 'auto' fullscreen mode (no resolution change). Uses your display's resolution. This is most likely what you want. -c, --config section:key[:value] Set a custom [section] key=value in the configuration object. -f, --fullscreen Force running in fullscreen mode. -k, --fake-fullscreen Force 'fake' fullscreen mode (no window border/decoration). Uses the resolution specified by width and height in your config. -w, --windowed Force running in a window. -p, --provider id:provider[,options] Add an input provider (eg: ccvtable1:tuio,192.168.0.1:3333). -m mod, --module=mod Activate a module (use "list" to get a list of available modules). -r, --rotation Rotate the window's contents (0, 90, 180, 270). -s, --save Save current Kivy configuration. --size=640x480 Size of window geometry. --dpi=96 Manually overload the Window DPI (for testing only.) ''' print(kivy_usage.__doc__ % (basename(sys.argv[0]))) #: Global settings options for kivy kivy_options = { 'window': ('egl_rpi', 'sdl2', 'pygame', 'sdl', 'x11'), 'text': ('pil', 'sdl2', 'pygame', 'sdlttf'), 'video': ( 'gstplayer', 'ffmpeg', 'ffpyplayer', 'gi', 'pygst', 'pyglet', 'null'), 'audio': ('gstplayer', 'pygame', 'gi', 'pygst', 'ffpyplayer', 'sdl2'), 'image': ('tex', 'imageio', 'dds', 'gif', 'sdl2', 'pygame', 'pil', 'ffpy'), 'camera': ('opencv', 'gi', 'pygst', 'videocapture', 'avfoundation'), 'spelling': ('enchant', 'osxappkit', ), 'clipboard': ( 'android', 'winctypes', 'xsel', 'dbusklipper', 'nspaste', 'sdl2', 'pygame', 'dummy', 'gtk3', )} # Read environment for option in kivy_options: key = 'KIVY_%s' % option.upper() if key in environ: try: if type(kivy_options[option]) in (list, tuple): kivy_options[option] = environ[key].split(',') else: kivy_options[option] = environ[key].lower() in \ ('true', '1', 'yes', 'yup') except Exception: Logger.warning('Core: Wrong value for %s environment key' % key) Logger.exception('') # Extract all needed path in kivy #: Kivy directory kivy_base_dir = dirname(sys.modules[__name__].__file__) #: Kivy modules directory kivy_modules_dir = environ.get('KIVY_MODULES_DIR', join(kivy_base_dir, 'modules')) #: Kivy extension directory kivy_exts_dir = environ.get('KIVY_EXTS_DIR', join(kivy_base_dir, 'extensions')) #: Kivy data directory kivy_data_dir = environ.get('KIVY_DATA_DIR', join(kivy_base_dir, 'data')) #: Kivy glsl shader directory kivy_shader_dir = join(kivy_data_dir, 'glsl') #: Kivy icons config path (don't remove the last '') kivy_icons_dir = join(kivy_data_dir, 'icons', '') #: Kivy user-home storage directory kivy_home_dir = '' #: Kivy configuration filename kivy_config_fn = '' #: Kivy user modules directory kivy_usermodules_dir = '' #: Kivy user extensions directory kivy_userexts_dir = '' # Don't go further if we generate documentation if any(name in sys.argv[0] for name in ('sphinx-build', 'autobuild.py')): environ['KIVY_DOC'] = '1' if 'sphinx-build' in sys.argv[0]: environ['KIVY_DOC_INCLUDE'] = '1' if any('nosetests' in arg for arg in sys.argv): environ['KIVY_UNITTEST'] = '1' if any('pyinstaller' in arg for arg in sys.argv): environ['KIVY_PACKAGING'] = '1' if not environ.get('KIVY_DOC_INCLUDE'): # Configuration management if 'KIVY_HOME' in environ: kivy_home_dir = expanduser(environ['KIVY_HOME']) else: user_home_dir = expanduser('~') if platform == 'android': user_home_dir = environ['ANDROID_APP_PATH'] elif platform == 'ios': user_home_dir = join(expanduser('~'), 'Documents') kivy_home_dir = join(user_home_dir, '.kivy') kivy_config_fn = join(kivy_home_dir, 'config.ini') kivy_usermodules_dir = join(kivy_home_dir, 'mods') kivy_userexts_dir = join(kivy_home_dir, 'extensions') icon_dir = join(kivy_home_dir, 'icon') if 'KIVY_NO_CONFIG' not in environ: if not exists(kivy_home_dir): mkdir(kivy_home_dir) if not exists(kivy_usermodules_dir): mkdir(kivy_usermodules_dir) if not exists(kivy_userexts_dir): mkdir(kivy_userexts_dir) if not exists(icon_dir): try: shutil.copytree(join(kivy_data_dir, 'logo'), icon_dir) except: Logger.exception('Error when copying logo directory') # configuration from kivy.config import Config # Set level of logger level = LOG_LEVELS.get(Config.get('kivy', 'log_level')) Logger.setLevel(level=level) # Can be overrided in command line if ('KIVY_UNITTEST' not in environ and 'KIVY_PACKAGING' not in environ and 'KIVY_NO_ARGS' not in environ): # save sys argv, otherwize, gstreamer use it and display help.. sys_argv = sys.argv sys.argv = sys.argv[:1] try: opts, args = getopt(sys_argv[1:], 'hp:fkawFem:sr:dc:', [ 'help', 'fullscreen', 'windowed', 'fps', 'event', 'module=', 'save', 'fake-fullscreen', 'auto-fullscreen', 'display=', 'size=', 'rotate=', 'config=', 'debug', 'dpi=']) except GetoptError as err: Logger.error('Core: %s' % str(err)) kivy_usage() sys.exit(2) # set argv to the non-read args sys.argv = sys_argv[0:1] + args else: opts = [] args = [] need_save = False for opt, arg in opts: if opt in ('-h', '--help'): kivy_usage() sys.exit(0) elif opt in ('-p', '--provider'): try: pid, args = arg.split(':', 1) Config.set('input', pid, args) except ValueError: # when we are doing an executable on macosx with # pyinstaller, they are passing information with -p. so # it will conflict with our current -p option. since the # format is not the same, just avoid it. pass elif opt in ('-a', '--auto-fullscreen'): Config.set('graphics', 'fullscreen', 'auto') elif opt in ('-c', '--config'): l = arg.split(':', 2) if len(l) == 2: Config.set(l[0], l[1], '') elif len(l) == 3: Config.set(l[0], l[1], l[2]) else: raise Exception('Invalid --config value') if l[0] == 'kivy' and l[1] == 'log_level': level = LOG_LEVELS.get(Config.get('kivy', 'log_level')) Logger.setLevel(level=level) elif opt in ('-k', '--fake-fullscreen'): Config.set('graphics', 'fullscreen', 'fake') elif opt in ('-f', '--fullscreen'): Config.set('graphics', 'fullscreen', '1') elif opt in ('-w', '--windowed'): Config.set('graphics', 'fullscreen', '0') elif opt in ('--size', ): w, h = str(arg).split('x') Config.set('graphics', 'width', w) Config.set('graphics', 'height', h) elif opt in ('--display', ): Config.set('graphics', 'display', str(arg)) elif opt in ('-m', '--module'): if str(arg) == 'list': from kivy.modules import Modules Modules.usage_list() sys.exit(0) args = arg.split(':', 1) if len(args) == 1: args += [''] Config.set('modules', args[0], args[1]) elif opt in ('-s', '--save'): need_save = True elif opt in ('-r', '--rotation'): Config.set('graphics', 'rotation', arg) elif opt in ('-d', '--debug'): level = LOG_LEVELS.get('debug') Logger.setLevel(level=level) elif opt == '--dpi': environ['KIVY_DPI'] = arg if need_save and 'KIVY_NO_CONFIG' not in environ: try: with open(kivy_config_fn, 'w') as fd: Config.write(fd) except Exception as e: Logger.exception('Core: error while saving default' 'configuration file:', str(e)) Logger.info('Core: Kivy configuration saved.') sys.exit(0) # configure all activated modules from kivy.modules import Modules Modules.configure() # android hooks: force fullscreen and add android touch input provider if platform in ('android', 'ios'): from kivy.config import Config Config.set('graphics', 'fullscreen', 'auto') Config.remove_section('input') Config.add_section('input') if platform == 'android': Config.set('input', 'androidtouch', 'android') Logger.info('Kivy: v%s' % (__version__)) Logger.info('Python: v{}'.format(sys.version)) ================================================ FILE: tickeys/kivy/_event.pxd ================================================ from cpython.ref cimport PyObject cdef class ObjectWithUid(object): cdef readonly int uid cdef class Observable(ObjectWithUid): cdef object __fast_bind_mapping cdef object bound_uid cdef class EventDispatcher(ObjectWithUid): cdef dict __event_stack cdef dict __properties cdef dict __storage cdef object __weakref__ cpdef dict properties(self) cdef enum BoundLock: # the state of the BoundCallback, i.e. whether it can be deleted unlocked # whether the BoundCallback is unlocked and can be deleted locked # whether the BoundCallback is locked and cannot be deleted deleted # whether the locked BoundCallback was marked for deletion cdef class BoundCallback: cdef object func cdef tuple largs cdef dict kwargs cdef int is_ref # if func is a ref to the function cdef BoundLock lock # see BoundLock cdef BoundCallback next # next callback in chain cdef BoundCallback prev # previous callback in chain cdef object uid # the uid given for this callback, None if not given cdef class EventObservers: # If dispatching should occur in normal or reverse order of binding. cdef int dispatch_reverse # If in dispatch, the value parameter is dispatched or ignored. cdef int dispatch_value # The first callback bound cdef BoundCallback first_callback # The last callback bound cdef BoundCallback last_callback # The uid to assign to the next bound callback. cdef object uid cdef inline void bind(self, object observer, int is_ref=*) except * cdef inline object fast_bind(self, object observer, tuple largs, dict kwargs, int is_ref) cdef inline void unbind(self, object observer, int is_ref, int stop_on_first) except * cdef inline void fast_unbind(self, object observer, tuple largs, dict kwargs) except * cdef inline object unbind_uid(self, object uid) cdef inline void remove_callback(self, BoundCallback callback, int force=*) except * cdef inline object _dispatch( self, object f, tuple slargs, dict skwargs, object obj, object value, tuple largs, dict kwargs) cdef inline int dispatch(self, object obj, object value, tuple largs, dict kwargs, int stop_on_true) except 2 ================================================ FILE: tickeys/kivy/adapters/__init__.py ================================================ ''' Adapters ======== .. versionadded:: 1.5.0 An adapter is a mediating controller-type class that processes and presents data for use in views. It does this by generating models, generally lists of :class:`~kivy.uix.listview.SelectableView` items, that are consumed and presented by views. Views are top-level widgets, such as a :class:`~kivy.uix.listview.ListView`, that allow users to scroll through and (optionally) interact with your data. The Concept ----------- Kivy adapters are modelled on the `Adapter design pattern `_. Conceptually, they play the role of a 'controller' between you data and views in a `Model-View-Controller `_ type architecture. The role of an adapter can be depicted as follows: .. image:: images/adapters.png The Components -------------- The components involved in this process are: - **Adapters**: The adapter plays a mediating role between the user interface and your data. It manages the creation of the view elements for the model using the args_converter to prepare the contructor arguments for your cls/template view items. The base :class:`Adapter` is subclassed by the :class:`SimpleListAdapter` and :class:`ListAdapter`. The :class:`DictAdapter` is a more advanced and flexible subclass of :class:`ListAdapter`. :doc:`api-kivy.adapters.adapter`, :doc:`api-kivy.adapters.simplelistadapter`, :doc:`api-kivy.adapters.listadapter`, :doc:`api-kivy.adapters.dictadapter`. - **Models**: The data for which an adapter serves as a bridge to views can be any sort of data. However, for convenience, model mixin classes can ease the preparation or shaping of data for use in the system. For selection operations, the :class:`SelectableDataItem` can optionally prepare data items to provide and receive selection information (data items are not required to be "selection-aware", but in some cases it may be desired). :doc:`api-kivy.adapters.models`. - **Args Converters**: Argument converters are made by the application programmer to do the work of converting data items to argument dictionaries suitable for instantiating views. In effect, they take each row of your data and create dictionaries that are passed into the constructors of your cls/template which are then used populate your View. :doc:`api-kivy.adapters.args_converters`. - **Views**: Models of your data are presented to the user via views. Each of your data items create a corresponding view subitem (the cls or template) presented in a list by the View. The base :class:`AbstractView` currently has one concrete implementation: the :class:`ListView`. :doc:`api-kivy.uix.abstractview`, :doc:`api-kivy.uix.listview`. ---- ''' ================================================ FILE: tickeys/kivy/adapters/adapter.py ================================================ ''' Adapter ======= .. versionadded:: 1.5 .. warning:: This code is still experimental, and its API is subject to change in a future version. An :class:`~kivy.adapters.adapter.Adapter` is a bridge between data and an :class:`~kivy.uix.abstractview.AbstractView` or one of its subclasses, such as a :class:`~kivy.uix.listview.ListView`. The following arguments can be passed to the contructor to initialise the corresponding properties: * :attr:`~Adapter.data`: for any sort of data to be used in a view. For an :class:`~kivy.adapters.adapter.Adapter`, data can be an object as well as a list, dict, etc. For a :class:`~kivy.adapters.listadapter.ListAdapter`, data should be a list. For a :class:`~kivy.adapters.dictadapter.DictAdapter`, data should be a dict. * :attr:`~Adapter.cls`: the class used to instantiate each list item view instance (Use this or the template argument). * :attr:`~Adapter.template`: a kv template to use to instantiate each list item view instance (Use this or the cls argument). * :attr:`~Adapter.args_converter`: a function used to transform the data items in preparation for either a cls instantiation or a kv template invocation. If no args_converter is provided, the data items are assumed to be simple strings. Please refer to the :mod:`~kivy.adapters` documentation for an overview of how adapters are used. ''' __all__ = ('Adapter', ) from kivy.event import EventDispatcher from kivy.properties import ObjectProperty from kivy.lang import Builder from kivy.adapters.args_converters import list_item_args_converter from kivy.factory import Factory from kivy.compat import string_types class Adapter(EventDispatcher): '''An :class:`~kivy.adapters.adapter.Adapter` is a bridge between data and an :class:`~kivy.uix.abstractview.AbstractView` or one of its subclasses, such as a :class:`~kivy.uix.listview.ListView`. ''' data = ObjectProperty(None) ''' The data for which a view is to be constructed using either the cls or template provided, together with the args_converter provided or the default args_converter. In this base class, data is an ObjectProperty, so it could be used for a wide variety of single-view needs. Subclasses may override it in order to use another data type, such as a :class:`~kivy.properties.ListProperty` or :class:`~kivy.properties.DictProperty` as appropriate. For example, in a :class:`~.kivy.adapters.listadapter.ListAdapter`, data is a :class:`~kivy.properties.ListProperty`. :attr:`data` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' cls = ObjectProperty(None) ''' A class for instantiating a given view item (Use this or template). If this is not set and neither is the template, a :class:`~kivy.uix.label.Label` is used for the view item. :attr:`cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' template = ObjectProperty(None) ''' A kv template for instantiating a given view item (Use this or cls). :attr:`template` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' args_converter = ObjectProperty(None) ''' A function that prepares an args dict for the cls or kv template to build a view from a data item. If an args_converter is not provided, a default one is set that assumes simple content in the form of a list of strings. :attr:`args_converter` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' def __init__(self, **kwargs): if 'data' not in kwargs: raise Exception('adapter: input must include data argument') if 'cls' in kwargs: if 'template' in kwargs: msg = 'adapter: cannot use cls and template at the same time' raise Exception(msg) elif not kwargs['cls']: raise Exception('adapter: a cls or template must be defined') else: if 'template' in kwargs: if not kwargs['template']: msg = 'adapter: a cls or template must be defined' raise Exception(msg) else: raise Exception('adapter: a cls or template must be defined') if 'args_converter' in kwargs: self.args_converter = kwargs['args_converter'] else: self.args_converter = list_item_args_converter super(Adapter, self).__init__(**kwargs) def bind_triggers_to_view(self, func): self.bind(data=func) def get_data_item(self): return self.data def get_cls(self): ''' .. versionadded:: 1.9.0 Returns the widget type specified by self.cls. If it is a string, the :class:`~kivy.factory.Factory` is queried to retrieve the widget class with the given name, otherwise it is returned directly. ''' cls = self.cls if isinstance(cls, string_types): try: cls = getattr(Factory, cls) except AttributeError: raise AttributeError( 'Listadapter cls widget does not exist.') return cls def get_view(self, index): # pragma: no cover item_args = self.args_converter(self.data) cls = self.get_cls() if cls: return cls(**item_args) else: return Builder.template(self.template, **item_args) ================================================ FILE: tickeys/kivy/adapters/args_converters.py ================================================ ''' List Item View Argument Converters ================================== .. versionadded:: 1.5 The default list item args converter for list adapters is a function (shown below) that takes a row index and a string. It returns a dict with the string as the *text* item, along with two properties suited for simple text items with a height of 25. Simple Usage ------------ Argument converters may be normal functions or, as in the case of the default args converter, lambdas:: list_item_args_converter = lambda row_index, x: {'text': x, 'size_hint_y': None, 'height': 25} Advanced Usage -------------- Typically, having the argument converter perform a simple mapping suffices. There are times, however, when more complex manipulation is required. When using a :class:`~kivy.uix.listview.CompositeListItem`, it is possible to specify a list of cls dictionaries. This allows you so compose a single view item out of multiple classes, each of which can recieve their own class constructor arguments via the *kwargs* keyword:: args_converter = lambda row_index, rec: \\ {'text': rec['text'], 'size_hint_y': None, 'height': 25, 'cls_dicts': [{'cls': ListItemButton, 'kwargs': {'text': rec['text']}}, {'cls': ListItemLabel, 'kwargs': {'text': "Middle-{0}".format(rec['text']), 'is_representing_cls': True}}, {'cls': ListItemButton, 'kwargs': {'text': rec['text']}}]} Please see the `list_composite.py `_ for a complete example. ''' list_item_args_converter = lambda row_index, x: {'text': x, 'size_hint_y': None, 'height': 25} ================================================ FILE: tickeys/kivy/adapters/dictadapter.py ================================================ ''' DictAdapter =========== .. versionadded:: 1.5 .. warning:: This code is still experimental, and its API is subject to change in a future version. A :class:`~kivy.adapters.dictadapter.DictAdapter` is an adapter around a python dictionary of records. It extends the list-like capabilities of the :class:`~kivy.adapters.listadapter.ListAdapter`. If you wish to have a bare-bones list adapter, without selection, use the :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter`. ''' __all__ = ('DictAdapter', ) from kivy.properties import ListProperty, DictProperty from kivy.adapters.listadapter import ListAdapter class DictAdapter(ListAdapter): '''A :class:`~kivy.adapters.dictadapter.DictAdapter` is an adapter around a python dictionary of records. It extends the list-like capabilities of the :class:`~kivy.adapters.listadapter.ListAdapter`. ''' sorted_keys = ListProperty([]) '''The sorted_keys list property contains a list of hashable objects (can be strings) that will be used directly if no args_converter function is provided. If there is an args_converter, the record received from a lookup of the data, using keys from sorted_keys, will be passed to it for instantiation of list item view class instances. :attr:`sorted_keys` is a :class:`~kivy.properties.ListProperty` and defaults to []. ''' data = DictProperty(None) '''A dict that indexes records by keys that are equivalent to the keys in sorted_keys, or they are a superset of the keys in sorted_keys. The values can be strings, class instances, dicts, etc. :attr:`data` is a :class:`~kivy.properties.DictProperty` and defaults to None. ''' def __init__(self, **kwargs): if 'sorted_keys' in kwargs: if type(kwargs['sorted_keys']) not in (tuple, list): msg = 'DictAdapter: sorted_keys must be tuple or list' raise Exception(msg) else: self.sorted_keys = sorted(kwargs['data'].keys()) super(DictAdapter, self).__init__(**kwargs) self.bind(sorted_keys=self.initialize_sorted_keys) def bind_triggers_to_view(self, func): self.bind(sorted_keys=func) self.bind(data=func) # self.data is paramount to self.sorted_keys. If sorted_keys is reset to # mismatch data, force a reset of sorted_keys to data.keys(). So, in order # to do a complete reset of data and sorted_keys, data must be reset # first, followed by a reset of sorted_keys, if needed. def initialize_sorted_keys(self, *args, **kwargs): stale_sorted_keys = False for key in self.sorted_keys: if not key in self.data: stale_sorted_keys = True break else: if kwargs.get('new_data'): if len(self.sorted_keys) != len(self.data): stale_sorted_keys = True if stale_sorted_keys: self.sorted_keys = sorted(self.data.keys()) self.delete_cache() self.initialize_selection() # Override ListAdapter.update_for_new_data(). def update_for_new_data(self, *args): self.initialize_sorted_keys(new_data=True) # Note: this is not len(self.data). def get_count(self): return len(self.sorted_keys) def get_data_item(self, index): if index < 0 or index >= len(self.sorted_keys): return None return self.data[self.sorted_keys[index]] # [TODO] Also make methods for scroll_to_sel_start, scroll_to_sel_end, # scroll_to_sel_middle. def trim_left_of_sel(self, *args): '''Cut list items with indices in sorted_keys that are less than the index of the first selected item, if there is a selection. sorted_keys will be updated by update_for_new_data(). ''' if len(self.selection) > 0: selected_keys = [sel.text for sel in self.selection] first_sel_index = self.sorted_keys.index(selected_keys[0]) desired_keys = self.sorted_keys[first_sel_index:] self.data = dict([(key, self.data[key]) for key in desired_keys]) def trim_right_of_sel(self, *args): '''Cut list items with indices in sorted_keys that are greater than the index of the last selected item, if there is a selection. sorted_keys will be updated by update_for_new_data(). ''' if len(self.selection) > 0: selected_keys = [sel.text for sel in self.selection] last_sel_index = self.sorted_keys.index(selected_keys[-1]) desired_keys = self.sorted_keys[:last_sel_index + 1] self.data = dict([(key, self.data[key]) for key in desired_keys]) def trim_to_sel(self, *args): '''Cut list items with indices in sorted_keys that are les than or greater than the index of the last selected item, if there is a selection. This preserves intervening list items within the selected range. sorted_keys will be updated by update_for_new_data(). ''' if len(self.selection) > 0: selected_keys = [sel.text for sel in self.selection] first_sel_index = self.sorted_keys.index(selected_keys[0]) last_sel_index = self.sorted_keys.index(selected_keys[-1]) desired_keys = self.sorted_keys[first_sel_index:last_sel_index + 1] self.data = dict([(key, self.data[key]) for key in desired_keys]) def cut_to_sel(self, *args): '''Same as trim_to_sel, but intervening list items within the selected range are also cut, leaving only list items that are selected. sorted_keys will be updated by update_for_new_data(). ''' if len(self.selection) > 0: selected_keys = [sel.text for sel in self.selection] self.data = dict([(key, self.data[key]) for key in selected_keys]) ================================================ FILE: tickeys/kivy/adapters/listadapter.py ================================================ ''' ListAdapter ================= .. versionadded:: 1.5 .. warning:: This code is still experimental, and its API is subject to change in a future version. A :class:`ListAdapter` is an adapter around a python list and adds support for selection operations. If you wish to have a bare-bones list adapter, without selection, use a :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter`. From an :class:`~kivy.adapters.Adapter`, a :class:`ListAdapter` inherits cls, template, and args_converter properties and adds others that control selection behaviour: * :attr:`~ListAdapter.selection`: a list of selected items. * :attr:`~ListAdapter.selection_mode`: one of 'single', 'multiple' or 'none'. * :attr:`~ListAdapter.allow_empty_selection`: a boolean. If False, a selection is forced. If True, and only user or programmatic action will change selection, it can be empty. A :class:`~kivy.adapters.dictadapter.DictAdapter` is a subclass of a :class:`~kivy.adapters.listadapter.ListAdapter`. They both dispatch the :attr:`~ListAdapter.on_selection_change` event when selection changes. .. versionchanged:: 1.6.0 Added data = ListProperty([]), which was proably inadvertently deleted at some point. This means that whenever data changes an update will fire, instead of having to reset the data object (Adapter has data defined as an ObjectProperty, so we need to reset it here to ListProperty). See also DictAdapter and its set of data = DictProperty(). ''' __all__ = ('ListAdapter', ) import inspect from kivy.event import EventDispatcher from kivy.adapters.adapter import Adapter from kivy.adapters.models import SelectableDataItem from kivy.properties import ListProperty from kivy.properties import DictProperty from kivy.properties import BooleanProperty from kivy.properties import OptionProperty from kivy.properties import NumericProperty from kivy.lang import Builder class ListAdapter(Adapter, EventDispatcher): ''' A base class for adapters interfacing with lists, dictionaries or other collection type data, adding selection, view creation and management functonality. ''' data = ListProperty([]) '''The data list property is redefined here, overriding its definition as an ObjectProperty in the Adapter class. We bind to data so that any changes will trigger updates. See also how the :class:`~kivy.adapters.DictAdapter` redefines data as a :class:`~kivy.properties.DictProperty`. :attr:`data` is a :class:`~kivy.properties.ListProperty` and defaults to []. ''' selection = ListProperty([]) '''The selection list property is the container for selected items. :attr:`selection` is a :class:`~kivy.properties.ListProperty` and defaults to []. ''' selection_mode = OptionProperty('single', options=('none', 'single', 'multiple')) '''The selection_mode is a string and can be set to one of the following values: * 'none': use the list as a simple list (no select action). This option is here so that selection can be turned off, momentarily or permanently, for an existing list adapter. A :class:`~kivy.adapters.listadapter.ListAdapter` is not meant to be used as a primary no-selection list adapter. Use a :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` for that. * 'single': multi-touch/click ignored. Single item selection only. * 'multiple': multi-touch / incremental addition to selection allowed; may be limited to a count by setting the :attr:`~ListAdapter.selection_limit`. :attr:`selection_mode` is an :class:`~kivy.properties.OptionProperty` and defaults to 'single'. ''' propagate_selection_to_data = BooleanProperty(False) '''Normally, data items are not selected/deselected because the data items might not have an is_selected boolean property -- only the item view for a given data item is selected/deselected as part of the maintained selection list. However, if the data items do have an is_selected property, or if they mix in :class:`~kivy.adapters.models.SelectableDataItem`, the selection machinery can propagate selection to data items. This can be useful for storing selection state in a local database or backend database for maintaining state in game play or other similar scenarios. It is a convenience function. To propagate selection or not? Consider a shopping list application for shopping for fruits at the market. The app allows for the selection of fruits to buy for each day of the week, presenting seven lists: one for each day of the week. Each list is loaded with all the available fruits, but the selection for each is a subset. There is only one set of fruit data shared between the lists, so it would not make sense to propagate selection to the data because selection in any of the seven lists would clash and mix with that of the others. However, consider a game that uses the same fruits data for selecting fruits available for fruit-tossing. A given round of play could have a full fruits list, with fruits available for tossing shown selected. If the game is saved and rerun, the full fruits list, with selection marked on each item, would be reloaded correctly if selection is always propagated to the data. You could accomplish the same functionality by writing code to operate on list selection, but having selection stored in the data ListProperty might prove convenient in some cases. .. note:: This setting should be set to True if you wish to initialize the view with item views already selected. :attr:`propagate_selection_to_data` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' allow_empty_selection = BooleanProperty(True) '''The allow_empty_selection may be used for cascading selection between several list views, or between a list view and an observing view. Such automatic maintenance of the selection is important for all but simple list displays. Set allow_empty_selection to False and the selection is auto-initialized and always maintained, so any observing views may likewise be updated to stay in sync. :attr:`allow_empty_selection` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' selection_limit = NumericProperty(-1) '''When the :attr:`~ListAdapter.selection_mode` is 'multiple' and the selection_limit is non-negative, this number will limit the number of selected items. It can be set to 1, which is equivalent to single selection. If selection_limit is not set, the default value is -1, meaning that no limit will be enforced. :attr:`selection_limit` is a :class:`~kivy.properties.NumericProperty` and defaults to -1 (no limit). ''' cached_views = DictProperty({}) '''View instances for data items are instantiated and managed by the adapter. Here we maintain a dictionary containing the view instances keyed to the indices in the data. This dictionary works as a cache. get_view() only asks for a view from the adapter if one is not already stored for the requested index. :attr:`cached_views` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' __events__ = ('on_selection_change', ) def __init__(self, **kwargs): super(ListAdapter, self).__init__(**kwargs) self.bind(selection_mode=self.selection_mode_changed, allow_empty_selection=self.check_for_empty_selection, data=self.update_for_new_data) self.update_for_new_data() def delete_cache(self, *args): self.cached_views = {} def get_count(self): return len(self.data) def get_data_item(self, index): if index < 0 or index >= len(self.data): return None return self.data[index] def selection_mode_changed(self, *args): if self.selection_mode == 'none': for selected_view in self.selection: self.deselect_item_view(selected_view) else: self.check_for_empty_selection() def get_view(self, index): if index in self.cached_views: return self.cached_views[index] item_view = self.create_view(index) if item_view: self.cached_views[index] = item_view return item_view def create_view(self, index): '''This method is more complicated than the ones in the :class:`~kivy.adapters.adapter.Adapter` and :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` classes because here we create bindings for the data items and their children back to the *self.handle_selection()* event. We also perform other selection-related tasks to keep item views in sync with the data. ''' item = self.get_data_item(index) if item is None: return None item_args = self.args_converter(index, item) item_args['index'] = index cls = self.get_cls() if cls: view_instance = cls(**item_args) else: view_instance = Builder.template(self.template, **item_args) if self.propagate_selection_to_data: # The data item must be a subclass of SelectableDataItem, or must # have an is_selected boolean or function, so it has is_selected # available. If is_selected is unavailable on the data item, an # exception is raised. # if isinstance(item, SelectableDataItem): if item.is_selected: self.handle_selection(view_instance) elif type(item) == dict and 'is_selected' in item: if item['is_selected']: self.handle_selection(view_instance) elif hasattr(item, 'is_selected'): if (inspect.isfunction(item.is_selected) or inspect.ismethod(item.is_selected)): if item.is_selected(): self.handle_selection(view_instance) else: if item.is_selected: self.handle_selection(view_instance) else: msg = "ListAdapter: unselectable data item for {0}" raise Exception(msg.format(index)) view_instance.bind(on_release=self.handle_selection) for child in view_instance.children: child.bind(on_release=self.handle_selection) return view_instance def on_selection_change(self, *args): '''on_selection_change() is the default handler for the on_selection_change event. You can bind to this event to get notified of selection changes. :Parameters: adapter: :class:`~ListAdapter` or subclass The instance of the list adapter where the selection changed. Use the adapters :attr:`selection` property to see what has been selected. ''' pass def handle_selection(self, view, hold_dispatch=False, *args): if view not in self.selection: if self.selection_mode in ['none', 'single'] and \ len(self.selection) > 0: for selected_view in self.selection: self.deselect_item_view(selected_view) if self.selection_mode != 'none': if self.selection_mode == 'multiple': if self.allow_empty_selection: # If < 0, selection_limit is not active. if self.selection_limit < 0: self.select_item_view(view) else: if len(self.selection) < self.selection_limit: self.select_item_view(view) else: self.select_item_view(view) else: self.select_item_view(view) else: self.deselect_item_view(view) if self.selection_mode != 'none': # If the deselection makes selection empty, the following call # will check allows_empty_selection, and if False, will # select the first item. If view happens to be the first item, # this will be a reselection, and the user will notice no # change, except perhaps a flicker. # self.check_for_empty_selection() if not hold_dispatch: self.dispatch('on_selection_change') def select_data_item(self, item): self.set_data_item_selection(item, True) def deselect_data_item(self, item): self.set_data_item_selection(item, False) def set_data_item_selection(self, item, value): if isinstance(item, SelectableDataItem): item.is_selected = value elif type(item) == dict: item['is_selected'] = value elif hasattr(item, 'is_selected'): if (inspect.isfunction(item.is_selected) or inspect.ismethod(item.is_selected)): item.is_selected() else: item.is_selected = value def select_item_view(self, view): view.select() view.is_selected = True self.selection.append(view) # [TODO] sibling selection for composite items # Needed? Or handled from parent? # (avoid circular, redundant selection) #if hasattr(view, 'parent') and hasattr(view.parent, 'children'): #siblings = [child for child in view.parent.children if child != view] #for sibling in siblings: #if hasattr(sibling, 'select'): #sibling.select() if self.propagate_selection_to_data: data_item = self.get_data_item(view.index) self.select_data_item(data_item) def select_list(self, view_list, extend=True): '''The select call is made for the items in the provided view_list. Arguments: view_list: the list of item views to become the new selection, or to add to the existing selection extend: boolean for whether or not to extend the existing list ''' if not extend: self.selection = [] for view in view_list: self.handle_selection(view, hold_dispatch=True) self.dispatch('on_selection_change') def deselect_item_view(self, view): view.deselect() view.is_selected = False self.selection.remove(view) # [TODO] sibling deselection for composite items # Needed? Or handled from parent? # (avoid circular, redundant selection) #if hasattr(view, 'parent') and hasattr(view.parent, 'children'): #siblings = [child for child in view.parent.children if child != view] #for sibling in siblings: #if hasattr(sibling, 'deselect'): #sibling.deselect() if self.propagate_selection_to_data: item = self.get_data_item(view.index) self.deselect_data_item(item) def deselect_list(self, l): for view in l: self.handle_selection(view, hold_dispatch=True) self.dispatch('on_selection_change') # [TODO] Could easily add select_all() and deselect_all(). def update_for_new_data(self, *args): self.delete_cache() self.initialize_selection() def initialize_selection(self, *args): if len(self.selection) > 0: self.selection = [] self.dispatch('on_selection_change') self.check_for_empty_selection() def check_for_empty_selection(self, *args): if not self.allow_empty_selection: if len(self.selection) == 0: # Select the first item if we have it. v = self.get_view(0) if v is not None: self.handle_selection(v) # [TODO] Also make methods for scroll_to_sel_start, scroll_to_sel_end, # scroll_to_sel_middle. def trim_left_of_sel(self, *args): '''Cut list items with indices in sorted_keys that are less than the index of the first selected item if there is a selection. ''' if len(self.selection) > 0: first_sel_index = min([sel.index for sel in self.selection]) self.data = self.data[first_sel_index:] def trim_right_of_sel(self, *args): '''Cut list items with indices in sorted_keys that are greater than the index of the last selected item if there is a selection. ''' if len(self.selection) > 0: last_sel_index = max([sel.index for sel in self.selection]) print('last_sel_index', last_sel_index) self.data = self.data[:last_sel_index + 1] def trim_to_sel(self, *args): '''Cut list items with indices in sorted_keys that are less than or greater than the index of the last selected item if there is a selection. This preserves intervening list items within the selected range. ''' if len(self.selection) > 0: sel_indices = [sel.index for sel in self.selection] first_sel_index = min(sel_indices) last_sel_index = max(sel_indices) self.data = self.data[first_sel_index:last_sel_index + 1] def cut_to_sel(self, *args): '''Same as trim_to_sel, but intervening list items within the selected range are also cut, leaving only list items that are selected. ''' if len(self.selection) > 0: self.data = self.selection ================================================ FILE: tickeys/kivy/adapters/models.py ================================================ ''' SelectableDataItem ================== .. versionadded:: 1.5 .. warning:: This code is still experimental, and its API is subject to change in a future version. Data Models ----------- Kivy is open about the type of data used in applications built with the system. However, base classes are sometimes needed to ensure data conforms to the requirements of some parts of the system. A :class:`SelectableDataItem` is a basic Python data model class that can be used as a mixin to build data objects that are compatible with Kivy's :class:`~kivy.adapters.adapter.Adapter` and selection system and which work with views such as a :class:`~kivy.uix.listview.ListView`. A boolean *is_selected* property a requirement. The default operation of the selection system is to not propogate selection in views such as ListView to the underlying data: selection is by default a view-only operation. However, in some cases, it is useful to propogate selection to the actual data items. You may, of course, build your own Python data model system as the backend for a Kivy application. For instance, to use the `Google App Engine Data Modeling `_ system with Kivy, you could define your class as follows:: from google.appengine.ext import db class MySelectableDataItem(db.Model): # ... other properties is_selected = db.BooleanProperty() It is easy to build such a class with plain Python. ''' __all__ = ('SelectableDataItem', ) class SelectableDataItem(object): ''' A mixin class containing requirements for selection operations. ''' def __init__(self, **kwargs): super(SelectableDataItem, self).__init__() self._is_selected = kwargs.get('is_selected', False) @property def is_selected(self): """A boolean property indicating whether the data item is selected or not.""" return self._is_selected @is_selected.setter def is_selected(self, value): self._is_selected = value ================================================ FILE: tickeys/kivy/adapters/simplelistadapter.py ================================================ ''' SimpleListAdapter ================= .. versionadded:: 1.5 .. warning:: This code is still experimental, and its API is subject to change in a future version. The :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` is used for basic lists. For example, it can be used for displaying a list of read-only strings that do not require user interaction. ''' __all__ = ('SimpleListAdapter', ) from kivy.adapters.adapter import Adapter from kivy.properties import ListProperty from kivy.lang import Builder class SimpleListAdapter(Adapter): '''A :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` is an adapter around a Python list. From :class:`~kivy.adapters.adapter.Adapter`, the :class:`~kivy.adapters.simplelistadapter.ListAdapter` gets cls, template, and args_converter properties. ''' data = ListProperty([]) '''The data list property contains a list of objects (which can be strings) that will be used directly if no args_converter function is provided. If there is an args_converter, the data objects will be passed to it for instantiating the item view class instances. :attr:`data` is a :class:`~kivy.properties.ListProperty` and defaults to []. ''' def __init__(self, **kwargs): if 'data' not in kwargs: raise Exception('list adapter: input must include data argument') if not isinstance(kwargs['data'], list) and \ not isinstance(kwargs['data'], tuple): raise Exception('list adapter: data must be a tuple or list') super(SimpleListAdapter, self).__init__(**kwargs) def get_count(self): return len(self.data) def get_data_item(self, index): if index < 0 or index >= len(self.data): return None return self.data[index] # Returns a view instance for an item. def get_view(self, index): item = self.get_data_item(index) if item is None: return None item_args = self.args_converter(index, item) cls = self.get_cls() if cls: instance = cls(**item_args) return instance else: return Builder.template(self.template, **item_args) ================================================ FILE: tickeys/kivy/animation.py ================================================ ''' Animation ========= :class:`Animation` and :class:`AnimationTransition` are used to animate :class:`~kivy.uix.widget.Widget` properties. You must specify at least a property name and target value. To use an Animation, follow these steps: * Setup an Animation object * Use the Animation object on a Widget Simple animation ---------------- To animate a Widget's x or y position, simply specify the target x/y values where you want the widget positioned at the end of the animation:: anim = Animation(x=100, y=100) anim.start(widget) The animation will last for 1 second unless :attr:`duration` is specified. When anim.start() is called, the Widget will move smoothly from the current x/y position to (100, 100). Multiple properties and transitions ----------------------------------- You can animate multiple properties and use built-in or custom transition functions using :attr:`transition` (or the `t=` shortcut). For example, to animate the position and size using the 'in_quad' transition:: anim = Animation(x=50, size=(80, 80), t='in_quad') anim.start(widget) Note that the `t=` parameter can be the string name of a method in the :class:`AnimationTransition` class or your own animation function. Sequential animation -------------------- To join animations sequentially, use the '+' operator. The following example will animate to x=50 over 1 second, then animate the size to (80, 80) over the next two seconds:: anim = Animation(x=50) + Animation(size=(80, 80), duration=2.) anim.start(widget) Parallel animation ------------------ To join animations in parallel, use the '&' operator. The following example will animate the position to (80, 10) over 1 second, whilst in parallel animating the size to (800, 800):: anim = Animation(pos=(80, 10)) anim &= Animation(size=(800, 800), duration=2.) anim.start(widget) Keep in mind that creating overlapping animations on the same property may have unexpected results. If you want to apply multiple animations to the same property, you should either schedule them sequentially (via the '+' operator or using the *on_complete* callback) or cancel previous animations using the :attr:`~Animation.cancel_all` method. Repeating animation ------------------- .. versionadded:: 1.8.0 .. note:: This is currently only implemented for 'Sequence' animations. To set an animation to repeat, simply set the :attr:`Sequence.repeat` property to `True`:: anim = Animation(...) + Animation(...) anim.repeat = True anim.start(widget) For flow control of animations such as stopping and cancelling, use the methods already in place in the animation module. ''' __all__ = ('Animation', 'AnimationTransition') from math import sqrt, cos, sin, pi from kivy.event import EventDispatcher from kivy.clock import Clock from kivy.compat import string_types, iterkeys from kivy.weakproxy import WeakProxy class Animation(EventDispatcher): '''Create an animation definition that can be used to animate a Widget. :Parameters: `duration` or `d`: float, defaults to 1. Duration of the animation, in seconds. `transition` or `t`: str or func Transition function for animate properties. It can be the name of a method from :class:`AnimationTransition`. `step` or `s`: float Step in milliseconds of the animation. Defaults to 1 / 60. :Events: `on_start`: widget Fired when the animation is started on a widget. `on_complete`: widget Fired when the animation is completed or stopped on a widget. `on_progress`: widget, progression Fired when the progression of the animation is changing. .. versionchanged:: 1.4.0 Added s/step parameter. ''' _instances = set() __events__ = ('on_start', 'on_progress', 'on_complete') def __init__(self, **kw): super(Animation, self).__init__(**kw) # Initialize self._clock_installed = False self._duration = kw.get('d', kw.get('duration', 1.)) self._transition = kw.get('t', kw.get('transition', 'linear')) self._step = kw.get('s', kw.get('step', 1. / 60.)) if isinstance(self._transition, string_types): self._transition = getattr(AnimationTransition, self._transition) for key in ('d', 't', 's', 'step', 'duration', 'transition'): kw.pop(key, None) self._animated_properties = kw self._widgets = {} @property def duration(self): '''Return the duration of the animation. ''' return self._duration @property def transition(self): '''Return the transition of the animation. ''' return self._transition @property def animated_properties(self): '''Return the properties used to animate. ''' return self._animated_properties @staticmethod def stop_all(widget, *largs): '''Stop all animations that concern a specific widget / list of properties. Example:: anim = Animation(x=50) anim.start(widget) # and later Animation.stop_all(widget, 'x') ''' if len(largs): for animation in list(Animation._instances): for x in largs: animation.stop_property(widget, x) else: for animation in set(Animation._instances): animation.stop(widget) @staticmethod def cancel_all(widget, *largs): '''Cancel all animations that concern a specific widget / list of properties. See :attr:`cancel`. Example:: anim = Animation(x=50) anim.start(widget) # and later Animation.cancel_all(widget, 'x') .. versionadded:: 1.4.0 ''' if len(largs): for animation in list(Animation._instances): for x in largs: animation.cancel_property(widget, x) else: for animation in set(Animation._instances): animation.cancel(widget) def start(self, widget): '''Start the animation on a widget. ''' self.stop(widget) self._initialize(widget) self._register() self.dispatch('on_start', widget) def stop(self, widget): '''Stop the animation previously applied to a widget, triggering the `on_complete` event.''' props = self._widgets.pop(widget.uid, None) if props: self.dispatch('on_complete', widget) self.cancel(widget) def cancel(self, widget): '''Cancel the animation previously applied to a widget. Same effect as :attr:`stop`, except the `on_complete` event will *not* be triggered! .. versionadded:: 1.4.0 ''' self._widgets.pop(widget.uid, None) self._clock_uninstall() if not self._widgets: self._unregister() def stop_property(self, widget, prop): '''Even if an animation is running, remove a property. It will not be animated futher. If it was the only/last property being animated, the animation will be stopped (see :attr:`stop`). ''' props = self._widgets.get(widget.uid, None) if not props: return props['properties'].pop(prop, None) # no more properties to animation ? kill the animation. if not props['properties']: self.stop(widget) def cancel_property(self, widget, prop): '''Even if an animation is running, remove a property. It will not be animated further. If it was the only/last property being animated, the animation will be canceled (see :attr:`cancel`) .. versionadded:: 1.4.0 ''' props = self._widgets.get(widget.uid, None) if not props: return props['properties'].pop(prop, None) # no more properties to animation ? kill the animation. if not props['properties']: self.cancel(widget) def have_properties_to_animate(self, widget): '''Return True if a widget still has properties to animate. .. versionadded:: 1.8.0 ''' props = self._widgets.get(widget.uid, None) if props and props['properties']: return True # # Private # def _register(self): Animation._instances.add(self) def _unregister(self): if self in Animation._instances: Animation._instances.remove(self) def _initialize(self, widget): d = self._widgets[widget.uid] = { 'widget': widget, 'properties': {}, 'time': None} # get current values p = d['properties'] for key, value in self._animated_properties.items(): original_value = getattr(widget, key) if isinstance(original_value, (tuple, list)): original_value = original_value[:] elif isinstance(original_value, dict): original_value = original_value.copy() p[key] = (original_value, value) # install clock self._clock_install() def _clock_install(self): if self._clock_installed: return Clock.schedule_interval(self._update, self._step) self._clock_installed = True def _clock_uninstall(self): if self._widgets or not self._clock_installed: return self._clock_installed = False Clock.unschedule(self._update) def _update(self, dt): widgets = self._widgets transition = self._transition calculate = self._calculate for uid in list(widgets.keys())[:]: anim = widgets[uid] widget = anim['widget'] if isinstance(widget, WeakProxy) and not len(dir(widget)): # empty proxy, widget is gone. ref: #2458 del widgets[uid] continue if anim['time'] is None: anim['time'] = 0. else: anim['time'] += dt # calculate progression if self._duration: progress = min(1., anim['time'] / self._duration) else: progress = 1 t = transition(progress) # apply progression on widget for key, values in anim['properties'].items(): a, b = values value = calculate(a, b, t) setattr(widget, key, value) self.dispatch('on_progress', widget, progress) # time to stop ? if progress >= 1.: self.stop(widget) def _calculate(self, a, b, t): _calculate = self._calculate if isinstance(a, list) or isinstance(a, tuple): if isinstance(a, list): tp = list else: tp = tuple return tp([_calculate(a[x], b[x], t) for x in range(len(a))]) elif isinstance(a, dict): d = {} for x in iterkeys(a): if x not in b: # User requested to animate only part of the dict. # Copy the rest d[x] = a[x] else: d[x] = _calculate(a[x], b[x], t) return d else: return (a * (1. - t)) + (b * t) # # Default handlers # def on_start(self, widget): pass def on_progress(self, widget, progress): pass def on_complete(self, widget): pass def __add__(self, animation): return Sequence(self, animation) def __and__(self, animation): return Parallel(self, animation) class Sequence(Animation): def __init__(self, anim1, anim2): super(Sequence, self).__init__() #: Repeat the sequence. See 'Repeating animation' in the header #: documentation. self.repeat = False self.anim1 = anim1 self.anim2 = anim2 self.anim1.bind(on_start=self.on_anim1_start, on_progress=self.on_anim1_progress) self.anim2.bind(on_complete=self.on_anim2_complete, on_progress=self.on_anim2_progress) @property def duration(self): return self.anim1.duration + self.anim2.duration def start(self, widget): self.stop(widget) self._widgets[widget.uid] = True self._register() self.anim1.start(widget) self.anim1.bind(on_complete=self.on_anim1_complete) def stop(self, widget): self.anim1.stop(widget) self.anim2.stop(widget) props = self._widgets.pop(widget.uid, None) if props: self.dispatch('on_complete', widget) super(Sequence, self).cancel(widget) def stop_property(self, widget, prop): self.anim1.stop_property(widget, prop) self.anim2.stop_property(widget, prop) if (not self.anim1.have_properties_to_animate(widget) and not self.anim2.have_properties_to_animate(widget)): self.stop(widget) def cancel(self, widget): self.anim1.cancel(widget) self.anim2.cancel(widget) super(Sequence, self).cancel(widget) def on_anim1_start(self, instance, widget): self.dispatch('on_start', widget) def on_anim1_complete(self, instance, widget): self.anim1.unbind(on_complete=self.on_anim1_complete) self.anim2.start(widget) def on_anim1_progress(self, instance, widget, progress): self.dispatch('on_progress', widget, progress / 2.) def on_anim2_complete(self, instance, widget): '''Repeating logic used with boolean variable "repeat". .. versionadded:: 1.7.1 ''' if self.repeat: self.anim1.start(widget) self.anim1.bind(on_complete=self.on_anim1_complete) else: self.dispatch('on_complete', widget) def on_anim2_progress(self, instance, widget, progress): self.dispatch('on_progress', widget, .5 + progress / 2.) class Parallel(Animation): def __init__(self, anim1, anim2): super(Parallel, self).__init__() self.anim1 = anim1 self.anim2 = anim2 self.anim1.bind(on_complete=self.on_anim_complete) self.anim2.bind(on_complete=self.on_anim_complete) @property def duration(self): return max(self.anim1.duration, self.anim2.duration) def start(self, widget): self.stop(widget) self.anim1.start(widget) self.anim2.start(widget) self._widgets[widget.uid] = {'complete': 0} self._register() self.dispatch('on_start', widget) def stop(self, widget): self.anim1.stop(widget) self.anim2.stop(widget) props = self._widgets.pop(widget.uid, None) if props: self.dispatch('on_complete', widget) super(Parallel, self).cancel(widget) def stop_property(self, widget, prop): self.anim1.stop_property(widget, prop) self.anim2.stop_property(widget, prop) if (not self.anim1.have_properties_to_animate(widget) and not self.anim2.have_properties_to_animate(widget)): self.stop(widget) def cancel(self, widget): self.anim1.cancel(widget) self.anim2.cancel(widget) super(Parallel, self).cancel(widget) def on_anim_complete(self, instance, widget): self._widgets[widget.uid]['complete'] += 1 if self._widgets[widget.uid]['complete'] == 2: self.stop(widget) class AnimationTransition(object): '''Collection of animation functions to be used with the Animation object. Easing Functions ported to Kivy from the Clutter Project http://www.clutter-project.org/docs/clutter/stable/ClutterAlpha.html The `progress` parameter in each animation function is in the range 0-1. ''' @staticmethod def linear(progress): '''.. image:: images/anim_linear.png''' return progress @staticmethod def in_quad(progress): '''.. image:: images/anim_in_quad.png ''' return progress * progress @staticmethod def out_quad(progress): '''.. image:: images/anim_out_quad.png ''' return -1.0 * progress * (progress - 2.0) @staticmethod def in_out_quad(progress): '''.. image:: images/anim_in_out_quad.png ''' p = progress * 2 if p < 1: return 0.5 * p * p p -= 1.0 return -0.5 * (p * (p - 2.0) - 1.0) @staticmethod def in_cubic(progress): '''.. image:: images/anim_in_cubic.png ''' return progress * progress * progress @staticmethod def out_cubic(progress): '''.. image:: images/anim_out_cubic.png ''' p = progress - 1.0 return p * p * p + 1.0 @staticmethod def in_out_cubic(progress): '''.. image:: images/anim_in_out_cubic.png ''' p = progress * 2 if p < 1: return 0.5 * p * p * p p -= 2 return 0.5 * (p * p * p + 2.0) @staticmethod def in_quart(progress): '''.. image:: images/anim_in_quart.png ''' return progress * progress * progress * progress @staticmethod def out_quart(progress): '''.. image:: images/anim_out_quart.png ''' p = progress - 1.0 return -1.0 * (p * p * p * p - 1.0) @staticmethod def in_out_quart(progress): '''.. image:: images/anim_in_out_quart.png ''' p = progress * 2 if p < 1: return 0.5 * p * p * p * p p -= 2 return -0.5 * (p * p * p * p - 2.0) @staticmethod def in_quint(progress): '''.. image:: images/anim_in_quint.png ''' return progress * progress * progress * progress * progress @staticmethod def out_quint(progress): '''.. image:: images/anim_out_quint.png ''' p = progress - 1.0 return p * p * p * p * p + 1.0 @staticmethod def in_out_quint(progress): '''.. image:: images/anim_in_out_quint.png ''' p = progress * 2 if p < 1: return 0.5 * p * p * p * p * p p -= 2.0 return 0.5 * (p * p * p * p * p + 2.0) @staticmethod def in_sine(progress): '''.. image:: images/anim_in_sine.png ''' return -1.0 * cos(progress * (pi / 2.0)) + 1.0 @staticmethod def out_sine(progress): '''.. image:: images/anim_out_sine.png ''' return sin(progress * (pi / 2.0)) @staticmethod def in_out_sine(progress): '''.. image:: images/anim_in_out_sine.png ''' return -0.5 * (cos(pi * progress) - 1.0) @staticmethod def in_expo(progress): '''.. image:: images/anim_in_expo.png ''' if progress == 0: return 0.0 return pow(2, 10 * (progress - 1.0)) @staticmethod def out_expo(progress): '''.. image:: images/anim_out_expo.png ''' if progress == 1.0: return 1.0 return -pow(2, -10 * progress) + 1.0 @staticmethod def in_out_expo(progress): '''.. image:: images/anim_in_out_expo.png ''' if progress == 0: return 0.0 if progress == 1.: return 1.0 p = progress * 2 if p < 1: return 0.5 * pow(2, 10 * (p - 1.0)) p -= 1.0 return 0.5 * (-pow(2, -10 * p) + 2.0) @staticmethod def in_circ(progress): '''.. image:: images/anim_in_circ.png ''' return -1.0 * (sqrt(1.0 - progress * progress) - 1.0) @staticmethod def out_circ(progress): '''.. image:: images/anim_out_circ.png ''' p = progress - 1.0 return sqrt(1.0 - p * p) @staticmethod def in_out_circ(progress): '''.. image:: images/anim_in_out_circ.png ''' p = progress * 2 if p < 1: return -0.5 * (sqrt(1.0 - p * p) - 1.0) p -= 2.0 return 0.5 * (sqrt(1.0 - p * p) + 1.0) @staticmethod def in_elastic(progress): '''.. image:: images/anim_in_elastic.png ''' p = .3 s = p / 4.0 q = progress if q == 1: return 1.0 q -= 1.0 return -(pow(2, 10 * q) * sin((q - s) * (2 * pi) / p)) @staticmethod def out_elastic(progress): '''.. image:: images/anim_out_elastic.png ''' p = .3 s = p / 4.0 q = progress if q == 1: return 1.0 return pow(2, -10 * q) * sin((q - s) * (2 * pi) / p) + 1.0 @staticmethod def in_out_elastic(progress): '''.. image:: images/anim_in_out_elastic.png ''' p = .3 * 1.5 s = p / 4.0 q = progress * 2 if q == 2: return 1.0 if q < 1: q -= 1.0 return -.5 * (pow(2, 10 * q) * sin((q - s) * (2.0 * pi) / p)) else: q -= 1.0 return pow(2, -10 * q) * sin((q - s) * (2.0 * pi) / p) * .5 + 1.0 @staticmethod def in_back(progress): '''.. image:: images/anim_in_back.png ''' return progress * progress * ((1.70158 + 1.0) * progress - 1.70158) @staticmethod def out_back(progress): '''.. image:: images/anim_out_back.png ''' p = progress - 1.0 return p * p * ((1.70158 + 1) * p + 1.70158) + 1.0 @staticmethod def in_out_back(progress): '''.. image:: images/anim_in_out_back.png ''' p = progress * 2. s = 1.70158 * 1.525 if p < 1: return 0.5 * (p * p * ((s + 1.0) * p - s)) p -= 2.0 return 0.5 * (p * p * ((s + 1.0) * p + s) + 2.0) @staticmethod def _out_bounce_internal(t, d): p = t / d if p < (1.0 / 2.75): return 7.5625 * p * p elif p < (2.0 / 2.75): p -= (1.5 / 2.75) return 7.5625 * p * p + .75 elif p < (2.5 / 2.75): p -= (2.25 / 2.75) return 7.5625 * p * p + .9375 else: p -= (2.625 / 2.75) return 7.5625 * p * p + .984375 @staticmethod def _in_bounce_internal(t, d): return 1.0 - AnimationTransition._out_bounce_internal(d - t, d) @staticmethod def in_bounce(progress): '''.. image:: images/anim_in_bounce.png ''' return AnimationTransition._in_bounce_internal(progress, 1.) @staticmethod def out_bounce(progress): '''.. image:: images/anim_out_bounce.png ''' return AnimationTransition._out_bounce_internal(progress, 1.) @staticmethod def in_out_bounce(progress): '''.. image:: images/anim_in_out_bounce.png ''' p = progress * 2. if p < 1.: return AnimationTransition._in_bounce_internal(p, 1.) * .5 return AnimationTransition._out_bounce_internal(p - 1., 1.) * .5 + .5 ================================================ FILE: tickeys/kivy/app.py ================================================ ''' Application =========== The :class:`App` class is the base for creating Kivy applications. Think of it as your main entry point into the Kivy run loop. In most cases, you subclass this class and make your own app. You create an instance of your specific app class and then, when you are ready to start the application's life cycle, you call your instance's :meth:`App.run` method. Creating an Application ----------------------- Method using build() override ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To initialize your app with a widget tree, override the :meth:`~App.build` method in your app class and return the widget tree you constructed. Here's an example of a very simple application that just shows a button: .. include:: ../../examples/application/app_with_build.py :literal: The file is also available in the examples folder at :file:`kivy/examples/application/app_with_build.py`. Here, no widget tree was constructed (or if you will, a tree with only the root node). Method using kv file ~~~~~~~~~~~~~~~~~~~~ You can also use the :doc:`api-kivy.lang` for creating applications. The .kv can contain rules and root widget definitions at the same time. Here is the same example as the Button one in a kv file. Contents of 'test.kv': .. include:: ../../examples/application/test.kv :literal: Contents of 'main.py': .. include:: ../../examples/application/app_with_kv.py :literal: See :file:`kivy/examples/application/app_with_kv.py`. The relation between main.py and test.kv is explained in :meth:`App.load_kv`. Application configuration ------------------------- .. versionadded:: 1.0.7 Use the configuration file ~~~~~~~~~~~~~~~~~~~~~~~~~~ Your application might want to have its own configuration file. The :class:`App` is able to handle an INI file automatically. You add your section/key/value in the :meth:`App.build_config` method by using the `config` parameter (which is an instance of :class:`~kivy.config.ConfigParser`):: class TestApp(App): def build_config(self, config): config.setdefaults('section1', { 'key1': 'value1', 'key2': '42' }) As soon as you add one section in the config, a file is created on the disk and named from the mangled name of your class. "TestApp" will give a config file-name "test.ini" with the content:: [section1] key1 = value1 key2 = 42 The "test.ini" will be automatically loaded at runtime and you can access the configuration in your :meth:`App.build` method:: class TestApp(App): def build_config(self, config): config.setdefaults('section1', { 'key1': 'value1', 'key2': '42' }) def build(self): config = self.config return Label(text='key1 is %s and key2 is %d' % ( config.get('section1', 'key1'), config.getint('section1', 'key2'))) Create a settings panel ~~~~~~~~~~~~~~~~~~~~~~~ Your application can have a settings panel to let your user configure some of your config tokens. Here is an example done in the KinectViewer example (available in the examples directory): .. image:: images/app-settings.jpg :align: center You can add your own panels of settings by extending the :meth:`App.build_settings` method. Check the :class:`~kivy.uix.settings.Settings` about how to create a panel, because you need a JSON file / data first. Let's take as an example the previous snippet of TestApp with custom config. We could create a JSON like this:: [ { "type": "title", "title": "Test application" }, { "type": "options", "title": "My first key", "desc": "Description of my first key", "section": "section1", "key": "key1", "options": ["value1", "value2", "another value"] }, { "type": "numeric", "title": "My second key", "desc": "Description of my second key", "section": "section1", "key": "key2" } ] Then, we can create a panel using this JSON to automatically create all the options and link them to our :attr:`App.config` ConfigParser instance:: class TestApp(App): # ... def build_settings(self, settings): jsondata = """... put the json data here ...""" settings.add_json_panel('Test application', self.config, data=jsondata) That's all! Now you can press F1 (default keystroke) to toggle the settings panel or press the "settings" key on your android device. You can manually call :meth:`App.open_settings` and :meth:`App.close_settings` if you want to handle this manually. Every change in the panel is automatically saved in the config file. You can also use :meth:`App.build_settings` to modify properties of the settings panel. For instance, the default panel has a sidebar for switching between json panels whose width defaults to 200dp. If you'd prefer this to be narrower, you could add:: settings.interface.menu.width = dp(100) to your :meth:`build_settings` method. You might want to know when a config value has been changed by the user in order to adapt or reload your UI. You can then overload the :meth:`on_config_change` method:: class TestApp(App): # ... def on_config_change(self, config, section, key, value): if config is self.config: token = (section, key) if token == ('section1', 'key1'): print('Our key1 have been changed to', value) elif token == ('section1', 'key2'): print('Our key2 have been changed to', value) The Kivy configuration panel is added by default to the settings instance. If you don't want this panel, you can declare your Application as follows:: class TestApp(App): use_kivy_settings = False # ... This only removes the Kivy panel but does not stop the settings instance from appearing. If you want to prevent the settings instance from appearing altogether, you can do this:: class TestApp(App): def open_settings(self, *largs): pass Profiling with on_start and on_stop ----------------------------------- It is often useful to profile python code in order to discover locations to optimise. The standard library profilers (http://docs.python.org/2/library/profile.html) provides multiple options for profiling code. For profiling the entire program, the natural approaches of using profile as a module or profile's run method does not work with Kivy. It is however, possible to use :meth:`App.on_start` and :meth:`App.on_stop` methods:: import cProfile class MyApp(App): def on_start(self): self.profile = cProfile.Profile() self.profile.enable() def on_stop(self): self.profile.disable() self.profile.dump_stats('myapp.profile') This will create a file called `myapp.profile` when you exit your app. Customising layout ------------------ You can choose different settings widget layouts by setting :attr:`App.settings_cls`. By default, this is a :class:`~kivy.uix.settings.Settings` class which provides the pictured sidebar layout, but you could set it to any of the other layouts provided in :mod:`kivy.uix.settings` or create your own. See the module documentation for :mod:`kivy.uix.settings` for more information. You can customise how the settings panel is displayed by overriding :meth:`App.display_settings` which is called before displaying the settings panel on the screen. By default, it simply draws the panel on top of the window, but you could modify it to (for instance) show the settings in a :class:`~kivy.uix.popup.Popup` or add it to your app's :class:`~kivy.uix.screenmanager.ScreenManager` if you are using one. If you do so, you should also modify :meth:`App.close_settings` to exit the panel appropriately. For instance, to have the settings panel appear in a popup you can do:: def display_settings(self, settings): try: p = self.settings_popup except AttributeError: self.settings_popup = Popup(content=settings, title='Settings', size_hint=(0.8, 0.8)) p = self.settings_popup if p.content is not settings: p.content = settings p.open() def close_settings(self, *args): try: p = self.settings_popup p.dismiss() except AttributeError: pass # Settings popup doesn't exist Finally, if you want to replace the current settings panel widget, you can remove the internal references to it using :meth:`App.destroy_settings`. If you have modified :meth:`App.display_settings`, you should be careful to detect if the settings panel has been replaced. Pause mode ---------- .. versionadded:: 1.1.0 On tablets and phones, the user can switch at any moment to another application. By default, your application will close and the :meth:`App.on_stop` event will be fired. If you support Pause mode, when switching to another application, your application will wait indefinitely until the user switches back to your application. There is an issue with OpenGL on Android devices: it is not guaranteed that the OpenGL ES Context will be restored when your app resumes. The mechanism for restoring all the OpenGL data is not yet implemented in Kivy. The currently implemented Pause mechanism is: #. Kivy checks every frame if Pause mode is activated by the Operating System due to the user switching to another application, a phone shutdown or any other reason. #. :meth:`App.on_pause` is called: #. If False is returned (default case), then :meth:`App.on_stop` is called. #. Otherwise the application will sleep until the OS resumes our App #. When the app is resumed, :meth:`App.on_resume` is called. #. If our app memory has been reclaimed by the OS, then nothing will be called. Here is a simple example of how on_pause() should be used:: class TestApp(App): def on_pause(self): # Here you can save data if needed return True def on_resume(self): # Here you can check if any data needs replacing (usually nothing) pass .. warning:: Both `on_pause` and `on_stop` must save important data because after `on_pause` is called, `on_resume` may not be called at all. ''' __all__ = ('App', ) import os from inspect import getfile from os.path import dirname, join, exists, sep, expanduser, isfile from kivy.config import ConfigParser from kivy.base import runTouchApp, stopTouchApp from kivy.compat import string_types from kivy.factory import Factory from kivy.logger import Logger from kivy.event import EventDispatcher from kivy.lang import Builder from kivy.resources import resource_find from kivy.utils import platform as core_platform from kivy.uix.widget import Widget from kivy.properties import ObjectProperty, StringProperty platform = core_platform class App(EventDispatcher): ''' Application class, see module documentation for more information. :Events: `on_start`: Fired when the application is being started (before the :func:`~kivy.base.runTouchApp` call. `on_stop`: Fired when the application stops. `on_pause`: Fired when the application is paused by the OS. `on_resume`: Fired when the application is resumed from pause by the OS. Beware: you have no guarantee that this event will be fired after the `on_pause` event has been called. .. versionchanged:: 1.7.0 Parameter `kv_file` added. .. versionchanged:: 1.8.0 Parameters `kv_file` and `kv_directory` are now properties of App. ''' title = StringProperty(None) ''' Title of your application. You can set this as follows:: class MyApp(App): def build(self): self.title = 'Hello world' .. versionadded:: 1.0.5 .. versionchanged:: 1.8.0 `title` is now a :class:`~kivy.properties.StringProperty`. Don't set the title in the class as previously stated in the documentation. .. note:: For Kivy < 1.8.0, you can set this as follows:: class MyApp(App): title = 'Custom title' If you want to dynamically change the title, you can do:: from kivy.base import EventLoop EventLoop.window.title = 'New title' ''' icon = StringProperty(None) '''Icon of your application. The icon can be located in the same directory as your main file. You can set this as follows:: class MyApp(App): def build(self): self.icon = 'myicon.png' .. versionadded:: 1.0.5 .. versionchanged:: 1.8.0 `icon` is now a :class:`~kivy.properties.StringProperty`. Don't set the icon in the class as previously stated in the documentation. .. note:: For Kivy prior to 1.8.0, you need to set this as follows:: class MyApp(App): icon = 'customicon.png' Recommended 256x256 or 1024x1024? for GNU/Linux and Mac OSX 32x32 for Windows7 or less. <= 256x256 for windows 8 256x256 does work (on Windows 8 at least), but is scaled down and doesn't look as good as a 32x32 icon. ''' use_kivy_settings = True '''.. versionadded:: 1.0.7 If True, the application settings will also include the Kivy settings. If you don't want the user to change any kivy settings from your settings UI, change this to False. ''' settings_cls = ObjectProperty(None) '''.. versionadded:: 1.8.0 The class used to construct the settings panel and the instance passed to :meth:`build_config`. You should use either :class:`~kivy.uix.settings.Settings` or one of the provided subclasses with different layouts (:class:`~kivy.uix.settings.SettingsWithSidebar`, :class:`~kivy.uix.settings.SettingsWithSpinner`, :class:`~kivy.uix.settings.SettingsWithTabbedPanel`, :class:`~kivy.uix.settings.SettingsWithNoMenu`). You can also create your own Settings subclass. See the documentation of :mod:`~kivy.uix.settings.Settings` for more information. :attr:`~App.settings_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`~kivy.uix.settings.SettingsWithSpinner` which displays settings panels with a spinner to switch between them. If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' kv_directory = StringProperty(None) '''Path of the directory where application kv is stored, defaults to None .. versionadded:: 1.8.0 If a kv_directory is set, it will be used to get the initial kv file. By default, the file is assumed to be in the same directory as the current App definition file. ''' kv_file = StringProperty(None) '''Filename of the Kv file to load, defaults to None. .. versionadded:: 1.8.0 If a kv_file is set, it will be loaded when the application starts. The loading of the "default" kv file will be prevented. ''' # Return the current running App instance _running_app = None __events__ = ('on_start', 'on_stop', 'on_pause', 'on_resume') def __init__(self, **kwargs): App._running_app = self self._app_directory = None self._app_name = None self._app_settings = None self._app_window = None super(App, self).__init__(**kwargs) self.built = False #: Options passed to the __init__ of the App self.options = kwargs #: Returns an instance of the :class:`~kivy.config.ConfigParser` for #: the application configuration. You can use this to query some config #: tokens in the :meth:`build` method. self.config = None #: The *root* widget returned by the :meth:`build` method or by the #: :meth:`load_kv` method if the kv file contains a root widget. self.root = None def build(self): '''Initializes the application; it will be called only once. If this method returns a widget (tree), it will be used as the root widget and added to the window. :return: None or a root :class:`~kivy.uix.widget.Widget` instance if no self.root exists.''' if not self.root: return Widget() def build_config(self, config): '''.. versionadded:: 1.0.7 This method is called before the application is initialized to construct your :class:`~kivy.config.ConfigParser` object. This is where you can put any default section / key / value for your config. If anything is set, the configuration will be automatically saved in the file returned by :meth:`get_application_config`. :Parameters: `config`: :class:`~kivy.config.ConfigParser` Use this to add default section / key / value items ''' def build_settings(self, settings): '''.. versionadded:: 1.0.7 This method is called when the user (or you) want to show the application settings. It is called once when the settings panel is first opened, after which the panel is cached. It may be called again if the cached settings panel is removed by :meth:`destroy_settings`. You can use this method to add settings panels and to customise the settings widget e.g. by changing the sidebar width. See the module documentation for full details. :Parameters: `settings`: :class:`~kivy.uix.settings.Settings` Settings instance for adding panels ''' def load_kv(self, filename=None): '''This method is invoked the first time the app is being run if no widget tree has been constructed before for this app. This method then looks for a matching kv file in the same directory as the file that contains the application class. For example, say you have a file named main.py that contains:: class ShowcaseApp(App): pass This method will search for a file named `showcase.kv` in the directory that contains main.py. The name of the kv file has to be the lowercase name of the class, without the 'App' postfix at the end if it exists. You can define rules and a root widget in your kv file:: : # this is a rule ... ClassName: # this is a root widget ... There must be only one root widget. See the :doc:`api-kivy.lang` documentation for more information on how to create kv files. If your kv file contains a root widget, it will be used as self.root, the root widget for the application. .. note:: This function is called from :meth:`run`, therefore, any widget whose styling is defined in this kv file and is created before :meth:`run` is called (e.g. in `__init__`), won't have its styling applied. Note that :meth:`build` is called after :attr:`load_kv` has been called. ''' # Detect filename automatically if it was not specified. if filename: filename = resource_find(filename) else: try: default_kv_directory = dirname(getfile(self.__class__)) if default_kv_directory == '': default_kv_directory = '.' except TypeError: # if it's a builtin module.. use the current dir. default_kv_directory = '.' kv_directory = self.kv_directory or default_kv_directory clsname = self.__class__.__name__.lower() if (clsname.endswith('app') and not isfile(join(kv_directory, '%s.kv' % clsname))): clsname = clsname[:-3] filename = join(kv_directory, '%s.kv' % clsname) # Load KV file Logger.debug('App: Loading kv <{0}>'.format(filename)) rfilename = resource_find(filename) if rfilename is None or not exists(rfilename): Logger.debug('App: kv <%s> not found' % filename) return False root = Builder.load_file(rfilename) if root: self.root = root return True def get_application_name(self): '''Return the name of the application. ''' if self.title is not None: return self.title clsname = self.__class__.__name__ if clsname.endswith('App'): clsname = clsname[:-3] return clsname def get_application_icon(self): '''Return the icon of the application. ''' if not resource_find(self.icon): return '' else: return resource_find(self.icon) def get_application_config(self, defaultpath='%(appdir)s/%(appname)s.ini'): '''.. versionadded:: 1.0.7 .. versionchanged:: 1.4.0 Customized the default path for iOS and Android platforms. Added a defaultpath parameter for desktop OS's (not applicable to iOS and Android.) Return the filename of your application configuration. Depending on the platform, the application file will be stored in different locations: - on iOS: /Documents/..ini - on Android: /sdcard/..ini - otherwise: /.ini When you are distributing your application on Desktops, please note that if the application is meant to be installed system-wide, the user might not have write-access to the application directory. If you want to store user settings, you should overload this method and change the default behavior to save the configuration file in the user directory.:: class TestApp(App): def get_application_config(self): return super(TestApp, self).get_application_config( '~/.%(appname)s.ini') Some notes: - The tilda '~' will be expanded to the user directory. - %(appdir)s will be replaced with the application :attr:`directory` - %(appname)s will be replaced with the application :attr:`name` ''' if platform == 'android': defaultpath = '/sdcard/.%(appname)s.ini' elif platform == 'ios': defaultpath = '~/Documents/%(appname)s.ini' elif platform == 'win': defaultpath = defaultpath.replace('/', sep) return expanduser(defaultpath) % { 'appname': self.name, 'appdir': self.directory} @property def root_window(self): '''.. versionadded:: 1.9.0 Returns the root window instance used by :meth:`run`. ''' return self._app_window def load_config(self): '''(internal) This function is used for returning a ConfigParser with the application configuration. It's doing 3 things: #. Creating an instance of a ConfigParser #. Loading the default configuration by calling :meth:`build_config`, then #. If it exists, it loads the application configuration file, otherwise it creates one. :return: :class:`~kivy.config.ConfigParser` instance ''' try: config = ConfigParser.get_configparser('app') except KeyError: config = None if config is None: config = ConfigParser(name='app') self.config = config self.build_config(config) # if no sections are created, that's mean the user don't have # configuration. if len(config.sections()) == 0: return # ok, the user have some sections, read the default file if exist # or write it ! filename = self.get_application_config() if filename is None: return config Logger.debug('App: Loading configuration <{0}>'.format(filename)) if exists(filename): try: config.read(filename) except: Logger.error('App: Corrupted config file, ignored.') config.name = '' try: config = ConfigParser.get_configparser('app') except KeyError: config = None if config is None: config = ConfigParser(name='app') self.config = config self.build_config(config) pass else: Logger.debug('App: First configuration, create <{0}>'.format( filename)) config.filename = filename config.write() return config @property def directory(self): '''.. versionadded:: 1.0.7 Return the directory where the application lives. ''' if self._app_directory is None: try: self._app_directory = dirname(getfile(self.__class__)) if self._app_directory == '': self._app_directory = '.' except TypeError: # if it's a builtin module.. use the current dir. self._app_directory = '.' return self._app_directory @property def user_data_dir(self): ''' .. versionadded:: 1.7.0 Returns the path to the directory in the users file system which the application can use to store additional data. Different platforms have different conventions with regards to where the user can store data such as preferences, saved games and settings. This function implements these conventions. The directory is created when the property is called, unless it already exists. On iOS, `~/Documents` is returned (which is inside the app's sandbox). On Android, `/sdcard/` is returned. On Windows, `%APPDATA%/` is returned. On Mac OSX, `~/Library/Application Support/` is returned. On Linux, `$XDG_CONFIG_HOME/` is returned. ''' data_dir = "" if platform == 'ios': data_dir = join('~/Documents', self.name) elif platform == 'android': data_dir = join('/sdcard', self.name) elif platform == 'win': data_dir = os.path.join(os.environ['APPDATA'], self.name) elif platform == 'macosx': data_dir = '~/Library/Application Support/{}'.format(self.name) else: # _platform == 'linux' or anything else...: data_dir = os.environ.get('XDG_CONFIG_HOME', '~/.config') data_dir = join(data_dir, self.name) data_dir = expanduser(data_dir) if not exists(data_dir): os.mkdir(data_dir) return data_dir @property def name(self): '''.. versionadded:: 1.0.7 Return the name of the application based on the class name. ''' if self._app_name is None: clsname = self.__class__.__name__ if clsname.endswith('App'): clsname = clsname[:-3] self._app_name = clsname.lower() return self._app_name def run(self): '''Launches the app in standalone mode. ''' if not self.built: self.load_config() self.load_kv(filename=self.kv_file) root = self.build() if root: self.root = root if self.root: if not isinstance(self.root, Widget): Logger.critical('App.root must be an _instance_ of Widget') raise Exception('Invalid instance in App.root') from kivy.core.window import Window Window.add_widget(self.root) # Check if the window is already created from kivy.base import EventLoop window = EventLoop.window if window: self._app_window = window window.set_title(self.get_application_name()) icon = self.get_application_icon() if icon: window.set_icon(icon) self._install_settings_keys(window) else: Logger.critical("Application: No window is created." " Terminating application run.") return self.dispatch('on_start') runTouchApp() self.stop() def stop(self, *largs): '''Stop the application. If you use this method, the whole application will stop by issuing a call to :func:`~kivy.base.stopTouchApp`. ''' self.dispatch('on_stop') stopTouchApp() # Clear the window children for child in self._app_window.children: self._app_window.remove_widget(child) def on_start(self): '''Event handler for the `on_start` event which is fired after initialization (after build() has been called) but before the application has started running. ''' pass def on_stop(self): '''Event handler for the `on_stop` event which is fired when the application has finished running (i.e. the window is about to be closed). ''' pass def on_pause(self): '''Event handler called when Pause mode is requested. You should return True if your app can go into Pause mode, otherwise return False and your application will be stopped (the default). You cannot control when the application is going to go into this mode. It's determined by the Operating System and mostly used for mobile devices (android/ios) and for resizing. The default return value is False. .. versionadded:: 1.1.0 ''' return False def on_resume(self): '''Event handler called when your application is resuming from the Pause mode. .. versionadded:: 1.1.0 .. warning:: When resuming, the OpenGL Context might have been damaged / freed. This is where you can reconstruct some of your OpenGL state e.g. FBO content. ''' pass @staticmethod def get_running_app(): '''Return the currently running application instance. .. versionadded:: 1.1.0 ''' return App._running_app def on_config_change(self, config, section, key, value): '''Event handler fired when a configuration token has been changed by the settings page. ''' pass def open_settings(self, *largs): '''Open the application settings panel. It will be created the very first time, or recreated if the previously cached panel has been removed by :meth:`destroy_settings`. The settings panel will be displayed with the :meth:`display_settings` method, which by default adds the settings panel to the Window attached to your application. You should override that method if you want to display the settings panel differently. :return: True if the settings has been opened. ''' if self._app_settings is None: self._app_settings = self.create_settings() displayed = self.display_settings(self._app_settings) if displayed: return True return False def display_settings(self, settings): '''.. versionadded:: 1.8.0 Display the settings panel. By default, the panel is drawn directly on top of the window. You can define other behaviour by overriding this method, such as adding it to a ScreenManager or Popup. You should return True if the display is successful, otherwise False. :Parameters: `settings`: :class:`~kivy.uix.settings.Settings` You can modify this object in order to modify the settings display. ''' win = self._app_window if not win: raise Exception('No windows are set on the application, you cannot' ' open settings yet.') if settings not in win.children: win.add_widget(settings) return True return False def close_settings(self, *largs): '''Close the previously opened settings panel. :return: True if the settings has been closed. ''' win = self._app_window settings = self._app_settings if win is None or settings is None: return if settings in win.children: win.remove_widget(settings) return True return False def create_settings(self): '''Create the settings panel. This method will normally be called only one time per application life-time and the result is cached internally, but it may be called again if the cached panel is removed by :meth:`destroy_settings`. By default, it will build a settings panel according to :attr:`settings_cls`, call :meth:`build_settings`, add a Kivy panel if :attr:`use_kivy_settings` is True, and bind to on_close/on_config_change. If you want to plug your own way of doing settings, without the Kivy panel or close/config change events, this is the method you want to overload. .. versionadded:: 1.8.0 ''' if self.settings_cls is None: from kivy.uix.settings import SettingsWithSpinner self.settings_cls = SettingsWithSpinner elif isinstance(self.settings_cls, string_types): self.settings_cls = Factory.get(self.settings_cls) s = self.settings_cls() self.build_settings(s) if self.use_kivy_settings: s.add_kivy_panel() s.bind(on_close=self.close_settings, on_config_change=self._on_config_change) return s def destroy_settings(self): '''.. versionadded:: 1.8.0 Dereferences the current settings panel if one exists. This means that when :meth:`App.open_settings` is next run, a new panel will be created and displayed. It doesn't affect any of the contents of the panel, but lets you (for instance) refresh the settings panel layout if you have changed the settings widget in response to a screen size change. If you have modified :meth:`~App.open_settings` or :meth:`~App.display_settings`, you should be careful to correctly detect if the previous settings widget has been destroyed. ''' if self._app_settings is not None: self._app_settings = None # # privates # def _on_config_change(self, *largs): self.on_config_change(*largs[1:]) def _install_settings_keys(self, window): window.bind(on_keyboard=self._on_keyboard_settings) def _on_keyboard_settings(self, window, *largs): key = largs[0] setting_key = 282 # F1 # android hack, if settings key is pygame K_MENU if platform == 'android': import pygame setting_key = pygame.K_MENU if key == setting_key: # toggle settings panel if not self.open_settings(): self.close_settings() return True if key == 27: return self.close_settings() def on_title(self, instance, title): if self._app_window: self._app_window.set_title(title) def on_icon(self, instance, icon): if self._app_window: self._app_window.set_icon(self.get_application_icon()) ================================================ FILE: tickeys/kivy/atlas.py ================================================ ''' Atlas ===== .. versionadded:: 1.1.0 Atlas manages texture atlases: packing multiple textures into one. With it, you reduce the number of images loaded and speedup the application loading. This module contains both the Atlas class and command line processing for creating an atlas from a set of individual PNG files. The command line section requires the Pillow library, or the defunct Python Imaging Library (PIL), to be installed. An Atlas is composed of files: - a json file (.atlas) that contains the image file names and texture locations of the atlas. - one or multiple image files containing textures referenced by the .atlas file. Definition of .atlas files -------------------------- A file with ``.atlas`` is a json file formatted like this:: { "-.png": { "id1": [ , , , ], "id2": [ , , , ], # ... }, # ... } Example from the Kivy ``data/images/defaulttheme.atlas``:: { "defaulttheme-0.png": { "progressbar_background": [431, 224, 59, 24], "image-missing": [253, 344, 48, 48], "filechooser_selected": [1, 207, 118, 118], "bubble_btn": [83, 174, 32, 32], # ... and more ... } } In this example, "defaulttheme-0.png" is a large image, with the pixels in the rectangle from (431, 224) to (431 + 59, 224 + 24) usable as ``atlas://data/images/defaulttheme/progressbar_background`` in any image parameter. How to create an Atlas ---------------------- .. warning:: The atlas creation requires the Pillow library (or the defunct Imaging/PIL library). This requirement will be removed in the future when the Kivy core Image is able to support loading, blitting, and saving operations. You can directly use this module to create atlas files with this command:: $ python -m kivy.atlas Let's say you have a list of images that you want to put into an Atlas. The directory is named ``images`` with lots of 64x64 png files inside:: $ ls images $ cd images $ ls bubble.png bubble-red.png button.png button-down.png You can combine all the png's into one and generate the atlas file with:: $ python -m kivy.atlas myatlas 256x256 *.png Atlas created at myatlas.atlas 1 image has been created $ ls bubble.png bubble-red.png button.png button-down.png myatlas.atlas myatlas-0.png As you can see, we get 2 new files: ``myatlas.atlas`` and ``myatlas-0.png``. ``myatlas-0.png`` is a new 256x256 .png composed of all your images. .. note:: When using this script, the ids referenced in the atlas are the base names of the images without the extension. So, if you are going to name a file ``../images/button.png``, the id for this image will be ``button``. If you need path information included, you should include ``use_path`` as follows:: $ python -m kivy.atlas use_path myatlas 256 *.png In which case the id for ``../images/button.png`` will be ``images_button`` How to use an Atlas ------------------- Usually, you would use the atlas as follows:: a = Button(background_normal='images/button.png', background_down='images/button_down.png') In our previous example, we have created the atlas containing both images and put them in ``images/myatlas.atlas``. You can use url notation to reference them:: atlas://path/to/myatlas/id # will search for the ``path/to/myatlas.atlas`` and get the image ``id`` In our case, it would be:: atlas://images/myatlas/button .. note:: In the atlas url, there is no need to add the ``.atlas`` extension. It will be automatically append to the filename. Manual usage of the Atlas ------------------------- :: >>> from kivy.atlas import Atlas >>> atlas = Atlas('path/to/myatlas.atlas') >>> print(atlas.textures.keys()) ['bubble', 'bubble-red', 'button', 'button-down'] >>> print(atlas['button']) ''' __all__ = ('Atlas', ) import json from os.path import basename, dirname, join, splitext from kivy.event import EventDispatcher from kivy.logger import Logger from kivy.properties import AliasProperty, DictProperty import os # late import to prevent recursion CoreImage = None class Atlas(EventDispatcher): '''Manage texture atlas. See module documentation for more information. ''' textures = DictProperty({}) '''List of available textures within the atlas. :attr:`textures` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' def _get_filename(self): return self._filename filename = AliasProperty(_get_filename, None) '''Filename of the current Atlas. :attr:`filename` is an :class:`~kivy.properties.AliasProperty` and defaults to None. ''' def __init__(self, filename): self._filename = filename super(Atlas, self).__init__() self._load() def __getitem__(self, key): return self.textures[key] def _load(self): # late import to prevent recursive import. global CoreImage if CoreImage is None: from kivy.core.image import Image as CoreImage # must be a name finished by .atlas ? filename = self._filename assert(filename.endswith('.atlas')) filename = filename.replace('/', os.sep) Logger.debug('Atlas: Load <%s>' % filename) with open(filename, 'r') as fd: meta = json.load(fd) Logger.debug('Atlas: Need to load %d images' % len(meta)) d = dirname(filename) textures = {} for subfilename, ids in meta.items(): subfilename = join(d, subfilename) Logger.debug('Atlas: Load <%s>' % subfilename) # load the image ci = CoreImage(subfilename) # for all the uid, load the image, get the region, and put # it in our dict. for meta_id, meta_coords in ids.items(): x, y, w, h = meta_coords textures[meta_id] = ci.texture.get_region(*meta_coords) self.textures = textures @staticmethod def create(outname, filenames, size, padding=2, use_path=False): '''This method can be used to create an atlas manually from a set of images. :Parameters: `outname`: str Basename to use for ``.atlas`` creation and ``-.png`` associated images. `filenames`: list List of filenames to put in the atlas. `size`: int or list (width, height) Size of the atlas image. `padding`: int, defaults to 2 Padding to put around each image. Be careful. If you're using a padding < 2, you might have issues with the borders of the images. Because of the OpenGL linearization, it might use the pixels of the adjacent image. If you're using a padding >= 2, we'll automatically generate a "border" of 1px around your image. If you look at the result, don't be scared if the image inside is not exactly the same as yours :). `use_path`: bool, defaults to False If True, the relative path of the source png file names will be included in the atlas ids rather that just in the file names. Leading dots and slashes will be excluded and all other slashes in the path will be replaced with underscores. For example, if `use_path` is False (the default) and the file name is ``../data/tiles/green_grass.png``, the id will be ``green_grass``. If `use_path` is True, it will be ``data_tiles_green_grass``. .. versionchanged:: 1.8.0 Parameter use_path added ''' # Thanks to # omnisaurusgames.com/2011/06/texture-atlas-generation-using-python/ # for its initial implementation. try: from PIL import Image except ImportError: Logger.critical('Atlas: Imaging/PIL are missing') raise if isinstance(size, (tuple, list)): size_w, size_h = map(int, size) else: size_w = size_h = int(size) # open all of the images ims = list() for f in filenames: fp = open(f, 'rb') im = Image.open(fp) im.load() fp.close() ims.append((f, im)) # sort by image area ims = sorted(ims, key=lambda im: im[1].size[0] * im[1].size[1], reverse=True) # free boxes are empty space in our output image set # the freebox tuple format is: outidx, x, y, w, h freeboxes = [(0, 0, 0, size_w, size_h)] numoutimages = 1 # full boxes are areas where we have placed images in the atlas # the full box tuple format is: image, outidx, x, y, w, h, filename fullboxes = [] # do the actual atlasing by sticking the largest images we can # have into the smallest valid free boxes for imageinfo in ims: im = imageinfo[1] imw, imh = im.size imw += padding imh += padding if imw > size_w or imh > size_h: Logger.error( 'Atlas: image %s (%d by %d) is larger than the atlas size!' % (imageinfo[0], imw, imh)) return inserted = False while not inserted: for idx, fb in enumerate(freeboxes): # find the smallest free box that will contain this image if fb[3] >= imw and fb[4] >= imh: # we found a valid spot! Remove the current # freebox, and split the leftover space into (up to) # two new freeboxes del freeboxes[idx] if fb[3] > imw: freeboxes.append(( fb[0], fb[1] + imw, fb[2], fb[3] - imw, imh)) if fb[4] > imh: freeboxes.append(( fb[0], fb[1], fb[2] + imh, fb[3], fb[4] - imh)) # keep this sorted! freeboxes = sorted(freeboxes, key=lambda fb: fb[3] * fb[4]) fullboxes.append((im, fb[0], fb[1] + padding, fb[2] + padding, imw - padding, imh - padding, imageinfo[0])) inserted = True break if not inserted: # oh crap - there isn't room in any of our free # boxes, so we have to add a new output image freeboxes.append((numoutimages, 0, 0, size_w, size_h)) numoutimages += 1 # now that we've figured out where everything goes, make the output # images and blit the source images to the approriate locations Logger.info('Atlas: create an {0}x{1} rgba image'.format(size_w, size_h)) outimages = [Image.new('RGBA', (size_w, size_h)) for i in range(0, int(numoutimages))] for fb in fullboxes: x, y = fb[2], fb[3] out = outimages[fb[1]] out.paste(fb[0], (fb[2], fb[3])) w, h = fb[0].size if padding > 1: out.paste(fb[0].crop((0, 0, w, 1)), (x, y - 1)) out.paste(fb[0].crop((0, h - 1, w, h)), (x, y + h)) out.paste(fb[0].crop((0, 0, 1, h)), (x - 1, y)) out.paste(fb[0].crop((w - 1, 0, w, h)), (x + w, y)) # save the output images for idx, outimage in enumerate(outimages): outimage.save('%s-%d.png' % (outname, idx)) # write out an json file that says where everything ended up meta = {} for fb in fullboxes: fn = '%s-%d.png' % (basename(outname), fb[1]) if fn not in meta: d = meta[fn] = {} else: d = meta[fn] # fb[6] contain the filename if use_path: # use the path with separators replaced by _ # example '../data/tiles/green_grass.png' becomes # 'data_tiles_green_grass' uid = splitext(fb[6])[0] # remove leading dots and slashes uid = uid.lstrip('./\\') # replace remaining slashes with _ uid = uid.replace('/', '_').replace('\\', '_') else: # for example, '../data/tiles/green_grass.png' # just get only 'green_grass' as the uniq id. uid = splitext(basename(fb[6]))[0] x, y, w, h = fb[2:6] d[uid] = x, size_h - y - h, w, h outfn = '%s.atlas' % outname with open(outfn, 'w') as fd: json.dump(meta, fd) return outfn, meta if __name__ == '__main__': """ Main line program. Process command line arguments to make a new atlas. """ import sys from glob import glob argv = sys.argv[1:] # earlier import of kivy has already called getopt to remove kivy system # arguments from this line. That is all arguments up to the first '--' if len(argv) < 3: print('Usage: python -m kivy.atlas [-- [--use-path] ' '[--padding=2]] ' ' [, ...]') sys.exit(1) options = {'use_path': False} while True: option = argv[0] if option == '--use-path': options['use_path'] = True elif option.startswith('--padding='): options['padding'] = int(option.split('=', 1)[-1]) elif option[:2] == '--': print('Unknown option {}'.format(option)) sys.exit(1) else: break argv = argv[1:] outname = argv[0] try: if 'x' in argv[1]: size = map(int, argv[1].split('x', 1)) else: size = int(argv[1]) except ValueError: print('Error: size must be an integer or x') sys.exit(1) filenames = [fname for fnames in argv[2:] for fname in glob(fnames)] ret = Atlas.create(outname, filenames, size, **options) if not ret: print('Error while creating atlas!') sys.exit(1) fn, meta = ret print('Atlas created at', fn) print('%d image%s been created' % (len(meta), 's have' if len(meta) > 1 else ' has')) ================================================ FILE: tickeys/kivy/base.py ================================================ # pylint: disable=W0611 ''' Kivy Base ========= This module contains core Kivy functionality and is not intended for end users. Feel free to look though it, but calling any of these methods directly may well result in unpredicatable behavior. Event loop management --------------------- ''' __all__ = ( 'EventLoop', 'EventLoopBase', 'ExceptionHandler', 'ExceptionManagerBase', 'ExceptionManager', 'runTouchApp', 'stopTouchApp', ) import sys from kivy.config import Config from kivy.logger import Logger from kivy.utils import platform from kivy.clock import Clock from kivy.event import EventDispatcher from kivy.lang import Builder from kivy.context import register_context # private vars EventLoop = None class ExceptionHandler(object): '''Base handler that catches exceptions in :func:`runTouchApp`. You can subclass and extend it as follows:: class E(ExceptionHandler): def handle_exception(self, inst): Logger.exception('Exception catched by ExceptionHandler') return ExceptionManager.PASS ExceptionManager.add_handler(E()) All exceptions will be set to PASS, and logged to the console! ''' def __init__(self): pass def handle_exception(self, exception): '''Handle one exception, defaults to returning ExceptionManager.STOP. ''' return ExceptionManager.RAISE class ExceptionManagerBase: '''ExceptionManager manages exceptions handlers.''' RAISE = 0 PASS = 1 def __init__(self): self.handlers = [] self.policy = ExceptionManagerBase.RAISE def add_handler(self, cls): '''Add a new exception handler to the stack.''' if not cls in self.handlers: self.handlers.append(cls) def remove_handler(self, cls): '''Remove a exception handler from the stack.''' if cls in self.handlers: self.handlers.remove(cls) def handle_exception(self, inst): '''Called when an exception occured in the runTouchApp() main loop.''' ret = self.policy for handler in self.handlers: r = handler.handle_exception(inst) if r == ExceptionManagerBase.PASS: ret = r return ret #: Instance of a :class:`ExceptionManagerBase` implementation. ExceptionManager = register_context('ExceptionManager', ExceptionManagerBase) class EventLoopBase(EventDispatcher): '''Main event loop. This loop handles the updating of input and dispatching events. ''' __events__ = ('on_start', 'on_pause', 'on_stop') def __init__(self): super(EventLoopBase, self).__init__() self.quit = False self.input_events = [] self.postproc_modules = [] self.status = 'idle' self.input_providers = [] self.input_providers_autoremove = [] self.event_listeners = [] self.window = None self.me_list = [] @property def touches(self): '''Return the list of all touches currently in down or move states. ''' return self.me_list def ensure_window(self): '''Ensure that we have a window. ''' import kivy.core.window # NOQA if not self.window: Logger.critical('App: Unable to get a Window, abort.') sys.exit(1) def set_window(self, window): '''Set the window used for the event loop. ''' self.window = window def add_input_provider(self, provider, auto_remove=False): '''Add a new input provider to listen for touch events. ''' if provider not in self.input_providers: self.input_providers.append(provider) if auto_remove: self.input_providers_autoremove.append(provider) def remove_input_provider(self, provider): '''Remove an input provider. ''' if provider in self.input_providers: self.input_providers.remove(provider) def add_event_listener(self, listener): '''Add a new event listener for getting touch events. ''' if not listener in self.event_listeners: self.event_listeners.append(listener) def remove_event_listener(self, listener): '''Remove an event listener from the list. ''' if listener in self.event_listeners: self.event_listeners.remove(listener) def start(self): '''Must be called only once before run(). This starts all configured input providers.''' self.status = 'started' self.quit = False for provider in self.input_providers: provider.start() self.dispatch('on_start') def close(self): '''Exit from the main loop and stop all configured input providers.''' self.quit = True self.stop() self.status = 'closed' def stop(self): '''Stop all input providers and call callbacks registered using EventLoop.add_stop_callback().''' # XXX stop in reverse order that we started them!! (like push # pop), very important because e.g. wm_touch and WM_PEN both # store old window proc and the restore, if order is messed big # problem happens, crashing badly without error for provider in reversed(self.input_providers[:]): provider.stop() if provider in self.input_providers_autoremove: self.input_providers_autoremove.remove(provider) self.input_providers.remove(provider) # ensure any restart will not break anything later. self.input_events = [] self.status = 'stopped' self.dispatch('on_stop') def add_postproc_module(self, mod): '''Add a postproc input module (DoubleTap, TripleTap, DeJitter RetainTouch are defaults).''' if mod not in self.postproc_modules: self.postproc_modules.append(mod) def remove_postproc_module(self, mod): '''Remove a postproc module.''' if mod in self.postproc_modules: self.postproc_modules.remove(mod) def post_dispatch_input(self, etype, me): '''This function is called by dispatch_input() when we want to dispatch an input event. The event is dispatched to all listeners and if grabbed, it's dispatched to grabbed widgets. ''' # update available list if etype == 'begin': self.me_list.append(me) elif etype == 'end': if me in self.me_list: self.me_list.remove(me) # dispatch to listeners if not me.grab_exclusive_class: for listener in self.event_listeners: listener.dispatch('on_motion', etype, me) # dispatch grabbed touch me.grab_state = True for _wid in me.grab_list[:]: # it's a weakref, call it! wid = _wid() if wid is None: # object is gone, stop. me.grab_list.remove(_wid) continue root_window = wid.get_root_window() if wid != root_window and root_window is not None: me.push() w, h = root_window.system_size if platform == 'ios': w, h = root_window.size kheight = root_window.keyboard_height smode = root_window.softinput_mode me.scale_for_screen(w, h, rotation=root_window.rotation, smode=smode, kheight=kheight) parent = wid.parent # and do to_local until the widget try: if parent: me.apply_transform_2d(parent.to_widget) else: me.apply_transform_2d(wid.to_widget) me.apply_transform_2d(wid.to_parent) except AttributeError: # when using inner window, an app have grab the touch # but app is removed. the touch can't access # to one of the parent. (i.e, self.parent will be None) # and BAM the bug happen. me.pop() continue me.grab_current = wid wid._context.push() if etype == 'begin': # don't dispatch again touch in on_touch_down # a down event are nearly uniq here. # wid.dispatch('on_touch_down', touch) pass elif etype == 'update': if wid._context.sandbox: with wid._context.sandbox: wid.dispatch('on_touch_move', me) else: wid.dispatch('on_touch_move', me) elif etype == 'end': if wid._context.sandbox: with wid._context.sandbox: wid.dispatch('on_touch_up', me) else: wid.dispatch('on_touch_up', me) wid._context.pop() me.grab_current = None if wid != root_window and root_window is not None: me.pop() me.grab_state = False def _dispatch_input(self, *ev): # remove the save event for the touch if exist if ev in self.input_events: self.input_events.remove(ev) self.input_events.append(ev) def dispatch_input(self): '''Called by idle() to read events from input providers, pass events to postproc, and dispatch final events. ''' # first, aquire input events for provider in self.input_providers: provider.update(dispatch_fn=self._dispatch_input) # execute post-processing modules for mod in self.postproc_modules: self.input_events = mod.process(events=self.input_events) # real dispatch input input_events = self.input_events pop = input_events.pop post_dispatch_input = self.post_dispatch_input while input_events: post_dispatch_input(*pop(0)) def idle(self): '''This function is called after every frame. By default: * it "ticks" the clock to the next frame. * it reads all input and dispatches events. * it dispatches `on_update`, `on_draw` and `on_flip` events to the window. ''' # update dt Clock.tick() # read and dispatch input from providers self.dispatch_input() # flush all the canvas operation Builder.sync() # tick before draw Clock.tick_draw() # flush all the canvas operation Builder.sync() window = self.window if window and window.canvas.needs_redraw: window.dispatch('on_draw') window.dispatch('on_flip') # don't loop if we don't have listeners ! if len(self.event_listeners) == 0: Logger.error('Base: No event listeners have been created') Logger.error('Base: Application will leave') self.exit() return False return self.quit def run(self): '''Main loop''' while not self.quit: self.idle() self.exit() def exit(self): '''Close the main loop and close the window.''' self.close() if self.window: self.window.close() def on_stop(self): '''Event handler for `on_stop` events which will be fired right after all input providers have been stopped.''' pass def on_pause(self): '''Event handler for `on_pause` which will be fired when the event loop is paused.''' pass def on_start(self): '''Event handler for `on_start` which will be fired right after all input providers have been started.''' pass #: EventLoop instance EventLoop = EventLoopBase() def _run_mainloop(): '''If no window has been created, this will be the executed mainloop.''' while True: try: EventLoop.run() stopTouchApp() break except BaseException as inst: # use exception manager first r = ExceptionManager.handle_exception(inst) if r == ExceptionManager.RAISE: stopTouchApp() raise else: pass def runTouchApp(widget=None, slave=False): '''Static main function that starts the application loop. You can access some magic via the following arguments: :Parameters: `` To make dispatching work, you need at least one input listener. If not, application will leave. (MTWindow act as an input listener) `widget` If you pass only a widget, a MTWindow will be created and your widget will be added to the window as the root widget. `slave` No event dispatching is done. This will be your job. `widget + slave` No event dispatching is done. This will be your job but we try to get the window (must be created by you beforehand) and add the widget to it. Very usefull for embedding Kivy in another toolkit. (like Qt, check kivy-designed) ''' from kivy.input import MotionEventFactory, kivy_postproc_modules # Ok, we got one widget, and we are not in slave mode # so, user don't create the window, let's create it for him ! if widget: EventLoop.ensure_window() # Instance all configured input for key, value in Config.items('input'): Logger.debug('Base: Create provider from %s' % (str(value))) # split value args = str(value).split(',', 1) if len(args) == 1: args.append('') provider_id, args = args provider = MotionEventFactory.get(provider_id) if provider is None: Logger.warning('Base: Unknown <%s> provider' % str(provider_id)) continue # create provider p = provider(key, args) if p: EventLoop.add_input_provider(p, True) # add postproc modules for mod in list(kivy_postproc_modules.values()): EventLoop.add_postproc_module(mod) # add main widget if widget and EventLoop.window: if widget not in EventLoop.window.children: EventLoop.window.add_widget(widget) # start event loop Logger.info('Base: Start application main loop') EventLoop.start() # we are in a slave mode, don't do dispatching. if slave: return # in non-slave mode, they are 2 issues # # 1. if user created a window, call the mainloop from window. # This is due to glut, it need to be called with # glutMainLoop(). Only FreeGLUT got a gluMainLoopEvent(). # So, we are executing the dispatching function inside # a redisplay event. # # 2. if no window is created, we are dispatching event lopp # ourself (previous behavior.) # try: if EventLoop.window is None: _run_mainloop() else: EventLoop.window.mainloop() finally: stopTouchApp() def stopTouchApp(): '''Stop the current application by leaving the main loop''' if EventLoop is None: return if EventLoop.status != 'started': return Logger.info('Base: Leaving application in progress...') EventLoop.close() ================================================ FILE: tickeys/kivy/cache.py ================================================ ''' Cache manager ============= The cache manager can be used to store python objects attached to a unique key. The cache can be controlled in two ways: with a object limit or a timeout. For example, we can create a new cache with a limit of 10 objects and a timeout of 5 seconds:: # register a new Cache Cache.register('mycache', limit=10, timeout=5) # create an object + id key = 'objectid' instance = Label(text=text) Cache.append('mycache', key, instance) # retrieve the cached object instance = Cache.get('mycache', key) If the instance is NULL, the cache may have trashed it because you've not used the label for 5 seconds and you've reach the limit. ''' __all__ = ('Cache', ) from os import environ from kivy.logger import Logger from kivy.clock import Clock class Cache(object): '''See module documentation for more information. ''' _categories = {} _objects = {} @staticmethod def register(category, limit=None, timeout=None): '''Register a new category in the cache with the specified limit. :Parameters: `category` : str Identifier of the category. `limit` : int (optional) Maximum number of objects allowed in the cache. If None, no limit is applied. `timeout` : double (optional) Time after which to delete the object if it has not been used. If None, no timeout is applied. ''' Cache._categories[category] = { 'limit': limit, 'timeout': timeout} Cache._objects[category] = {} Logger.debug( 'Cache: register <%s> with limit=%s, timeout=%s' % (category, str(limit), str(timeout))) @staticmethod def append(category, key, obj, timeout=None): '''Add a new object to the cache. :Parameters: `category` : str Identifier of the category. `key` : str Unique identifier of the object to store. `obj` : object Object to store in cache. `timeout` : double (optional) Time after which to delete the object if it has not been used. If None, no timeout is applied. ''' #check whether obj should not be cached first if getattr(obj, '_no_cache', False): return try: cat = Cache._categories[category] except KeyError: Logger.warning('Cache: category <%s> not exist' % category) return timeout = timeout or cat['timeout'] # FIXME: activate purge when limit is hit #limit = cat['limit'] #if limit is not None and len(Cache._objects[category]) >= limit: # Cache._purge_oldest(category) Cache._objects[category][key] = { 'object': obj, 'timeout': timeout, 'lastaccess': Clock.get_time(), 'timestamp': Clock.get_time()} @staticmethod def get(category, key, default=None): '''Get a object from the cache. :Parameters: `category` : str Identifier of the category. `key` : str Unique identifier of the object in the store. `default` : anything, defaults to None Default value to be returned if the key is not found. ''' try: Cache._objects[category][key]['lastaccess'] = Clock.get_time() return Cache._objects[category][key]['object'] except Exception: return default @staticmethod def get_timestamp(category, key, default=None): '''Get the object timestamp in the cache. :Parameters: `category` : str Identifier of the category. `key` : str Unique identifier of the object in the store. `default` : anything, defaults to None Default value to be returned if the key is not found. ''' try: return Cache._objects[category][key]['timestamp'] except Exception: return default @staticmethod def get_lastaccess(category, key, default=None): '''Get the objects last access time in the cache. :Parameters: `category` : str Identifier of the category. `key` : str Unique identifier of the object in the store. `default` : anything, defaults to None Default value to be returned if the key is not found. ''' try: return Cache._objects[category][key]['lastaccess'] except Exception: return default @staticmethod def remove(category, key=None): '''Purge the cache. :Parameters: `category` : str Identifier of the category. `key` : str (optional) Unique identifier of the object in the store. If this arguement is not supplied, the entire category will be purged. ''' try: if key is not None: del Cache._objects[category][key] else: Cache._objects[category] = {} except Exception: pass @staticmethod def _purge_oldest(category, maxpurge=1): print('PURGE', category) import heapq heap_list = [] for key in Cache._objects[category]: obj = Cache._objects[category][key] if obj['lastaccess'] == obj['timestamp']: continue heapq.heappush(heap_list, (obj['lastaccess'], key)) print('<<<', obj['lastaccess']) n = 0 while n < maxpurge: try: lastaccess, key = heapq.heappop(heap_list) print('=>', key, lastaccess, Clock.get_time()) except Exception: return del Cache._objects[category][key] @staticmethod def _purge_by_timeout(dt): curtime = Clock.get_time() for category in Cache._objects: if category not in Cache._categories: continue timeout = Cache._categories[category]['timeout'] if timeout is not None and dt > timeout: # XXX got a lag ! that may be because the frame take lot of # time to draw. and the timeout is not adapted to the current # framerate. So, increase the timeout by two. # ie: if the timeout is 1 sec, and framerate go to 0.7, newly # object added will be automaticly trashed. timeout *= 2 Cache._categories[category]['timeout'] = timeout continue for key in list(Cache._objects[category].keys())[:]: lastaccess = Cache._objects[category][key]['lastaccess'] objtimeout = Cache._objects[category][key]['timeout'] # take the object timeout if available if objtimeout is not None: timeout = objtimeout # no timeout, cancel if timeout is None: continue if curtime - lastaccess > timeout: del Cache._objects[category][key] @staticmethod def print_usage(): '''Print the cache usage to the console.''' print('Cache usage :') for category in Cache._categories: print(' * %s : %d / %s, timeout=%s' % ( category.capitalize(), len(Cache._objects[category]), str(Cache._categories[category]['limit']), str(Cache._categories[category]['timeout']))) if 'KIVY_DOC_INCLUDE' not in environ: # install the schedule clock for purging Clock.schedule_interval(Cache._purge_by_timeout, 1) ================================================ FILE: tickeys/kivy/clock.py ================================================ ''' Clock object ============ The :class:`Clock` object allows you to schedule a function call in the future; once or repeatedly at specified intervals. You can get the time elapsed between the scheduling and the calling of the callback via the `dt` argument:: # dt means delta-time def my_callback(dt): pass # call my_callback every 0.5 seconds Clock.schedule_interval(my_callback, 0.5) # call my_callback in 5 seconds Clock.schedule_once(my_callback, 5) # call my_callback as soon as possible (usually next frame.) Clock.schedule_once(my_callback) .. note:: If the callback returns False, the schedule will be removed. If you want to schedule a function to call with default arguments, you can use the `functools.partial `_ python module:: from functools import partial def my_callback(value, key, *largs): pass Clock.schedule_interval(partial(my_callback, 'my value', 'my key'), 0.5) Conversely, if you want to schedule a function that doesn't accept the dt argument, you can use a `lambda `_ expression to write a short function that does accept dt. For Example:: def no_args_func(): print("I accept no arguments, so don't schedule me in the clock") Clock.schedule_once(lambda dt: no_args_func(), 0.5) .. note:: You cannot unschedule an anonymous function unless you keep a reference to it. It's better to add \*args to your function definition so that it can be called with an arbitrary number of parameters. .. important:: The callback is weak-referenced: you are responsible for keeping a reference to your original object/callback. If you don't keep a reference, the ClockBase will never execute your callback. For example:: class Foo(object): def start(self): Clock.schedule_interval(self.callback, 0.5) def callback(self, dt): print('In callback') # A Foo object is created and the method start is called. # Because no reference is kept to the instance returned from Foo(), # the object will be collected by the Python Garbage Collector and # your callback will be never called. Foo().start() # So you should do the following and keep a reference to the instance # of foo until you don't need it anymore! foo = Foo() foo.start() .. _schedule-before-frame: Schedule before frame --------------------- .. versionadded:: 1.0.5 Sometimes you need to schedule a callback BEFORE the next frame. Starting from 1.0.5, you can use a timeout of -1:: Clock.schedule_once(my_callback, 0) # call after the next frame Clock.schedule_once(my_callback, -1) # call before the next frame The Clock will execute all the callbacks with a timeout of -1 before the next frame even if you add a new callback with -1 from a running callback. However, :class:`Clock` has an iteration limit for these callbacks: it defaults to 10. If you schedule a callback that schedules a callback that schedules a .. etc more than 10 times, it will leave the loop and send a warning to the console, then continue after the next frame. This is implemented to prevent bugs from hanging or crashing the application. If you need to increase the limit, set the :attr:`max_iteration` property:: from kivy.clock import Clock Clock.max_iteration = 20 .. _triggered-events: Triggered Events ---------------- .. versionadded:: 1.0.5 A triggered event is a way to defer a callback exactly like schedule_once(), but with some added convenience. The callback will only be scheduled once per frame even if you call the trigger twice (or more). This is not the case with :meth:`Clock.schedule_once`:: # will run the callback twice before the next frame Clock.schedule_once(my_callback) Clock.schedule_once(my_callback) # will run the callback once before the next frame t = Clock.create_trigger(my_callback) t() t() Before triggered events, you may have used this approach in a widget:: def trigger_callback(self, *largs): Clock.unschedule(self.callback) Clock.schedule_once(self.callback) As soon as you call `trigger_callback()`, it will correctly schedule the callback once in the next frame. It is more convenient to create and bind to the triggered event than using :meth:`Clock.schedule_once` in a function:: from kivy.clock import Clock from kivy.uix.widget import Widget class Sample(Widget): def __init__(self, **kwargs): self._trigger = Clock.create_trigger(self.cb) super(Sample, self).__init__(**kwargs) self.bind(x=self._trigger, y=self._trigger) def cb(self, *largs): pass Even if x and y changes within one frame, the callback is only run once. .. note:: :meth:`ClockBase.create_trigger` also has a timeout parameter that behaves exactly like :meth:`ClockBase.schedule_once`. Threading ---------- .. versionadded:: 1.9.0 Often, other threads are used to schedule callbacks with kivy's main thread using :class:`ClockBase`. Therefore, it's important to know what is thread safe and what isn't. All the :class:`ClockBase` and :class:`ClockEvent` methods are safe with respect to kivy's thread. That is, it's always safe to call these methods from a single thread that is not the kivy thread. However, there are no guarantees as to the order in which these callbacks will be executed. Calling a previously created trigger from two different threads (even if one of them is the kivy thread), or calling the trigger and its :meth:`ClockEvent.cancel` method from two different threads at the same time is not safe. That is, although no exception will be raised, there no guarantees that calling the trigger from two different threads will not result in the callback being executed twice, or not executed at all. Similarly, such issues might arise when calling the trigger and canceling it with :meth:`ClockBase.unschedule` or :meth:`ClockEvent.cancel` from two threads simultaneously. Therefore, it is safe to call :meth:`ClockBase.create_trigger`, :meth:`ClockBase.schedule_once`, :meth:`ClockBase.schedule_interval`, or call or cancel a previously created trigger from an external thread. The following code, though, is not safe because it calls or cancels from two threads simultaneously without any locking mechanism:: event = Clock.create_trigger(func) # in thread 1 event() # in thread 2 event() # now, the event may be scheduled twice or once # the following is also unsafe # in thread 1 event() # in thread 2 event.cancel() # now, the event may or may not be scheduled and a subsequent call # may schedule it twice Note, in the code above, thread 1 or thread 2 could be the kivy thread, not just an external thread. ''' __all__ = ('Clock', 'ClockBase', 'ClockEvent', 'mainthread') from sys import platform from os import environ from functools import wraps, partial from kivy.context import register_context from kivy.weakmethod import WeakMethod from kivy.config import Config from kivy.logger import Logger import time try: import ctypes if platform in ('win32', 'cygwin'): # Win32 Sleep function is only 10-millisecond resolution, so # instead use a waitable timer object, which has up to # 100-nanosecond resolution (hardware and implementation # dependent, of course). _kernel32 = ctypes.windll.kernel32 class _ClockBase(object): def __init__(self): self._timer = _kernel32.CreateWaitableTimerA(None, True, None) def usleep(self, microseconds): delay = ctypes.c_longlong(int(-microseconds * 10)) _kernel32.SetWaitableTimer( self._timer, ctypes.byref(delay), 0, ctypes.c_void_p(), ctypes.c_void_p(), False) _kernel32.WaitForSingleObject(self._timer, 0xffffffff) _default_time = time.clock else: if platform == 'darwin': _libc = ctypes.CDLL('libc.dylib') else: _libc = ctypes.CDLL('libc.so') _libc.usleep.argtypes = [ctypes.c_ulong] _libc_usleep = _libc.usleep class _ClockBase(object): def usleep(self, microseconds): _libc_usleep(int(microseconds)) _default_time = time.time except (OSError, ImportError): # ImportError: ctypes is not available on python-for-android. # OSError: if the libc cannot be readed (like with buildbot: invalid ELF # header) _default_time = time.time _default_sleep = time.sleep class _ClockBase(object): def usleep(self, microseconds): _default_sleep(microseconds / 1000000.) def _hash(cb): if hasattr(cb, '__self__') and cb.__self__ is not None: return (id(cb.__self__) & 0xFF00) >> 8 return (id(cb) & 0xFF00) >> 8 class ClockEvent(object): ''' A class that describes a callback scheduled with kivy's :attr:`Clock`. This class is never created by the user; instead, kivy creates and returns an instance of this class when scheduling a callback. .. warning:: Most of the methods of this class are internal and can change without notice. The only exception are the :meth:`cancel` and :meth:`__call__` methods. ''' def __init__(self, clock, loop, callback, timeout, starttime, cid, trigger=False): self.clock = clock self.cid = cid self.loop = loop self.weak_callback = None self.callback = callback self.timeout = timeout self._is_triggered = trigger self._last_dt = starttime self._dt = 0. if trigger: clock._events[cid].append(self) def __call__(self, *largs): ''' Schedules the callback associated with this instance. If the callback is already scheduled, it will not be scheduled again. ''' # if the event is not yet triggered, do it ! if self._is_triggered is False: self._is_triggered = True # update starttime self._last_dt = self.clock._last_tick self.clock._events[self.cid].append(self) return True def get_callback(self): callback = self.callback if callback is not None: return callback callback = self.weak_callback if callback.is_dead(): return None return callback() @property def is_triggered(self): return self._is_triggered def cancel(self): ''' Cancels the callback if it was scheduled to be called. ''' if self._is_triggered: self._is_triggered = False try: self.clock._events[self.cid].remove(self) except ValueError: pass def release(self): self.weak_callback = WeakMethod(self.callback) self.callback = None def tick(self, curtime, remove): # timeout happened ? (check also if we would miss from 5ms) this # 5ms increase the accuracy if the timing of animation for # example. if curtime - self._last_dt < self.timeout - 0.005: return True # calculate current timediff for this event self._dt = curtime - self._last_dt self._last_dt = curtime loop = self.loop # get the callback callback = self.get_callback() if callback is None: self._is_triggered = False try: remove(self) except ValueError: pass return False # if it's a trigger, allow to retrigger inside the callback # we have to remove event here, otherwise, if we remove later, the user # might have canceled in the callback and then re-triggered. That'd # result in the removal of the re-trigger if not loop: self._is_triggered = False try: remove(self) except ValueError: pass # call the callback ret = callback(self._dt) # if the user returns False explicitly, remove the event if loop and ret is False: self._is_triggered = False try: remove(self) except ValueError: pass return False return loop def __repr__(self): return '' % self.get_callback() class ClockBase(_ClockBase): '''A clock object with event support. ''' __slots__ = ('_dt', '_last_fps_tick', '_last_tick', '_fps', '_rfps', '_start_tick', '_fps_counter', '_rfps_counter', '_events', '_frames', '_frames_displayed', '_max_fps', 'max_iteration') MIN_SLEEP = 0.005 SLEEP_UNDERSHOOT = MIN_SLEEP - 0.001 def __init__(self): super(ClockBase, self).__init__() self._dt = 0.0001 self._start_tick = self._last_tick = self.time() self._fps = 0 self._rfps = 0 self._fps_counter = 0 self._rfps_counter = 0 self._last_fps_tick = None self._frames = 0 self._frames_displayed = 0 self._events = [[] for i in range(256)] self._max_fps = float(Config.getint('graphics', 'maxfps')) #: .. versionadded:: 1.0.5 #: When a schedule_once is used with -1, you can add a limit on #: how iteration will be allowed. That is here to prevent too much #: relayout. self.max_iteration = 10 @property def frametime(self): '''Time spent between the last frame and the current frame (in seconds). .. versionadded:: 1.8.0 ''' return self._dt @property def frames(self): '''Number of internal frames (not necesseraly drawed) from the start of the clock. .. versionadded:: 1.8.0 ''' return self._frames @property def frames_displayed(self): '''Number of displayed frames from the start of the clock. ''' return self._frames_displayed def tick(self): '''Advance the clock to the next step. Must be called every frame. The default clock has a tick() function called by the core Kivy framework.''' self._release_references() # do we need to sleep ? if self._max_fps > 0: min_sleep = self.MIN_SLEEP sleep_undershoot = self.SLEEP_UNDERSHOOT fps = self._max_fps usleep = self.usleep sleeptime = 1 / fps - (self.time() - self._last_tick) while sleeptime - sleep_undershoot > min_sleep: usleep(1000000 * (sleeptime - sleep_undershoot)) sleeptime = 1 / fps - (self.time() - self._last_tick) # tick the current time current = self.time() self._dt = current - self._last_tick self._frames += 1 self._fps_counter += 1 self._last_tick = current # calculate fps things if self._last_fps_tick is None: self._last_fps_tick = current elif current - self._last_fps_tick > 1: d = float(current - self._last_fps_tick) self._fps = self._fps_counter / d self._rfps = self._rfps_counter self._last_fps_tick = current self._fps_counter = 0 self._rfps_counter = 0 # process event self._process_events() return self._dt def tick_draw(self): '''Tick the drawing counter. ''' self._process_events_before_frame() self._rfps_counter += 1 self._frames_displayed += 1 def get_fps(self): '''Get the current average FPS calculated by the clock. ''' return self._fps def get_rfps(self): '''Get the current "real" FPS calculated by the clock. This counter reflects the real framerate displayed on the screen. In contrast to get_fps(), this function returns a counter of the number of frames, not the average of frames per second. ''' return self._rfps def get_time(self): '''Get the last tick made by the clock.''' return self._last_tick def get_boottime(self): '''Get the time in seconds from the application start.''' return self._last_tick - self._start_tick def create_trigger(self, callback, timeout=0): '''Create a Trigger event. Check module documentation for more information. :returns: A :class:`ClockEvent` instance. To schedule the callback of this instance, you can call it. .. versionadded:: 1.0.5 ''' ev = ClockEvent(self, False, callback, timeout, 0, _hash(callback)) ev.release() return ev def schedule_once(self, callback, timeout=0): '''Schedule an event in seconds. If is unspecified or 0, the callback will be called after the next frame is rendered. :returns: A :class:`ClockEvent` instance. As opposed to :meth:`create_trigger` which only creates the trigger event, this method also schedules it. .. versionchanged:: 1.0.5 If the timeout is -1, the callback will be called before the next frame (at :meth:`tick_draw`). ''' if not callable(callback): raise ValueError('callback must be a callable, got %s' % callback) event = ClockEvent( self, False, callback, timeout, self._last_tick, _hash(callback), True) return event def schedule_interval(self, callback, timeout): '''Schedule an event to be called every seconds. :returns: A :class:`ClockEvent` instance. As opposed to :meth:`create_trigger` which only creates the trigger event, this method also schedules it. ''' if not callable(callback): raise ValueError('callback must be a callable, got %s' % callback) event = ClockEvent( self, True, callback, timeout, self._last_tick, _hash(callback), True) return event def unschedule(self, callback, all=True): '''Remove a previously scheduled event. :parameters: `callback`: :class:`ClockEvent` or a callable. If it's a :class:`ClockEvent` instance, then the callback associated with this event will be canceled if it is scheduled. If it's a callable, then the callable will be unscheduled if it is scheduled. `all`: bool If True and if `callback` is a callable, all instances of this callable will be unscheduled (i.e. if this callable was scheduled multiple times). Defaults to `True`. .. versionchanged:: 1.9.0 The all parameter was added. Before, it behaved as if `all` was `True`. ''' if isinstance(callback, ClockEvent): callback.cancel() else: if all: for ev in self._events[_hash(callback)][:]: if ev.get_callback() == callback: ev.cancel() else: for ev in self._events[_hash(callback)][:]: if ev.get_callback() == callback: ev.cancel() break def _release_references(self): # call that function to release all the direct reference to any # callback and replace it with a weakref events = self._events for events in self._events: for event in events[:]: if event.callback is not None: event.release() def _process_events(self): for events in self._events: remove = events.remove for event in events[:]: # event may be already removed from original list if event in events: event.tick(self._last_tick, remove) def _process_events_before_frame(self): found = True count = self.max_iteration events = self._events while found: count -= 1 if count == -1: Logger.critical( 'Clock: Warning, too much iteration done before' ' the next frame. Check your code, or increase' ' the Clock.max_iteration attribute') break # search event that have timeout = -1 found = False for events in self._events: remove = events.remove for event in events[:]: if event.timeout != -1: continue found = True # event may be already removed from original list if event in events: event.tick(self._last_tick, remove) time = staticmethod(partial(_default_time)) ClockBase.time.__doc__ = '''Proxy method for time.time() or time.clock(), whichever is more suitable for the running OS''' def mainthread(func): '''Decorator that will schedule the call of the function for the next available frame in the mainthread. It can be useful when you use :class:`~kivy.network.urlrequest.UrlRequest` or when you do Thread programming: you cannot do any OpenGL-related work in a thread. Please note that this method will return directly and no result can be returned:: @mainthread def callback(self, *args): print('The request succedded!', 'This callback is called in the main thread.') self.req = UrlRequest(url='http://...', on_success=callback) .. versionadded:: 1.8.0 ''' @wraps(func) def delayed_func(*args, **kwargs): def callback_func(dt): func(*args, **kwargs) Clock.schedule_once(callback_func, 0) return delayed_func if 'KIVY_DOC_INCLUDE' in environ: #: Instance of :class:`ClockBase`. Clock = None else: Clock = register_context('Clock', ClockBase) ================================================ FILE: tickeys/kivy/compat.py ================================================ ''' Compatibility module for Python 2.7 and > 3.3 ============================================= ''' __all__ = ('PY2', 'string_types', 'queue', 'iterkeys', 'itervalues', 'iteritems') import sys try: import queue except ImportError: import Queue as queue #: True if Python 2 intepreter is used PY2 = sys.version_info[0] == 2 #: String types that can be used for checking if a object is a string string_types = None text_type = None if PY2: string_types = basestring text_type = unicode else: string_types = text_type = str #: unichr is just chr in py3, since all strings are unicode if PY2: unichr = unichr else: unichr = chr if PY2: iterkeys = lambda d: d.iterkeys() itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() else: iterkeys = lambda d: iter(d.keys()) itervalues = lambda d: iter(d.values()) iteritems = lambda d: iter(d.items()) ================================================ FILE: tickeys/kivy/config.py ================================================ ''' Configuration object ==================== The :class:`Config` object is an instance of a modified Python ConfigParser. See the `ConfigParser documentation `_ for more information. Kivy has a configuration file which determines the default settings. In order to change these settings, you can alter this file manually or use the Config object. Please see the :ref:`Configure Kivy` section for more information. Note: To avoid instances where the config settings do not work or they are not applied before window creation (like setting an initial window size), Config.set should be used before importing any modules that affect the application window (ie. importing Window). Ideally, these settings should be declared right at the start of your main.py script. Usage of the Config object -------------------------- To read a configuration token from a particular section:: >>> from kivy.config import Config >>> Config.getint('kivy', 'show_fps') 0 Change the configuration and save it:: >>> Config.set('postproc', 'retain_time', '50') >>> Config.write() .. versionchanged:: 1.7.1 The ConfigParser should work correctly with utf-8 now. The values are converted from ascii to unicode only when needed. The method get() returns utf-8 strings. .. _configuration-tokens: Available configuration tokens ------------------------------ .. |log_levels| replace:: 'debug', 'info', 'warning', 'error' or 'critical' :kivy: `desktop`: int, 0 or 1 This option controls desktop OS specific features, such as enabling drag-able scroll-bar in scroll views, disabling of bubbles in TextInput etc. 0 is disabled, 1 is enabled. `exit_on_escape`: int, 0 or 1 Enables exiting kivy when escape is pressed. 0 is disabled, 1 is enabled. `pause_on_minimize`: int, 0 or 1 If set to `1`, the main loop is paused and the `on_pause` event is dispatched when the window is minimized. This option is intended for desktop use only. Defaults to `0`. `keyboard_layout`: string Identifier of the layout to use. `keyboard_mode`: string Specifies the keyboard mode to use. If can be one of the following: * '' - Let Kivy choose the best option for your current platform. * 'system' - real keyboard. * 'dock' - one virtual keyboard docked to a screen side. * 'multi' - one virtual keyboard for every widget request. * 'systemanddock' - virtual docked keyboard plus input from real keyboard. * 'systemandmulti' - analogous. `log_dir`: string Path of log directory. `log_enable`: int, 0 or 1 Activate file logging. 0 is disabled, 1 is enabled. `log_level`: string, one of |log_levels| Set the minimum log level to use. `log_name`: string Format string to use for the filename of log file. `window_icon`: string Path of the window icon. Use this if you want to replace the default pygame icon. :postproc: `double_tap_distance`: float Maximum distance allowed for a double tap, normalized inside the range 0 - 1000. `double_tap_time`: int Time allowed for the detection of double tap, in milliseconds. `ignore`: list of tuples List of regions where new touches are ignored. This configuration token can be used to resolve hotspot problems with DIY hardware. The format of the list must be:: ignore = [(xmin, ymin, xmax, ymax), ...] All the values must be inside the range 0 - 1. `jitter_distance`: int Maximum distance for jitter detection, normalized inside the range 0 - 1000. `jitter_ignore_devices`: string, separated with commas List of devices to ignore from jitter detection. `retain_distance`: int If the touch moves more than is indicated by retain_distance, it will not be retained. Argument should be an int between 0 and 1000. `retain_time`: int Time allowed for a retain touch, in milliseconds. `triple_tap_distance`: float Maximum distance allowed for a triple tap, normalized inside the range 0 - 1000. `triple_tap_time`: int Time allowed for the detection of triple tap, in milliseconds. :graphics: `borderless`: int , one of 0 or 1 If set to `1`, removes the window border/decoration. `window_state`: string , one of 'visible', 'hidden', 'maximized' or 'minimized' Sets the window state, defaults to 'visible'. This option is available only for the SDL2 window provider and it should be used on desktop OSes. `fbo`: string, one of 'hardware', 'software' or 'force-hardware' Selects the FBO backend to use. `fullscreen`: int or string, one of 0, 1, 'fake' or 'auto' Activate fullscreen. If set to `1`, a resolution of `width` times `height` pixels will be used. If set to `auto`, your current display's resolution will be used instead. This is most likely what you want. If you want to place the window in another display, use `fake`, or set the `borderless` option from the graphics section, then adjust `width`, `height`, `top` and `left`. `height`: int Height of the :class:`~kivy.core.window.Window`, not used if `fullscreen` is set to `auto`. `left`: int Left position of the :class:`~kivy.core.window.Window`. `maxfps`: int, defaults to 60 Maximum FPS allowed. 'multisamples': int, defaults to 2 Sets the `MultiSample Anti-Aliasing (MSAA) `_ level. Increasing this value results in smoother graphics but at the cost of processing time. .. note:: This feature is limited by device hardware support and will have no effect on devices which do not support the level of MSAA requested. `position`: string, one of 'auto' or 'custom' Position of the window on your display. If `auto` is used, you have no control of the initial position: `top` and `left` are ignored. `show_cursor`: int, one of 0 or 1 Show the cursor on the screen. `top`: int Top position of the :class:`~kivy.core.window.Window`. `resizable`: int, one of 0 or 1 If 0, the window will have a fixed size. If 1, the window will be resizable. `rotation`: int, one of 0, 90, 180 or 270 Rotation of the :class:`~kivy.core.window.Window`. `width`: int Width of the :class:`~kivy.core.window.Window`, not used if `fullscreen` is set to `auto`. :input: You can create new input devices using this syntax:: # example of input provider instance yourid = providerid,parameters # example for tuio provider default = tuio,127.0.0.1:3333 mytable = tuio,192.168.0.1:3334 .. seealso:: Check the providers in kivy.input.providers for the syntax to use inside the configuration file. :widgets: `scroll_distance`: int Default value of the :attr:`~kivy.uix.scrollview.ScrollView.scroll_distance` property used by the :class:`~kivy.uix.scrollview.ScrollView` widget. Check the widget documentation for more information. `scroll_friction`: float Default value of the :attr:`~kivy.uix.scrollview.ScrollView.scroll_friction` property used by the :class:`~kivy.uix.scrollview.ScrollView` widget. Check the widget documentation for more information. `scroll_timeout`: int Default value of the :attr:`~kivy.uix.scrollview.ScrollView.scroll_timeout` property used by the :class:`~kivy.uix.scrollview.ScrollView` widget. Check the widget documentation for more information. `scroll_stoptime`: int Default value of the :attr:`~kivy.uix.scrollview.ScrollView.scroll_stoptime` property used by the :class:`~kivy.uix.scrollview.ScrollView` widget. Check the widget documentation for more information. .. deprecated:: 1.7.0 Please use :class:`~kivy.uix.scrollview.ScrollView.effect_cls` instead. `scroll_moves`: int Default value of the :attr:`~kivy.uix.scrollview.ScrollView.scroll_moves` property used by the :class:`~kivy.uix.scrollview.ScrollView` widget. Check the widget documentation for more information. .. deprecated:: 1.7.0 Please use :class:`~kivy.uix.scrollview.ScrollView.effect_cls` instead. :modules: You can activate modules with this syntax:: modulename = Anything after the = will be passed to the module as arguments. Check the specific module's documentation for a list of accepted arguments. .. note:: These options control only the initalization of the app and a restart is required for value changes to take effect. .. versionchanged:: 1.9.0 `borderless` and `window_state` have been added to the graphics section. The `fake` setting of the `fullscreen` option has been deprecated, use the `borderless` option instead. `pause_on_minimize` has been added to the kivy section. .. versionchanged:: 1.8.0 `systemanddock` and `systemandmulti` has been added as possible values for `keyboard_mode` in the kivy section. `exit_on_escape` has been added to the kivy section. .. versionchanged:: 1.2.0 `resizable` has been added to graphics section. .. versionchanged:: 1.1.0 tuio no longer listens by default. Window icons are not copied to user directory anymore. You can still set a new window icon by using the ``window_icon`` config setting. .. versionchanged:: 1.0.8 `scroll_timeout`, `scroll_distance` and `scroll_friction` have been added. `list_friction`, `list_trigger_distance` and `list_friction_bound` have been removed. `keyboard_type` and `keyboard_layout` have been removed from the widget. `keyboard_mode` and `keyboard_layout` have been added to the kivy section. ''' __all__ = ('Config', 'ConfigParser') try: from ConfigParser import ConfigParser as PythonConfigParser except ImportError: from configparser import RawConfigParser as PythonConfigParser from os import environ from os.path import exists from kivy import kivy_config_fn from kivy.logger import Logger, logger_config_update from collections import OrderedDict from kivy.utils import platform from kivy.compat import PY2, string_types from weakref import ref _is_rpi = exists('/opt/vc/include/bcm_host.h') # Version number of current configuration format KIVY_CONFIG_VERSION = 13 Config = None '''Kivy configuration object. Its :attr:`~kivy.config.ConfigParser.name` is `'kivy'` ''' class ConfigParser(PythonConfigParser, object): '''Enhanced ConfigParser class that supports the addition of default sections and default values. By default, the kivy ConfigParser instance, :attr:`~kivy.config.Config`, is given the name `'kivy'` and the ConfigParser instance used by App, :meth:`~kivy.app.App.build_settings`, is given the name `'app'`. :Parameters: `name`: string The name of the instance. See :attr:`name`. Defaults to `''`. ..versionchanged:: 1.9.0 Each ConfigParser can now be named, :attr:`name`. You can get the ConfigParser associated with a name using :meth:`get_configparser`. In addition, you can now control the config values with :class:`~kivy.properties.ConfigParserProperty`. .. versionadded:: 1.0.7 ''' def __init__(self, name=''): PythonConfigParser.__init__(self) self._sections = OrderedDict() self.filename = None self._callbacks = [] self.name = name def add_callback(self, callback, section=None, key=None): '''Add a callback to be called when a specific section/key changed. If you don't specify a section or a key, it will call the callback for all section/keys changes. Callbacks will receive 3 arguments: the section, key and value. .. versionadded:: 1.4.1 ''' if section is None and key is not None: raise Exception('You cannot specify a key without a section') self._callbacks.append((callback, section, key)) def remove_callback(self, callback, section=None, key=None): '''Removes a callback added with :meth:`add_callback`. :meth:`remove_callback` must be called with the same parameters as :meth:`add_callback`. Raises a `ValueError` if not found. .. versionadded:: 1.9.0 ''' self._callbacks.remove((callback, section, key)) def _do_callbacks(self, section, key, value): for callback, csection, ckey in self._callbacks: if csection is not None and csection != section: continue elif ckey is not None and ckey != key: continue callback(section, key, value) def read(self, filename): '''Read only one filename. In contrast to the original ConfigParser of Python, this one is able to read only one file at a time. The last read file will be used for the :meth:`write` method. .. versionchanged:: 1.9.0 :meth:`read` now calls the callbacks if read changed any values. ''' if not isinstance(filename, string_types): raise Exception('Only one filename is accepted ({})'.format( string_types.__name__)) self.filename = filename # If we try to open directly the configuration file in utf-8, # we correctly get the unicode value by default. # But, when we try to save it again, all the values we didn't changed # are still unicode, and then the PythonConfigParser internal do # a str() conversion -> fail. # Instead we currently to the conversion to utf-8 when value are # "get()", but we internally store them in ascii. #with codecs.open(filename, 'r', encoding='utf-8') as f: # self.readfp(f) old_vals = {sect: {k: v for k, v in self.items(sect)} for sect in self.sections()} PythonConfigParser.read(self, filename) # when reading new file, sections/keys are only increased, not removed f = self._do_callbacks for section in self.sections(): if section not in old_vals: # new section for k, v in self.items(section): f(section, k, v) continue old_keys = old_vals[section] for k, v in self.items(section): # just update new/changed keys if k not in old_keys or v != old_keys[k]: f(section, k, v) def set(self, section, option, value): '''Functions similarly to PythonConfigParser's set method, except that the value is implicitly converted to a string. ''' e_value = value if not isinstance(value, string_types): # might be boolean, int, etc. e_value = str(value) if PY2: if isinstance(value, unicode): e_value = value.encode('utf-8') ret = PythonConfigParser.set(self, section, option, e_value) self._do_callbacks(section, option, value) return ret def setall(self, section, keyvalues): '''Set a lot of keys/values in one section at the same time. ''' for key, value in keyvalues.items(): self.set(section, key, value) def get(self, section, option, **kwargs): value = PythonConfigParser.get(self, section, option, **kwargs) if PY2: if type(value) is str: return value.decode('utf-8') return value def setdefaults(self, section, keyvalues): '''Set a lot of keys/value defaults in one section at the same time. ''' self.adddefaultsection(section) for key, value in keyvalues.items(): self.setdefault(section, key, value) def setdefault(self, section, option, value): '''Set the default value of a particular option. ''' if self.has_option(section, option): return self.set(section, option, value) def getdefault(self, section, option, defaultvalue): '''Get an option. If not found, it will return the default value. ''' if not self.has_section(section): return defaultvalue if not self.has_option(section, option): return defaultvalue return self.get(section, option) def getdefaultint(self, section, option, defaultvalue): '''Get an option. If not found, it will return the default value. The return value will be always converted as an integer. .. versionadded:: 1.6.0 ''' return int(self.getdefault(section, option, defaultvalue)) def adddefaultsection(self, section): '''Add a section if the section is missing. ''' if self.has_section(section): return self.add_section(section) def write(self): '''Write the configuration to the last file opened using the :meth:`read` method. Return True if the write finished successfully. ''' if self.filename is None: return False try: with open(self.filename, 'w') as fd: PythonConfigParser.write(self, fd) except IOError: Logger.exception('Unable to write the config <%s>' % self.filename) return False return True def update_config(self, filename, overwrite=False): '''Upgrade the configuration based on a new default config file. Overwrite any existing values if overwrite is True. ''' pcp = PythonConfigParser() pcp.read(filename) confset = self.setall if overwrite else self.setdefaults for section in pcp.sections(): confset(section, dict(pcp.items(section))) self.write() @staticmethod def _register_named_property(name, widget_ref, *largs): ''' Called by the ConfigParserProperty to register a property which was created with a config name instead of a config object. When a ConfigParser with this name is later created, the properties are then notified that this parser now exists so they can use it. If the parser already exists, the property is notified here. See :meth:`~kivy.properties.ConfigParserProperty.set_config`. :Parameters: `name`: a non-empty string The name of the ConfigParser that is associated with the property. See :attr:`name`. `widget_ref`: 2-tuple. The first element is a reference to the widget containing the property, the second element is the name of the property. E.g.: class House(Widget): address = ConfigParserProperty('', 'info', 'street', 'directory') Then, the first element is a ref to a House instance, and the second is `'address'`. ''' configs = ConfigParser._named_configs try: config, props = configs[name] except KeyError: configs[name] = (None, [widget_ref]) return props.append(widget_ref) if config: config = config() widget = widget_ref[0]() if config and widget: # associate this config with property widget.property(widget_ref[1]).set_config(config) @staticmethod def get_configparser(name): '''Returns the :class:`ConfigParser` instance whose name is `name`, or None if not found. :Parameters: `name`: string The name of the :class:`ConfigParser` instance to return. ''' try: config = ConfigParser._named_configs[name][0] return config() if config else None except KeyError: return None # keys are configparser names, values are 2-tuple of (ref(configparser), # widget_ref), where widget_ref is same as in _register_named_property _named_configs = {} _name = '' @property def name(self): ''' The name associated with this ConfigParser instance, if not `''`. Defaults to `''`. It can be safely dynamically changed or set to `''`. When a ConfigParser is given a name, that config object can be retrieved using :meth:`get_configparser`. In addition, that config instance can also be used with a :class:`~kivy.properties.ConfigParserProperty` instance that set its `config` value to this name. Setting more than one ConfigParser with the same name will raise a `ValueError`. ''' return self._name @name.setter def name(self, value): old_name = self._name if value is old_name: return self._name = value configs = ConfigParser._named_configs if old_name: # disconnect this parser from previously connected props _, props = configs.get(old_name, (None, [])) for widget, prop in props: widget = widget() if widget: widget.property(prop).set_config(None) configs[old_name] = (None, props) if not value: return # if given new name, connect it with property that used this name try: config, props = configs[value] except KeyError: configs[value] = (ref(self), []) return if config is not None: raise ValueError('A parser named {} already exists'.format(value)) for widget, prop in props: widget = widget() if widget: widget.property(prop).set_config(self) configs[value] = (ref(self), props) if not environ.get('KIVY_DOC_INCLUDE'): # # Read, analyse configuration file # Support upgrade of older config file versions # # Create default configuration Config = ConfigParser(name='kivy') Config.add_callback(logger_config_update, 'kivy', 'log_level') # Read config file if exist if (exists(kivy_config_fn) and 'KIVY_USE_DEFAULTCONFIG' not in environ and 'KIVY_NO_CONFIG' not in environ): try: Config.read(kivy_config_fn) except Exception as e: Logger.exception('Core: error while reading local' 'configuration') version = Config.getdefaultint('kivy', 'config_version', 0) # Add defaults section Config.adddefaultsection('kivy') Config.adddefaultsection('graphics') Config.adddefaultsection('input') Config.adddefaultsection('postproc') Config.adddefaultsection('widgets') Config.adddefaultsection('modules') # Upgrade default configuration until we have the current version need_save = False if version != KIVY_CONFIG_VERSION and 'KIVY_NO_CONFIG' not in environ: Logger.warning('Config: Older configuration version detected' ' ({0} instead of {1})'.format( version, KIVY_CONFIG_VERSION)) Logger.warning('Config: Upgrading configuration in progress.') need_save = True while version < KIVY_CONFIG_VERSION: Logger.debug('Config: Upgrading from %d to %d' % (version, version + 1)) if version == 0: # log level Config.setdefault('kivy', 'keyboard_repeat_delay', '300') Config.setdefault('kivy', 'keyboard_repeat_rate', '30') Config.setdefault('kivy', 'log_dir', 'logs') Config.setdefault('kivy', 'log_enable', '1') Config.setdefault('kivy', 'log_level', 'info') Config.setdefault('kivy', 'log_name', 'kivy_%y-%m-%d_%_.txt') Config.setdefault('kivy', 'window_icon', '') # default graphics parameters Config.setdefault('graphics', 'display', '-1') Config.setdefault('graphics', 'fullscreen', 'no') Config.setdefault('graphics', 'height', '600') Config.setdefault('graphics', 'left', '0') Config.setdefault('graphics', 'maxfps', '0') Config.setdefault('graphics', 'multisamples', '2') Config.setdefault('graphics', 'position', 'auto') Config.setdefault('graphics', 'rotation', '0') Config.setdefault('graphics', 'show_cursor', '1') Config.setdefault('graphics', 'top', '0') Config.setdefault('graphics', 'vsync', '1') Config.setdefault('graphics', 'width', '800') # input configuration Config.setdefault('input', 'mouse', 'mouse') # activate native input provider in configuration # from 1.0.9, don't activate mactouch by default, or app are # unusable. if platform == 'win': Config.setdefault('input', 'wm_touch', 'wm_touch') Config.setdefault('input', 'wm_pen', 'wm_pen') elif platform == 'linux': probesysfs = 'probesysfs' if _is_rpi: probesysfs += ',provider=hidinput' Config.setdefault('input', '%(name)s', probesysfs) # input postprocessing configuration Config.setdefault('postproc', 'double_tap_distance', '20') Config.setdefault('postproc', 'double_tap_time', '250') Config.setdefault('postproc', 'ignore', '[]') Config.setdefault('postproc', 'jitter_distance', '0') Config.setdefault('postproc', 'jitter_ignore_devices', 'mouse,mactouch,') Config.setdefault('postproc', 'retain_distance', '50') Config.setdefault('postproc', 'retain_time', '0') # default configuration for keyboard repeatition Config.setdefault('widgets', 'keyboard_layout', 'qwerty') Config.setdefault('widgets', 'keyboard_type', '') Config.setdefault('widgets', 'list_friction', '10') Config.setdefault('widgets', 'list_friction_bound', '20') Config.setdefault('widgets', 'list_trigger_distance', '5') elif version == 1: Config.remove_option('graphics', 'vsync') Config.set('graphics', 'maxfps', '60') elif version == 2: # was a version to automatically copy windows icon in the user # directory, but it's now not used anymore. User can still change # the window icon by touching the config. pass elif version == 3: # add token for scrollview Config.setdefault('widgets', 'scroll_timeout', '55') Config.setdefault('widgets', 'scroll_distance', '20') Config.setdefault('widgets', 'scroll_friction', '1.') # remove old list_* token Config.remove_option('widgets', 'list_friction') Config.remove_option('widgets', 'list_friction_bound') Config.remove_option('widgets', 'list_trigger_distance') elif version == 4: Config.remove_option('widgets', 'keyboard_type') Config.remove_option('widgets', 'keyboard_layout') # add keyboard token Config.setdefault('kivy', 'keyboard_mode', '') Config.setdefault('kivy', 'keyboard_layout', 'qwerty') elif version == 5: Config.setdefault('graphics', 'resizable', '1') elif version == 6: # if the timeout is still the default value, change it Config.setdefault('widgets', 'scroll_stoptime', '300') Config.setdefault('widgets', 'scroll_moves', '5') elif version == 7: # desktop bool indicating whether to use desktop specific features is_desktop = int(platform in ('win', 'macosx', 'linux')) Config.setdefault('kivy', 'desktop', is_desktop) Config.setdefault('postproc', 'triple_tap_distance', '20') Config.setdefault('postproc', 'triple_tap_time', '375') elif version == 8: if Config.getint('widgets', 'scroll_timeout') == 55: Config.set('widgets', 'scroll_timeout', '250') elif version == 9: Config.setdefault('kivy', 'exit_on_escape', '1') elif version == 10: Config.set('graphics', 'fullscreen', '0') Config.setdefault('graphics', 'borderless', '0') elif version == 11: Config.setdefault('kivy', 'pause_on_minimize', '0') elif version == 12: Config.set('graphics', 'window_state', 'visible') #elif version == 1: # # add here the command for upgrading from configuration 0 to 1 # else: # for future. break # Pass to the next version version += 1 # Indicate to the Config that we've upgrade to the latest version. Config.set('kivy', 'config_version', KIVY_CONFIG_VERSION) # Now, activate log file Logger.logfile_activated = bool(Config.getint('kivy', 'log_enable')) # If no configuration exist, write the default one. if ((not exists(kivy_config_fn) or need_save) and 'KIVY_NO_CONFIG' not in environ): try: Config.filename = kivy_config_fn Config.write() except Exception as e: Logger.exception('Core: Error while saving default config file') ================================================ FILE: tickeys/kivy/context.py ================================================ ''' Context ======= .. versionadded:: 1.8.0 .. warning:: This is experimental and subject to change as long as this warning notice is present. Kivy has a few "global" instances that are used directly by many pieces of the framework: `Cache`, `Builder`, `Clock`. TODO: document this module. ''' __all__ = ('Context', 'ProxyContext', 'register_context', 'get_current_context') _contexts = {} _default_context = None _context_stack = [] class ProxyContext(object): __slots__ = ['_obj'] def __init__(self, obj): object.__init__(self) object.__setattr__(self, '_obj', obj) def __getattribute__(self, name): return getattr(object.__getattribute__(self, '_obj'), name) def __delattr__(self, name): delattr(object.__getattribute__(self, '_obj'), name) def __setattr__(self, name, value): setattr(object.__getattribute__(self, '_obj'), name, value) def __bool__(self): return bool(object.__getattribute__(self, '_obj')) def __str__(self): return str(object.__getattribute__(self, '_obj')) def __repr__(self): return repr(object.__getattribute__(self, '_obj')) class Context(dict): def __init__(self, init=False): dict.__init__(self) self.sandbox = None if not init: return for name in _contexts: context = _contexts[name] instance = context['cls'](*context['args'], **context['kwargs']) self[name] = instance def push(self): _context_stack.append(self) for name, instance in self.items(): object.__setattr__(_contexts[name]['proxy'], '_obj', instance) def pop(self): # After poping context from stack. Update proxy's _obj with # instances in current context _context_stack.pop(-1) for name, instance in get_current_context().items(): object.__setattr__(_contexts[name]['proxy'], '_obj', instance) def register_context(name, cls, *args, **kwargs): '''Register a new context. ''' instance = cls(*args, **kwargs) proxy = ProxyContext(instance) _contexts[name] = { 'cls': cls, 'args': args, 'kwargs': kwargs, 'proxy': proxy} _default_context[name] = instance return proxy def get_current_context(): '''Return the current context. ''' if not _context_stack: return _default_context return _context_stack[-1] _default_context = Context(init=False) ================================================ FILE: tickeys/kivy/core/__init__.py ================================================ ''' Core Abstraction ================ This module defines the abstraction layers for our core providers and their implementations. For further information, please refer to :ref:`architecture` and the :ref:`providers` section of the documentation. In most cases, you shouldn't directly use a library that's already covered by the core abstraction. Always try to use our providers first. In case we are missing a feature or method, please let us know by opening a new Bug report instead of relying on your library. .. warning:: These are **not** widgets! These are just abstractions of the respective functionality. For example, you cannot add a core image to your window. You have to use the image **widget** class instead. If you're really looking for widgets, please refer to :mod:`kivy.uix` instead. ''' import os import sys import traceback import kivy from kivy.logger import Logger class CoreCriticalException(Exception): pass def core_select_lib(category, llist, create_instance=False, base='kivy.core'): if 'KIVY_DOC' in os.environ: return category = category.lower() libs_ignored = [] errs = [] for option, modulename, classname in llist: try: # module activated in config ? try: if option not in kivy.kivy_options[category]: libs_ignored.append(modulename) Logger.debug( '{0}: Provider <{1}> ignored by config'.format( category.capitalize(), option)) continue except KeyError: pass # import module mod = __import__(name='{2}.{0}.{1}'.format( category, modulename, base), globals=globals(), locals=locals(), fromlist=[modulename], level=0) cls = mod.__getattribute__(classname) # ok ! Logger.info('{0}: Provider: {1}{2}'.format( category.capitalize(), option, '({0} ignored)'.format(libs_ignored) if libs_ignored else '')) if create_instance: cls = cls() return cls except ImportError as e: errs.append((option, e, sys.exc_info()[2])) libs_ignored.append(modulename) Logger.debug('{0}: Ignored <{1}> (import error)'.format( category.capitalize(), option)) Logger.trace('', exc_info=e) except CoreCriticalException as e: errs.append((option, e, sys.exc_info()[2])) Logger.error('{0}: Unable to use {1}'.format( category.capitalize(), option)) Logger.error( '{0}: The module raised an important error: {1!r}'.format( category.capitalize(), e.message)) raise except Exception as e: errs.append((option, e, sys.exc_info()[2])) libs_ignored.append(modulename) Logger.trace('{0}: Unable to use {1}'.format( category.capitalize(), option, category)) Logger.trace('', exc_info=e) err = '\n'.join(['{} - {}: {}\n{}'.format(opt, e.__class__.__name__, e, ''.join(traceback.format_tb(tb))) for opt, e, tb in errs]) Logger.critical( '{0}: Unable to find any valuable {0} provider at all!\n{1}'.format( category.capitalize(), err)) def core_register_libs(category, libs, base='kivy.core'): if 'KIVY_DOC' in os.environ: return category = category.lower() kivy_options = kivy.kivy_options[category] libs_loadable = {} libs_ignored = [] for option, lib in libs: # module activated in config ? if option not in kivy_options: Logger.debug('{0}: option <{1}> ignored by config'.format( category.capitalize(), option)) libs_ignored.append(lib) continue libs_loadable[option] = lib libs_loaded = [] for item in kivy_options: try: # import module try: lib = libs_loadable[item] except KeyError: continue __import__(name='{2}.{0}.{1}'.format(category, lib, base), globals=globals(), locals=locals(), fromlist=[lib], level=0) libs_loaded.append(lib) except Exception as e: Logger.trace('{0}: Unable to use <{1}> as loader!'.format( category.capitalize(), option)) Logger.trace('', exc_info=e) libs_ignored.append(lib) Logger.info('{0}: Providers: {1} {2}'.format( category.capitalize(), ', '.join(libs_loaded), '({0} ignored)'.format( ', '.join(libs_ignored)) if libs_ignored else '')) return libs_loaded ================================================ FILE: tickeys/kivy/core/audio/__init__.py ================================================ ''' Audio ===== Load an audio sound and play it with:: from kivy.core.audio import SoundLoader sound = SoundLoader.load('mytest.wav') if sound: print("Sound found at %s" % sound.source) print("Sound is %.3f seconds" % sound.length) sound.play() You should not use the Sound class directly. The class returned by **SoundLoader.load** will be the best sound provider for that particular file type, so it might return different Sound classes depending the file type. .. versionchanged:: 1.8.0 There are now 2 distinct Gstreamer implementations: one using Gi/Gst working for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for Python 2 + Gstreamer 0.10. If you have issue with GStreamer, have a look at :ref:`gstreamer-compatibility` .. note:: The core audio library does not support recording audio. If you require this functionality, please refer to the `audiostream `_ extension. ''' __all__ = ('Sound', 'SoundLoader') from kivy.logger import Logger from kivy.event import EventDispatcher from kivy.core import core_register_libs from kivy.compat import PY2 from kivy.resources import resource_find from kivy.properties import StringProperty, NumericProperty, OptionProperty, \ AliasProperty, BooleanProperty from kivy.setupconfig import USE_SDL2 class SoundLoader: '''Load a sound, using the best loader for the given file type. ''' _classes = [] @staticmethod def register(classobj): '''Register a new class to load the sound.''' Logger.debug('Audio: register %s' % classobj.__name__) SoundLoader._classes.append(classobj) @staticmethod def load(filename): '''Load a sound, and return a Sound() instance.''' rfn = resource_find(filename) if rfn is not None: filename = rfn ext = filename.split('.')[-1].lower() if '?' in ext: ext = ext.split('?')[0] for classobj in SoundLoader._classes: if ext in classobj.extensions(): return classobj(source=filename) Logger.warning('Audio: Unable to find a loader for <%s>' % filename) return None class Sound(EventDispatcher): '''Represents a sound to play. This class is abstract, and cannot be used directly. Use SoundLoader to load a sound. :Events: `on_play` : None Fired when the sound is played. `on_stop` : None Fired when the sound is stopped. ''' source = StringProperty(None) '''Filename / source of your audio file. .. versionadded:: 1.3.0 :attr:`source` is a :class:`~kivy.properties.StringProperty` that defaults to None and is read-only. Use the :meth:`SoundLoader.load` for loading audio. ''' volume = NumericProperty(1.) '''Volume, in the range 0-1. 1 means full volume, 0 means mute. .. versionadded:: 1.3.0 :attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' state = OptionProperty('stop', options=('stop', 'play')) '''State of the sound, one of 'stop' or 'play'. .. versionadded:: 1.3.0 :attr:`state` is a read-only :class:`~kivy.properties.OptionProperty`.''' loop = BooleanProperty(False) '''Set to True if the sound should automatically loop when it finishes. .. versionadded:: 1.8.0 :attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and defaults to False.''' # # deprecated # def _get_status(self): return self.state status = AliasProperty(_get_status, None, bind=('state', )) ''' .. deprecated:: 1.3.0 Use :attr:`state` instead. ''' def _get_filename(self): return self.source filename = AliasProperty(_get_filename, None, bind=('source', )) ''' .. deprecated:: 1.3.0 Use :attr:`source` instead. ''' __events__ = ('on_play', 'on_stop') def on_source(self, instance, filename): self.unload() if filename is None: return self.load() def get_pos(self): ''' Returns the current position of the audio file. Returns 0 if not playing. .. versionadded:: 1.4.1 ''' return 0 def _get_length(self): return 0 length = property(lambda self: self._get_length(), doc='Get length of the sound (in seconds).') def load(self): '''Load the file into memory.''' pass def unload(self): '''Unload the file from memory.''' pass def play(self): '''Play the file.''' self.state = 'play' self.dispatch('on_play') def stop(self): '''Stop playback.''' self.state = 'stop' self.dispatch('on_stop') def seek(self, position): '''Go to the (in seconds).''' pass def on_play(self): pass def on_stop(self): pass # Little trick here, don't activate gstreamer on window # seem to have lot of crackle or something... audio_libs = [] # from now on, prefer our gstplayer instead of gi/pygst. try: from kivy.lib.gstplayer import GstPlayer # NOQA audio_libs += [('gstplayer', 'audio_gstplayer')] except ImportError: #audio_libs += [('gi', 'audio_gi')] if PY2: audio_libs += [('pygst', 'audio_pygst')] audio_libs += [('ffpyplayer', 'audio_ffpyplayer')] if USE_SDL2: audio_libs += [('sdl2', 'audio_sdl2')] else: audio_libs += [('pygame', 'audio_pygame')] core_register_libs('audio', audio_libs) ================================================ FILE: tickeys/kivy/core/audio/audio_ffpyplayer.py ================================================ ''' FFmpeg based audio player ========================= To use, you need to install ffpyplyaer and have a compiled ffmpeg shared library. https://github.com/matham/ffpyplayer The docs there describe how to set this up. But briefly, first you need to compile ffmpeg using the shared flags while disabling the static flags (you'll probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/. Similarly, you should download SDL. Now, you should a ffmpeg and sdl directory. In each, you should have a include, bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files, while bin contains the actual dlls. The include directory holds the headers. The bin directory is only needed if the shared libraries are not already on the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to the ffmpeg, and SDL directories, respectively. (If you're using SDL2, the include directory will contain a directory called SDL2, which then holds the headers). Once defined, download the ffpyplayer git and run python setup.py build_ext --inplace Finally, before running you need to ensure that ffpyplayer is in python's path. ..Note:: When kivy exits by closing the window while the audio is playing, it appears that the __del__method of SoundFFPy is not called. Because of this the SoundFFPy object is not properly deleted when kivy exits. The consequence is that because MediaPlayer creates internal threads which do not have their daemon flag set, when the main threads exists it'll hang and wait for the other MediaPlayer threads to exit. But since __del__ is not called to delete the MediaPlayer object, those threads will remain alive hanging kivy. What this means is that you have to be sure to delete the MediaPlayer object before kivy exits by setting it to None. ''' __all__ = ('SoundFFPy', ) try: import ffpyplayer from ffpyplayer.player import MediaPlayer from ffpyplayer.tools import set_log_callback, loglevels,\ get_log_callback, formats_in except: raise from kivy.clock import Clock from kivy.logger import Logger from kivy.core.audio import Sound, SoundLoader from kivy.weakmethod import WeakMethod import time Logger.info('SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.version)) logger_func = {'quiet': Logger.critical, 'panic': Logger.critical, 'fatal': Logger.critical, 'error': Logger.error, 'warning': Logger.warning, 'info': Logger.info, 'verbose': Logger.debug, 'debug': Logger.debug} def _log_callback(message, level): message = message.strip() if message: logger_func[level]('ffpyplayer: {}'.format(message)) class SoundFFPy(Sound): @staticmethod def extensions(): return formats_in def __init__(self, **kwargs): self._ffplayer = None self.quitted = False self._log_callback_set = False self._state = '' self.state = 'stop' self._callback_ref = WeakMethod(self._player_callback) if not get_log_callback(): set_log_callback(_log_callback) self._log_callback_set = True super(SoundFFPy, self).__init__(**kwargs) def __del__(self): self.unload() if self._log_callback_set: set_log_callback(None) def _player_callback(self, selector, value): if self._ffplayer is None: return if selector == 'quit': def close(*args): self.quitted = True self.unload() Clock.schedule_once(close, 0) elif selector == 'eof': Clock.schedule_once(self._do_eos, 0) def load(self): self.unload() ff_opts = {'vn': True, 'sn': True} # only audio self._ffplayer = MediaPlayer(self.source, callback=self._callback_ref, loglevel='info', ff_opts=ff_opts) player = self._ffplayer player.set_volume(self.volume) player.toggle_pause() self._state = 'paused' # wait until loaded or failed, shouldn't take long, but just to make # sure metadata is available. s = time.clock() while ((not player.get_metadata()['duration']) and not self.quitted and time.clock() - s < 10.): time.sleep(0.005) def unload(self): if self._ffplayer: self._ffplayer = None self._state = '' self.state = 'stop' self.quitted = False def play(self): if self._state == 'playing': super(SoundFFPy, self).play() return if not self._ffplayer: self.load() self._ffplayer.toggle_pause() self._state = 'playing' self.state = 'play' super(SoundFFPy, self).play() def stop(self): if self._ffplayer and self._state == 'playing': self._ffplayer.toggle_pause() self._state = 'paused' self.state = 'stop' super(SoundFFPy, self).stop() def seek(self, position): if self._ffplayer is None: return self._ffplayer.seek(position, relative=False) def get_pos(self): if self._ffplayer is not None: return self._ffplayer.get_pts() return 0 def on_volume(self, instance, volume): if self._ffplayer is not None: self._ffplayer.set_volume(volume) def _get_length(self): if self._ffplayer is None: return super(SoundFFPy, self)._get_length() return self._ffplayer.get_metadata()['duration'] def _do_eos(self, *args): if not self.loop: self.stop() else: self.seek(0.) SoundLoader.register(SoundFFPy) ================================================ FILE: tickeys/kivy/core/audio/audio_gi.py ================================================ ''' Audio Gi ======== Implementation of Sound with Gi. Gi is both compatible with Python 2 and 3. ''' from gi.repository import Gst from kivy.core.audio import Sound, SoundLoader from kivy.logger import Logger from kivy.support import install_gobject_iteration import os import sys # initialize the audio/gi. if the older version is used, don't use audio_gi. Gst.init(None) version = Gst.version() if version < (1, 0, 0, 0): raise Exception('Cannot use audio_gi, Gstreamer < 1.0 is not supported.') Logger.info('AudioGi: Using Gstreamer {}'.format( '.'.join(['{}'.format(x) for x in Gst.version()]))) install_gobject_iteration() class SoundGi(Sound): @staticmethod def extensions(): return ('wav', 'ogg', 'mp3', ) def __init__(self, **kwargs): self._data = None super(SoundGi, self).__init__(**kwargs) def __del__(self): if self._data is not None: self._data.set_state(Gst.State.NULL) def _on_gst_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self._data.set_state(Gst.State.NULL) if self.loop: self.play() else: self.stop() elif t == Gst.MessageType.ERROR: self._data.set_state(Gst.State.NULL) err, debug = message.parse_error() Logger.error('AudioGi: %s' % err) Logger.debug(str(debug)) self.stop() def play(self): if not self._data: return self._data.props.volume = self.volume self._data.set_state(Gst.State.PLAYING) super(SoundGi, self).play() def stop(self): if not self._data: return self._data.set_state(Gst.State.NULL) super(SoundGi, self).stop() def load(self): self.unload() fn = self.filename if fn is None: return slash = '' if sys.platform in ('win32', 'cygwin'): slash = '/' if fn[0] == '/': uri = 'file://' + slash + fn else: uri = 'file://' + slash + os.path.join(os.getcwd(), fn) self._data = Gst.ElementFactory.make('playbin', '') fakesink = Gst.ElementFactory.make('fakesink', '') self._data.props.video_sink = fakesink bus = self._data.get_bus() bus.add_signal_watch() bus.connect('message', self._on_gst_message) self._data.props.uri = uri self._data.set_state(Gst.State.READY) def unload(self): self.stop() self._data = None def seek(self, position): if self._data is None: return self._data.seek_simple( Gst.Format.TIME, Gst.SeekFlags.SKIP, position * Gst.SECOND) def get_pos(self): if self._data is not None: if self._data.get_state()[1] == Gst.State.PLAYING: try: ret, value = self._data.query_position(Gst.Format.TIME) if ret: return value / float(Gst.SECOND) except: pass return 0 def on_volume(self, instance, volume): if self._data is not None: self._data.set_property('volume', volume) def _get_length(self): if self._data is not None: if self._data.get_state()[1] != Gst.State.PLAYING: volume_before = self._data.get_property('volume') self._data.set_property('volume', 0) self._data.set_state(Gst.State.PLAYING) try: self._data.get_state() ret, value = self._data.query_duration(Gst.Format.TIME) if ret: return value / float(Gst.SECOND) finally: self._data.set_state(Gst.State.NULL) self._data.set_property('volume', volume_before) else: ret, value = self._data.query_duration(Gst.Format.TIME) if ret: return value / float(Gst.SECOND) return super(SoundGi, self)._get_length() SoundLoader.register(SoundGi) ================================================ FILE: tickeys/kivy/core/audio/audio_gstplayer.py ================================================ ''' Audio Gstplayer =============== .. versionadded:: 1.8.0 Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer` This player is the prefered player, using Gstreamer 1.0, working on both Python 2 and 3. ''' from kivy.lib.gstplayer import GstPlayer, get_gst_version from kivy.core.audio import Sound, SoundLoader from kivy.logger import Logger from kivy.compat import PY2 from kivy.clock import Clock from os.path import realpath if PY2: from urllib import pathname2url else: from urllib.request import pathname2url Logger.info('AudioGstplayer: Using Gstreamer {}'.format( '.'.join(map(str, get_gst_version())))) def _on_gstplayer_message(mtype, message): if mtype == 'error': Logger.error('AudioGstplayer: {}'.format(message)) elif mtype == 'warning': Logger.warning('AudioGstplayer: {}'.format(message)) elif mtype == 'info': Logger.info('AudioGstplayer: {}'.format(message)) class SoundGstplayer(Sound): @staticmethod def extensions(): return ('wav', 'ogg', 'mp3', 'm4a') def __init__(self, **kwargs): self.player = None super(SoundGstplayer, self).__init__(**kwargs) def _on_gst_eos_sync(self): Clock.schedule_once(self._on_gst_eos, 0) def _on_gst_eos(self, *dt): if self.loop: self.player.stop() self.player.play() else: self.stop() def load(self): self.unload() uri = self._get_uri() self.player = GstPlayer(uri, None, self._on_gst_eos_sync, _on_gstplayer_message) self.player.load() def play(self): # we need to set the volume everytime, it seems that stopping + playing # the sound reset the volume. self.player.set_volume(self.volume) self.player.play() super(SoundGstplayer, self).play() def stop(self): self.player.stop() super(SoundGstplayer, self).stop() def unload(self): if self.player: self.player.unload() self.player = None def seek(self, position): self.player.seek(position / self.length) def get_pos(self): return self.player.get_position() def _get_length(self): return self.player.get_duration() def on_volume(self, instance, volume): self.player.set_volume(volume) def _get_uri(self): uri = self.filename if not uri: return if not '://' in uri: uri = 'file:' + pathname2url(realpath(uri)) return uri SoundLoader.register(SoundGstplayer) ================================================ FILE: tickeys/kivy/core/audio/audio_pygame.py ================================================ ''' AudioPygame: implementation of Sound with Pygame ''' __all__ = ('SoundPygame', ) from kivy.clock import Clock from kivy.utils import platform from kivy.core.audio import Sound, SoundLoader _platform = platform try: if _platform == 'android': try: import android.mixer as mixer except ImportError: # old python-for-android version import android_mixer as mixer else: from pygame import mixer except: raise # init pygame sound mixer.pre_init(44100, -16, 2, 1024) mixer.init() mixer.set_num_channels(32) class SoundPygame(Sound): # XXX we don't set __slots__ here, to automaticly add # a dictionary. We need that to be able to use weakref for # SoundPygame object. Otherwise, it failed with: # TypeError: cannot create weak reference to 'SoundPygame' object # We use our clock in play() method. # __slots__ = ('_data', '_channel') @staticmethod def extensions(): if _platform == 'android': return ('wav', 'ogg', 'mp3', 'm4a') return ('wav', 'ogg') def __init__(self, **kwargs): self._data = None self._channel = None super(SoundPygame, self).__init__(**kwargs) def _check_play(self, dt): if self._channel is None: return False if self._channel.get_busy(): return if self.loop: def do_loop(dt): self.play() Clock.schedule_once(do_loop) else: self.stop() return False def play(self): if not self._data: return self._data.set_volume(self.volume) self._channel = self._data.play() self.start_time = Clock.time() # schedule event to check if the sound is still playing or not Clock.schedule_interval(self._check_play, 0.1) super(SoundPygame, self).play() def stop(self): if not self._data: return self._data.stop() # ensure we don't have anymore the callback Clock.unschedule(self._check_play) self._channel = None super(SoundPygame, self).stop() def load(self): self.unload() if self.filename is None: return self._data = mixer.Sound(self.filename) def unload(self): self.stop() self._data = None def seek(self, position): if not self._data: return if _platform == 'android' and self._channel: self._channel.seek(position) def get_pos(self): if self._data is not None and self._channel: if _platform == 'android': return self._channel.get_pos() return Clock.time() - self.start_time return 0 def on_volume(self, instance, volume): if self._data is not None: self._data.set_volume(volume) def _get_length(self): if _platform == 'android' and self._channel: return self._channel.get_length() if self._data is not None: return self._data.get_length() return super(SoundPygame, self)._get_length() SoundLoader.register(SoundPygame) ================================================ FILE: tickeys/kivy/core/audio/audio_pygst.py ================================================ ''' Audio Gstreamer =============== Implementation of Sound with GStreamer ''' try: import gi # NOQA except ImportError: gi_found = False else: raise Exception('Avoiding PyGST, Gi is better.') try: import pygst if not hasattr(pygst, '_gst_already_checked'): pygst.require('0.10') pygst._gst_already_checked = True import gst except: raise from kivy.core.audio import Sound, SoundLoader import os import sys from kivy.logger import Logger # install the gobject iteration from kivy.support import install_gobject_iteration install_gobject_iteration() class SoundPyGst(Sound): @staticmethod def extensions(): return ('wav', 'ogg', 'mp3', ) def __init__(self, **kwargs): self._data = None super(SoundPyGst, self).__init__(**kwargs) def __del__(self): if self._data is not None: self._data.set_state(gst.STATE_NULL) def _on_gst_message(self, bus, message): t = message.type if t == gst.MESSAGE_EOS: self._data.set_state(gst.STATE_NULL) if self.loop: self.play() else: self.stop() elif t == gst.MESSAGE_ERROR: self._data.set_state(gst.STATE_NULL) err, debug = message.parse_error() Logger.error('AudioPyGst: %s' % err) Logger.debug(str(debug)) self.stop() def play(self): if not self._data: return self._data.set_property('volume', self.volume) self._data.set_state(gst.STATE_PLAYING) super(SoundPyGst, self).play() def stop(self): if not self._data: return self._data.set_state(gst.STATE_NULL) super(SoundPyGst, self).stop() def load(self): self.unload() fn = self.filename if fn is None: return slash = '' if sys.platform in ('win32', 'cygwin'): slash = '/' if fn[0] == '/': filepath = 'file://' + slash + fn else: filepath = 'file://' + slash + os.path.join(os.getcwd(), fn) self._data = gst.element_factory_make('playbin2', 'player') fakesink = gst.element_factory_make('fakesink', 'fakesink') self._data.set_property('video-sink', fakesink) bus = self._data.get_bus() bus.add_signal_watch() bus.connect('message', self._on_gst_message) self._data.set_property('uri', filepath) self._data.set_state(gst.STATE_READY) def unload(self): self.stop() self._data = None def seek(self, position): if self._data is None: return self._data.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_SKIP, position * 1000000000.) def get_pos(self): if self._data is not None: if self._data.get_state()[1] == gst.STATE_PLAYING: try: return self._data.query_position( gst.Format(gst.FORMAT_TIME))[0] / 1000000000. except: pass return 0 def on_volume(self, instance, volume): if self._data is not None: self._data.set_property('volume', volume) def _get_length(self): if self._data is not None: if self._data.get_state()[1] != gst.STATE_PLAYING: volume_before = self._data.get_property('volume') self._data.set_property('volume', 0) self._data.set_state(gst.STATE_PLAYING) try: self._data.get_state() return self._data.query_duration(gst.Format( gst.FORMAT_TIME))[0] / 1000000000. finally: self._data.set_state(gst.STATE_NULL) self._data.set_property('volume', volume_before) else: return self._data.query_duration( gst.Format(gst.FORMAT_TIME))[0] / 1000000000. return super(SoundPyGst, self)._get_length() SoundLoader.register(SoundPyGst) ================================================ FILE: tickeys/kivy/core/camera/__init__.py ================================================ ''' Camera ====== Core class for acquiring the camera and converting its input into a :class:`~kivy.graphics.texture.Texture`. .. versionchanged:: 1.8.0 There is now 2 distinct Gstreamer implementation: one using Gi/Gst working for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for Python 2 + Gstreamer 0.10. If you have issue with GStreamer, have a look at :ref:`gstreamer-compatibility` ''' __all__ = ('CameraBase', 'Camera') import sys from kivy.event import EventDispatcher from kivy.logger import Logger from kivy.core import core_select_lib class CameraBase(EventDispatcher): '''Abstract Camera Widget class. Concrete camera classes must implement initialization and frame capturing to a buffer that can be uploaded to the gpu. :Parameters: `index`: int Source index of the camera. `size` : tuple (int, int) Size at which the image is drawn. If no size is specified, it defaults to the resolution of the camera image. `resolution` : tuple (int, int) Resolution to try to request from the camera. Used in the gstreamer pipeline by forcing the appsink caps to this resolution. If the camera doesnt support the resolution, a negotiation error might be thrown. :Events: `on_load` Fired when the camera is loaded and the texture has become available. `on_frame` Fired each time the camera texture is updated. ''' __events__ = ('on_load', 'on_texture') def __init__(self, **kwargs): kwargs.setdefault('stopped', False) kwargs.setdefault('resolution', (640, 480)) kwargs.setdefault('index', 0) self.stopped = kwargs.get('stopped') self._resolution = kwargs.get('resolution') self._index = kwargs.get('index') self._buffer = None self._format = 'rgb' self._texture = None self.capture_device = None kwargs.setdefault('size', self._resolution) super(CameraBase, self).__init__() self.init_camera() if not self.stopped: self.start() def _set_resolution(self, res): self._resolution = res self.init_camera() def _get_resolution(self): return self._resolution resolution = property(lambda self: self._get_resolution(), lambda self, x: self._set_resolution(x), doc='Resolution of camera capture (width, height)') def _set_index(self, x): if x == self._index: return self._index = x self.init_camera() def _get_index(self): return self._x index = property(lambda self: self._get_index(), lambda self, x: self._set_index(x), doc='Source index of the camera') def _get_texture(self): return self._texture texture = property(lambda self: self._get_texture(), doc='Return the camera texture with the latest capture') def init_camera(self): '''Initialise the camera (internal)''' pass def start(self): '''Start the camera acquire''' self.stopped = False def stop(self): '''Release the camera''' self.stopped = True def _update(self, dt): '''Update the camera (internal)''' pass def _copy_to_gpu(self): '''Copy the the buffer into the texture''' if self._texture is None: Logger.debug('Camera: copy_to_gpu() failed, _texture is None !') return self._texture.blit_buffer(self._buffer, colorfmt=self._format) self._buffer = None self.dispatch('on_texture') def on_texture(self): pass def on_load(self): pass # Load the appropriate providers providers = () if sys.platform == 'win32': providers += (('videocapture', 'camera_videocapture', 'CameraVideoCapture'), ) elif sys.platform == 'darwin': providers += (('avfoundation', 'camera_avfoundation', 'CameraAVFoundation'), ) else: #providers += (('gi', 'camera_gi', 'CameraGi'), ) providers += (('pygst', 'camera_pygst', 'CameraPyGst'), ) providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), ) Camera = core_select_lib('camera', (providers)) ================================================ FILE: tickeys/kivy/core/camera/camera_gi.py ================================================ ''' Gi Camera ========= Implement CameraBase with Gi / Gstreamer, working on both Python 2 and 3 ''' __all__ = ('CameraGi', ) from gi.repository import Gst from kivy.clock import Clock from kivy.graphics.texture import Texture from kivy.core.camera import CameraBase from kivy.support import install_gobject_iteration from kivy.logger import Logger from ctypes import Structure, c_void_p, c_int, string_at from weakref import ref import atexit # initialize the camera/gi. if the older version is used, don't use camera_gi. Gst.init(None) version = Gst.version() if version < (1, 0, 0, 0): raise Exception('Cannot use camera_gi, Gstreamer < 1.0 is not supported.') Logger.info('CameraGi: Using Gstreamer {}'.format( '.'.join(['{}'.format(x) for x in Gst.version()]))) install_gobject_iteration() class _MapInfo(Structure): _fields_ = [ ('memory', c_void_p), ('flags', c_int), ('data', c_void_p)] # we don't care about the rest def _on_cameragi_unref(obj): if obj in CameraGi._instances: CameraGi._instances.remove(obj) class CameraGi(CameraBase): '''Implementation of CameraBase using GStreamer :Parameters: `video_src` : str, default is 'v4l2src' Other tested options are: 'dc1394src' for firewire dc camera (e.g. firefly MV). Any gstreamer video source should potentially work. Theoretically a longer string using "!" can be used describing the first part of a gstreamer pipeline. ''' _instances = [] def __init__(self, **kwargs): self._pipeline = None self._camerasink = None self._decodebin = None self._texturesize = None self._video_src = kwargs.get('video_src', 'v4l2src') wk = ref(self, _on_cameragi_unref) CameraGi._instances.append(wk) super(CameraGi, self).__init__(**kwargs) def init_camera(self): # TODO: This doesn't work when camera resolution is resized at runtime. # There must be some other way to release the camera? if self._pipeline: self._pipeline = None video_src = self._video_src if video_src == 'v4l2src': video_src += ' device=/dev/video%d' % self._index elif video_src == 'dc1394src': video_src += ' camera-number=%d' % self._index if Gst.version() < (1, 0, 0, 0): caps = ('video/x-raw-rgb,red_mask=(int)0xff0000,' 'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff') pl = ('{} ! decodebin name=decoder ! ffmpegcolorspace ! ' 'appsink name=camerasink emit-signals=True caps={}') else: caps = 'video/x-raw,format=RGB' pl = '{} ! decodebin name=decoder ! videoconvert ! appsink ' + \ 'name=camerasink emit-signals=True caps={}' self._pipeline = Gst.parse_launch(pl.format(video_src, caps)) self._camerasink = self._pipeline.get_by_name('camerasink') self._camerasink.connect('new-sample', self._gst_new_sample) self._decodebin = self._pipeline.get_by_name('decoder') if self._camerasink and not self.stopped: self.start() def _gst_new_sample(self, *largs): sample = self._camerasink.emit('pull-sample') if sample is None: return False self._sample = sample if self._texturesize is None: # try to get the camera image size for pad in self._decodebin.srcpads: s = pad.get_current_caps().get_structure(0) self._texturesize = ( s.get_value('width'), s.get_value('height')) Clock.schedule_once(self._update) return False Clock.schedule_once(self._update) return False def start(self): super(CameraGi, self).start() self._pipeline.set_state(Gst.State.PLAYING) def stop(self): super(CameraGi, self).stop() self._pipeline.set_state(Gst.State.PAUSED) def unload(self): self._pipeline.set_state(Gst.State.NULL) def _update(self, dt): sample, self._sample = self._sample, None if sample is None: return if self._texture is None and self._texturesize is not None: self._texture = Texture.create( size=self._texturesize, colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') # decode sample # read the data from the buffer memory try: buf = sample.get_buffer() result, mapinfo = buf.map(Gst.MapFlags.READ) # We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0 # related bug report: # https://bugzilla.gnome.org/show_bug.cgi?id=6t8663 # ie: mapinfo.data is normally a char*, but here, we have an int # So right now, we use ctypes instead to read the mapinfo ourself. addr = mapinfo.__hash__() c_mapinfo = _MapInfo.from_address(addr) # now get the memory self._buffer = string_at(c_mapinfo.data, mapinfo.size) self._copy_to_gpu() finally: if mapinfo is not None: buf.unmap(mapinfo) @atexit.register def camera_gi_clean(): # if we leave the python process with some video running, we can hit a # segfault. This is forcing the stop/unload of all remaining videos before # exiting the python process. for weakcamera in CameraGi._instances: camera = weakcamera() if isinstance(camera, CameraGi): camera.stop() camera.unload() ================================================ FILE: tickeys/kivy/core/camera/camera_opencv.py ================================================ ''' OpenCV Camera: Implement CameraBase with OpenCV ''' # # TODO: make usage of thread or multiprocess # __all__ = ('CameraOpenCV') from kivy.logger import Logger from kivy.clock import Clock from kivy.graphics.texture import Texture from kivy.core.camera import CameraBase try: import opencv as cv import opencv.highgui as hg except ImportError: import cv class Hg(object): ''' On OSX, not only are the import names different, but the API also differs. There is no module called 'highgui' but the names are directly available in the 'cv' module. Some of them even have a different names. Therefore we use this proxy object. ''' def __getattr__(self, attr): if attr.startswith('cv'): attr = attr[2:] got = getattr(cv, attr) return got hg = Hg() class CameraOpenCV(CameraBase): '''Implementation of CameraBase using OpenCV ''' def __init__(self, **kwargs): self._device = None super(CameraOpenCV, self).__init__(**kwargs) def init_camera(self): # create the device self._device = hg.cvCreateCameraCapture(self._index) # Set preferred resolution cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_WIDTH, self.resolution[0]) cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_HEIGHT, self.resolution[1]) # and get frame to check if it's ok frame = hg.cvQueryFrame(self._device) # Just set the resolution to the frame we just got, but don't use # self.resolution for that as that would cause an infinite recursion # with self.init_camera (but slowly as we'd have to always get a # frame). self._resolution = (int(frame.width), int(frame.height)) #get fps self.fps = cv.GetCaptureProperty(self._device, cv.CV_CAP_PROP_FPS) if self.fps <= 0: self.fps = 1 / 30. if not self.stopped: self.start() def _update(self, dt): if self.stopped: return if self._texture is None: # Create the texture self._texture = Texture.create(self._resolution) self._texture.flip_vertical() self.dispatch('on_load') try: frame = hg.cvQueryFrame(self._device) self._format = 'bgr' try: self._buffer = frame.imageData except AttributeError: # On OSX there is no imageData attribute but a tostring() # method. self._buffer = frame.tostring() self._copy_to_gpu() except: Logger.exception('OpenCV: Couldn\'t get image from Camera') def start(self): super(CameraOpenCV, self).start() Clock.unschedule(self._update) Clock.schedule_interval(self._update, self.fps) def stop(self): super(CameraOpenCV, self).stop() Clock.unschedule(self._update) ================================================ FILE: tickeys/kivy/core/camera/camera_pygst.py ================================================ ''' GStreamer Camera ================ Implement CameraBase with GStreamer, based on PyGST ''' __all__ = ('CameraPyGst', ) from kivy.clock import Clock from kivy.graphics.texture import Texture from kivy.core.camera import CameraBase try: import pygst if not hasattr(pygst, '_gst_already_checked'): pygst.require('0.10') pygst._gst_already_checked = True import gst except: raise # install the gobject iteration from kivy.support import install_gobject_iteration install_gobject_iteration() class CameraPyGst(CameraBase): '''Implementation of CameraBase using GStreamer :Parameters: `video_src` : str, default is 'v4l2src' Other tested options are: 'dc1394src' for firewire dc camera (e.g. firefly MV). Any gstreamer video source should potentially work. Theoretically a longer string using "!" can be used describing the first part of a gstreamer pipeline. ''' def __init__(self, **kwargs): self._pipeline = None self._camerasink = None self._decodebin = None self._texturesize = None self._video_src = kwargs.get('video_src', 'v4l2src') super(CameraPyGst, self).__init__(**kwargs) def init_camera(self): # TODO: This doesn't work when camera resolution is resized at runtime. # There must be some other way to release the camera? if self._pipeline: self._pipeline = None video_src = self._video_src if video_src == 'v4l2src': video_src += ' device=/dev/video%d' % self._index elif video_src == 'dc1394src': video_src += ' camera-number=%d' % self._index GL_CAPS = 'video/x-raw-rgb,red_mask=(int)0xff0000,' + \ 'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff' pl = '%s ! decodebin name=decoder ! ffmpegcolorspace ! appsink ' + \ 'name=camerasink emit-signals=True caps=%s' self._pipeline = gst.parse_launch(pl % (video_src, GL_CAPS)) self._camerasink = self._pipeline.get_by_name('camerasink') self._camerasink.connect('new-buffer', self._gst_new_buffer) self._decodebin = self._pipeline.get_by_name('decoder') if self._camerasink and not self.stopped: self.start() def _gst_new_buffer(self, *largs): self._format = 'rgb' frame = self._camerasink.emit('pull-buffer') if frame is None: return self._buffer = frame.data if self._texturesize is None: # try to get the camera image size for x in self._decodebin.src_pads(): for cap in x.get_caps(): self._texturesize = (cap['width'], cap['height']) Clock.schedule_once(self._update) return Clock.schedule_once(self._update) def start(self): super(CameraPyGst, self).start() self._pipeline.set_state(gst.STATE_PLAYING) def stop(self): super(CameraPyGst, self).stop() self._pipeline.set_state(gst.STATE_PAUSED) def _update(self, dt): if self._buffer is None: return if self._texture is None and self._texturesize is not None: self._texture = Texture.create( size=self._texturesize, colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') self._copy_to_gpu() ================================================ FILE: tickeys/kivy/core/camera/camera_videocapture.py ================================================ ''' VideoCapture Camera: Implement CameraBase with VideoCapture ''' # # TODO: make usage of thread or multiprocess # __all__ = ('CameraVideoCapture', ) from kivy.core.camera import CameraBase from kivy.clock import Clock try: from VideoCapture import Device except: raise class CameraVideoCapture(CameraBase): '''Implementation of CameraBase using VideoCapture ''' def __init__(self, **kwargs): self._device = None super(CameraVideoCapture, self).__init__(**kwargs) self._format = 'bgr' def init_camera(self): # create the device self._device = Device(devnum=self._index, showVideoWindow=0) # set resolution try: self._device.setResolution(self.resolution[0], self.resolution[1]) except: raise Exception('VideoCapture: Resolution not supported') self.fps = 1 / 30. def _update(self, dt): data, camera_width, camera_height = self._device.getBuffer() if self._texture is None: # first update, resize if necessary self.size = camera_width, camera_height # and create texture from kivy.graphics.texture import Texture self._texture = Texture.create(size=self.size, colorfmt='rgb') self.dispatch('on_load') # update buffer self._buffer = data self._copy_to_gpu() def start(self): super(CameraVideoCapture, self).start() Clock.unschedule(self._update) Clock.schedule_interval(self._update, self.fps) def stop(self): super(CameraVideoCapture, self).stop() Clock.unschedule(self._update) ================================================ FILE: tickeys/kivy/core/clipboard/__init__.py ================================================ ''' Clipboard ========= Core class for accessing the Clipboard. If we are not able to access the system clipboard, a fake one will be used. Usage example:: >>> from kivy.core.clipboard import Clipboard >>> Clipboard.get_types() ['TIMESTAMP', 'TARGETS', 'MULTIPLE', 'SAVE_TARGETS', 'UTF8_STRING', 'COMPOUND_TEXT', 'TEXT', 'STRING', 'text/plain;charset=utf-8', 'text/plain'] >>> Clipboard.get('TEXT') 'Hello World' >>> Clipboard.put('Great', 'UTF8_STRING') >>> Clipboard.get_types() ['UTF8_STRING'] >>> Clipboard.get('UTF8_STRING') 'Great' .. note:: The main implementation relies on Pygame and works well with text/strings. Anything else might not work the same on all platforms. ''' __all__ = ('ClipboardBase', 'Clipboard') from kivy.core import core_select_lib from kivy.utils import platform from kivy.setupconfig import USE_SDL2 class ClipboardBase(object): def get(self, mimetype): '''Get the current data in clipboard, using the mimetype if possible. You not use this method directly. Use :meth:`paste` instead. ''' return None def put(self, data, mimetype): '''Put data on the clipboard, and attach a mimetype. You should not use this method directly. Use :meth:`copy` instead. ''' pass def get_types(self): '''Return a list of supported mimetypes ''' return [] def _ensure_clipboard(self): ''' Ensure that the clipboard has been properly initialised. ''' if hasattr(self, '_clip_mime_type'): return if platform == 'win': self._clip_mime_type = 'text/plain;charset=utf-8' # windows clipboard uses a utf-16 little endian encoding self._encoding = 'utf-16-le' elif platform == 'linux': self._clip_mime_type = 'text/plain;charset=utf-8' self._encoding = 'utf-8' else: self._clip_mime_type = 'text/plain' self._encoding = 'utf-8' def copy(self, data=''): ''' Copy the value provided in argument `data` into current clipboard. If data is not of type string it will be converted to string. .. versionadded:: 1.9.0 ''' if data: self._copy(data) def paste(self): ''' Get text from the system clipboard and return it a usable string. .. versionadded:: 1.9.0 ''' return self._paste() def _copy(self, data): # explicitly terminate strings with a null character # so as to avoid putting spurious data after the end. # MS windows issue. self._ensure_clipboard() if not isinstance(data, bytes): data = data.encode(self._encoding) if platform == 'win': data += b'\x00' self.put(data, self._clip_mime_type) def _paste(self): self._ensure_clipboard() _clip_types = Clipboard.get_types() mime_type = self._clip_mime_type if mime_type not in _clip_types: mime_type = 'text/plain' data = self.get(mime_type) if data is not None: # decode only if we don't have unicode # we would still need to decode from utf-16 (windows) # data is of type bytes in PY3 if isinstance(data, bytes): data = data.decode(self._encoding, 'ignore') # remove null strings mostly a windows issue data = data.replace(u'\x00', u'') return data return u'' # load clipboard implementation _clipboards = [] if platform == 'android': _clipboards.append( ('android', 'clipboard_android', 'ClipboardAndroid')) elif platform == 'macosx': _clipboards.append( ('nspaste', 'clipboard_nspaste', 'ClipboardNSPaste')) elif platform == 'win': _clipboards.append( ('winctypes', 'clipboard_winctypes', 'ClipboardWindows')) elif platform == 'linux': _clipboards.append( ('dbusklipper', 'clipboard_dbusklipper', 'ClipboardDbusKlipper')) _clipboards.append( ('gtk3', 'clipboard_gtk3', 'ClipboardGtk3')) _clipboards.append( ('xsel', 'clipboard_xsel', 'ClipboardXsel')) if USE_SDL2: if platform != 'linux': _clipboards.append( ('sdl2', 'clipboard_sdl2', 'ClipboardSDL2')) else: _clipboards.append( ('pygame', 'clipboard_pygame', 'ClipboardPygame')) _clipboards.append( ('dummy', 'clipboard_dummy', 'ClipboardDummy')) Clipboard = core_select_lib('clipboard', _clipboards, True) ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_android.py ================================================ ''' Clipboard Android ================= Android implementation of Clipboard provider, using Pyjnius. ''' __all__ = ('ClipboardAndroid', ) from kivy.core.clipboard import ClipboardBase from jnius import autoclass from android.runnable import run_on_ui_thread AndroidString = autoclass('java.lang.String') PythonActivity = autoclass('org.renpy.android.PythonActivity') Context = autoclass('android.content.Context') VER = autoclass('android.os.Build$VERSION') sdk = VER.SDK_INT class ClipboardAndroid(ClipboardBase): def __init__(self): super(ClipboardAndroid, self).__init__() self._clipboard = None self._data = dict() self._data['text/plain'] = None self._data['application/data'] = None PythonActivity._clipboard = None def get(self, mimetype='text/plain'): return self._get(mimetype) def put(self, data, mimetype='text/plain'): self._set(data, mimetype) def get_types(self): return list(self._data.keys()) @run_on_ui_thread def _initialize_clipboard(self): PythonActivity._clipboard = PythonActivity.getSystemService( Context.CLIPBOARD_SERVICE) def _get_clipboard(f): def called(*args, **kargs): self = args[0] if not PythonActivity._clipboard: self._initialize_clipboard() import time while not PythonActivity._clipboard: time.sleep(.01) return f(*args, **kargs) return called @_get_clipboard def _get(self, mimetype='text/plain'): clippy = PythonActivity._clipboard if sdk < 11: data = clippy.getText() else: ClipDescription = autoclass('android.content.ClipDescription') primary_clip = clippy.getPrimaryClip() if primary_clip and clippy.getPrimaryClipDescription().hasMimeType( ClipDescription.MIMETYPE_TEXT_PLAIN): data = primary_clip.getItemAt(0).getText().toString() else: # TODO: non text data types Not yet implemented data = '' return data @_get_clipboard def _set(self, data, mimetype): clippy = PythonActivity._clipboard if sdk < 11: #versions previous to honeycomb clippy.setText(AndroidString(data)) else: ClipData = autoclass('android.content.ClipData') new_clip = ClipData.newPlainText(AndroidString(""), AndroidString(data)) # put text data onto clipboard clippy.setPrimaryClip(new_clip) ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_dbusklipper.py ================================================ ''' Clipboard Dbus: an implementation of the Clipboard using dbus and klipper. ''' __all__ = ('ClipboardDbusKlipper', ) from kivy.utils import platform from kivy.core.clipboard import ClipboardBase if platform != 'linux': raise SystemError('unsupported platform for dbus kde clipboard') try: import dbus bus = dbus.SessionBus() proxy = bus.get_object("org.kde.klipper", "/klipper") except: raise class ClipboardDbusKlipper(ClipboardBase): _is_init = False def init(self): if ClipboardDbusKlipper._is_init: return self.iface = dbus.Interface(proxy, "org.kde.klipper.klipper") ClipboardDbusKlipper._is_init = True def get(self, mimetype='text/plain'): self.init() return str(self.iface.getClipboardContents()) def put(self, data, mimetype='text/plain'): self.init() self.iface.setClipboardContents(data.replace('\x00', '')) def get_types(self): self.init() return [u'text/plain'] ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_dummy.py ================================================ ''' Clipboard Dummy: an internal implementation that does not use the system clipboard. ''' __all__ = ('ClipboardDummy', ) from kivy.core.clipboard import ClipboardBase class ClipboardDummy(ClipboardBase): def __init__(self): super(ClipboardDummy, self).__init__() self._data = dict() self._data['text/plain'] = None self._data['application/data'] = None def get(self, mimetype='text/plain'): return self._data.get(mimetype, None) def put(self, data, mimetype='text/plain'): self._data[mimetype] = data def get_types(self): return list(self._data.keys()) ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_gtk3.py ================================================ ''' Clipboard Gtk3: an implementation of the Clipboard using Gtk3. ''' __all__ = ('ClipboardGtk3',) from kivy.utils import platform from kivy.support import install_gobject_iteration from kivy.core.clipboard import ClipboardBase if platform != 'linux': raise SystemError('unsupported platform for gtk3 clipboard') from gi.repository import Gtk, Gdk clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) class ClipboardGtk3(ClipboardBase): _is_init = False def init(self): if self._is_init: return install_gobject_iteration() self._is_init = True def get(self, mimetype='text/plain;charset=utf-8'): self.init() if mimetype == 'text/plain;charset=utf-8': contents = clipboard.wait_for_text() if contents: return contents return '' def put(self, data, mimetype='text/plain;charset=utf-8'): self.init() if mimetype == 'text/plain;charset=utf-8': text = data.decode(self._encoding) clipboard.set_text(text, -1) def get_types(self): self.init() return ['text/plain;charset=utf-8'] ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_nspaste.py ================================================ ''' Clipboard OsX: implementation of clipboard using Appkit ''' __all__ = ('ClipboardNSPaste', ) from kivy.core.clipboard import ClipboardBase from kivy.utils import platform if platform != 'macosx': raise SystemError('Unsupported platform for appkit clipboard.') try: from pyobjus import autoclass from pyobjus.dylib_manager import load_framework, INCLUDE load_framework(INCLUDE.AppKit) except ImportError: raise SystemError('Pyobjus not installed. Please run the following' ' command to install it. `pip install --user pyobjus`') NSPasteboard = autoclass('NSPasteboard') NSString = autoclass('NSString') class ClipboardNSPaste(ClipboardBase): def __init__(self): super(ClipboardNSPaste, self).__init__() self._clipboard = NSPasteboard.generalPasteboard() def get(self, mimetype='text/plain'): pb = self._clipboard data = pb.stringForType_('public.utf8-plain-text') if not data: return "" return data.UTF8String() def put(self, data, mimetype='text/plain'): pb = self._clipboard pb.clearContents() pb.writeObjects_([data]) def get_types(self): return list('text/plain',) ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_pygame.py ================================================ ''' Clipboard Pygame: an implementation of the Clipboard using pygame.scrap. ''' __all__ = ('ClipboardPygame', ) from kivy.utils import platform from kivy.compat import PY2 from kivy.core.clipboard import ClipboardBase if platform not in ('win', 'linux', 'macosx'): raise SystemError('unsupported platform for pygame clipboard') try: import pygame import pygame.scrap except: raise class ClipboardPygame(ClipboardBase): _is_init = False _types = None _aliases = { 'text/plain;charset=utf-8': 'UTF8_STRING' } def init(self): if ClipboardPygame._is_init: return pygame.scrap.init() ClipboardPygame._is_init = True def get(self, mimetype='text/plain'): self.init() mimetype = self._aliases.get(mimetype, mimetype) text = pygame.scrap.get(mimetype) return text def put(self, data, mimetype='text/plain'): self.init() mimetype = self._aliases.get(mimetype, mimetype) pygame.scrap.put(mimetype, data) def get_types(self): if not self._types: self.init() types = pygame.scrap.get_types() for mime, pygtype in self._aliases.items()[:]: if mime in types: del self._aliases[mime] if pygtype in types: types.append(mime) self._types = types return self._types ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_sdl2.py ================================================ ''' Clipboard SDL2: an implementation of the Clipboard using sdl2. ''' __all__ = ('ClipboardSDL2', ) from kivy.utils import platform from kivy.core.clipboard import ClipboardBase if platform not in ('win', 'linux', 'macosx', 'android', 'ios'): raise SystemError('unsupported platform for sdl2 clipboard') try: from kivy.core.clipboard._clipboard_sdl2 import ( _get_text, _has_text, _set_text) except ImportError: raise SystemError('extension not compiled?') class ClipboardSDL2(ClipboardBase): def get(self, mimetype): return _get_text() if _has_text() else '' def _ensure_clipboard(self): super(ClipboardSDL2, self)._ensure_clipboard() self._encoding = 'utf8' def put(self, data=b'', mimetype='text/plain'): _set_text(data) def get_types(self): return ['text/plain'] ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_winctypes.py ================================================ ''' Clipboard windows: an implementation of the Clipboard using ctypes. ''' __all__ = ('ClipboardWindows', ) from kivy.utils import platform from kivy.core.clipboard import ClipboardBase if platform != 'win': raise SystemError('unsupported platform for Windows clipboard') import ctypes user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32 msvcrt = ctypes.cdll.msvcrt c_char_p = ctypes.c_char_p c_wchar_p = ctypes.c_wchar_p class ClipboardWindows(ClipboardBase): def get(self, mimetype='text/plain'): user32.OpenClipboard(0) # 1 is CF_TEXT pcontents = user32.GetClipboardData(13) if not pcontents: return '' data = c_wchar_p(pcontents).value.encode(self._encoding) #ctypes.windll.kernel32.GlobalUnlock(pcontents) user32.CloseClipboard() return data def put(self, text, mimetype='text/plain'): GMEM_DDESHARE = 0x2000 CF_UNICODETEXT = 13 user32.OpenClipboard(None) user32.EmptyClipboard() hCd = kernel32.GlobalAlloc(GMEM_DDESHARE, len(text) + 2) pchData = kernel32.GlobalLock(hCd) msvcrt.wcscpy(c_wchar_p(pchData), text) kernel32.GlobalUnlock(hCd) user32.SetClipboardData(CF_UNICODETEXT, hCd) user32.CloseClipboard() def get_types(self): return list('text/plain',) ================================================ FILE: tickeys/kivy/core/clipboard/clipboard_xsel.py ================================================ ''' Clipboard xsel: an implementation of the Clipboard using xsel command line tool. ''' __all__ = ('ClipboardXsel', ) from kivy.utils import platform from kivy.core.clipboard import ClipboardBase if platform != 'linux': raise SystemError('unsupported platform for xsel clipboard') try: import subprocess p = subprocess.Popen(['xsel'], stdout=subprocess.PIPE) p.communicate() except: raise class ClipboardXsel(ClipboardBase): def get(self, mimetype='text/plain'): p = subprocess.Popen(['xsel', '-bo'], stdout=subprocess.PIPE) data, _ = p.communicate() return data def put(self, data, mimetype='text/plain'): p = subprocess.Popen(['xsel', '-bi'], stdin=subprocess.PIPE) p.communicate(data) def get_types(self): return [u'text/plain'] ================================================ FILE: tickeys/kivy/core/gl/__init__.py ================================================ # pylint: disable=W0611 ''' OpenGL ====== Select and use the best OpenGL library available. Depending on your system, the core provider can select an OpenGL ES or a 'classic' desktop OpenGL library. ''' from os import environ from sys import platform as sysplatform, exit MIN_REQUIRED_GL_VERSION = (2, 0) def msgbox(message): if sysplatform == 'win32': import ctypes ctypes.windll.user32.MessageBoxW( None, message, u"Kivy Fatal Error", 0) exit(1) if 'KIVY_DOC' not in environ: from kivy.logger import Logger from kivy.graphics import gl_init_resources from kivy.graphics.opengl_utils import gl_get_version from kivy.graphics.opengl import GL_VERSION, GL_VENDOR, GL_RENDERER, \ GL_MAX_TEXTURE_IMAGE_UNITS, GL_MAX_TEXTURE_SIZE, \ GL_SHADING_LANGUAGE_VERSION,\ glGetString, glGetIntegerv, gl_init_symbols from kivy.utils import platform def init_gl(): gl_init_symbols() print_gl_version() gl_init_resources() def print_gl_version(): version = glGetString(GL_VERSION) vendor = glGetString(GL_VENDOR) renderer = glGetString(GL_RENDERER) Logger.info('GL: OpenGL version <{0}>'.format(version)) Logger.info('GL: OpenGL vendor <{0}>'.format(vendor)) Logger.info('GL: OpenGL renderer <{0}>'.format(renderer)) # Let the user know if his graphics hardware/drivers are too old major, minor = gl_get_version() Logger.info('GL: OpenGL parsed version: %d, %d' % (major, minor)) if (major, minor) < MIN_REQUIRED_GL_VERSION: msg = ( 'GL: Minimum required OpenGL version (2.0) NOT found!\n\n' 'OpenGL version detected: {0}.{1}\n\n' 'Version: {2}\nVendor: {3}\nRenderer: {4}\n\n' 'Try upgrading your graphics drivers and/or your ' 'graphics hardware in case of problems.\n\n' 'The application will leave now.').format( major, minor, version, vendor, renderer) Logger.critical(msg) msgbox(msg) if platform != 'android': # XXX in the android emulator (latest version at 22 march 2013), # this call was segfaulting the gl stack. Logger.info('GL: Shading version <{0}>'.format( glGetString(GL_SHADING_LANGUAGE_VERSION))) Logger.info('GL: Texture max size <{0}>'.format( glGetIntegerv(GL_MAX_TEXTURE_SIZE)[0])) Logger.info('GL: Texture max units <{0}>'.format( glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS)[0])) # To be able to use our GL provider, we must have a window # Automaticly import window auto to ensure the default window creation import kivy.core.window # NOQA ================================================ FILE: tickeys/kivy/core/image/__init__.py ================================================ ''' Image ===== Core classes for loading images and converting them to a :class:`~kivy.graphics.texture.Texture`. The raw image data can be keep in memory for further access. In-memory image loading ----------------------- .. versionadded:: 1.9.0 Official support for in-memory loading. Not all the providers supports it, but at the moment, pygame, pil and imageio works. To load an image with a filename, you usually do:: from kivy.core.image import Image as CoreImage im = CoreImage("image.png") Now you can load from memory block. Instead of passing the filename, you'll need to pass the data as a BytesIO object + an "ext" parameters. Both are mandatory:: import io from kivy.core.image import Image as CoreImage data = io.BytesIO(open("image.png", "rb").read()) im = CoreImage(data, ext="png") By default, the image will not be cached, as our internal cache require a filename. If you want caching, add a filename that represent your file (it will be used only for caching):: import io from kivy.core.image import Image as CoreImage data = io.BytesIO(open("image.png", "rb").read()) im = CoreImage(data, ext="png", filename="image.png") ''' __all__ = ('Image', 'ImageLoader', 'ImageData') from kivy.event import EventDispatcher from kivy.core import core_register_libs from kivy.logger import Logger from kivy.cache import Cache from kivy.clock import Clock from kivy.atlas import Atlas from kivy.resources import resource_find from kivy.utils import platform from kivy.compat import string_types from kivy.setupconfig import USE_SDL2 import zipfile from io import BytesIO # late binding Texture = TextureRegion = None # register image caching only for keep_data=True Cache.register('kv.image', timeout=60) Cache.register('kv.atlas') class ImageData(object): '''Container for images and mipmap images. The container will always have at least the mipmap level 0. ''' __slots__ = ('fmt', 'mipmaps', 'source', 'flip_vertical', 'source_image') _supported_fmts = ('rgb', 'rgba', 'bgr', 'bgra', 's3tc_dxt1', 's3tc_dxt3', 's3tc_dxt5', 'pvrtc_rgb2', 'pvrtc_rgb4', 'pvrtc_rgba2', 'pvrtc_rgba4', 'etc1_rgb8') def __init__(self, width, height, fmt, data, source=None, flip_vertical=True, source_image=None, rowlength=0): assert fmt in ImageData._supported_fmts #: Decoded image format, one of a available texture format self.fmt = fmt #: Data for each mipmap. self.mipmaps = {} self.add_mipmap(0, width, height, data, rowlength) #: Image source, if available self.source = source #: Indicate if the texture will need to be vertically flipped self.flip_vertical = flip_vertical # the original image, which we might need to save if it is a memoryview self.source_image = source_image def release_data(self): mm = self.mipmaps for item in mm.values(): item[2] = None self.source_image = None @property def width(self): '''Image width in pixels. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][0] @property def height(self): '''Image height in pixels. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][1] @property def data(self): '''Image data. (If the image is mipmapped, it will use the level 0) ''' return self.mipmaps[0][2] @property def rowlength(self): '''Image rowlength. (If the image is mipmapped, it will use the level 0) .. versionadded:: 1.9.0 ''' return self.mipmaps[0][3] @property def size(self): '''Image (width, height) in pixels. (If the image is mipmapped, it will use the level 0) ''' mm = self.mipmaps[0] return mm[0], mm[1] @property def have_mipmap(self): return len(self.mipmaps) > 1 def __repr__(self): return ('' % ( self.width, self.height, self.fmt, self.source, len(self.mipmaps))) def add_mipmap(self, level, width, height, data, rowlength): '''Add a image for a specific mipmap level. .. versionadded:: 1.0.7 ''' self.mipmaps[level] = [int(width), int(height), data, rowlength] def get_mipmap(self, level): '''Get the mipmap image at a specific level if it exists .. versionadded:: 1.0.7 ''' if level == 0: return (self.width, self.height, self.data, self.rowlength) assert(level < len(self.mipmaps)) return self.mipmaps[level] def iterate_mipmaps(self): '''Iterate over all mipmap images available. .. versionadded:: 1.0.7 ''' mm = self.mipmaps for x in range(len(mm)): item = mm.get(x, None) if item is None: raise Exception('Invalid mipmap level, found empty one') yield x, item[0], item[1], item[2], item[3] class ImageLoaderBase(object): '''Base to implement an image loader.''' __slots__ = ('_texture', '_data', 'filename', 'keep_data', '_mipmap', '_nocache', '_ext', '_inline') def __init__(self, filename, **kwargs): self._mipmap = kwargs.get('mipmap', False) self.keep_data = kwargs.get('keep_data', False) self._nocache = kwargs.get('nocache', False) self._ext = kwargs.get('ext') self._inline = kwargs.get('inline') self.filename = filename if self._inline: self._data = self.load(kwargs.get('rawdata')) else: self._data = self.load(filename) self._textures = None def load(self, filename): '''Load an image''' return None @staticmethod def can_save(): '''Indicate if the loader can save the Image object ''' return False @staticmethod def can_load_memory(): '''Indicate if the loader can load an image by passing data ''' return False @staticmethod def save(): raise NotImplementedError() def populate(self): self._textures = [] fname = self.filename if __debug__: Logger.trace('Image: %r, populate to textures (%d)' % (fname, len(self._data))) for count in range(len(self._data)): # first, check if a texture with the same name already exist in the # cache chr = type(fname) uid = chr(u'%s|%d|%d') % (fname, self._mipmap, count) texture = Cache.get('kv.texture', uid) # if not create it and append to the cache if texture is None: imagedata = self._data[count] source = '{}{}|'.format( 'zip|' if fname.endswith('.zip') else '', self._nocache) imagedata.source = chr(source) + uid texture = Texture.create_from_data( imagedata, mipmap=self._mipmap) if not self._nocache: Cache.append('kv.texture', uid, texture) if imagedata.flip_vertical: texture.flip_vertical() # set as our current texture self._textures.append(texture) # release data if ask if not self.keep_data: self._data[count].release_data() @property def width(self): '''Image width ''' return self._data[0].width @property def height(self): '''Image height ''' return self._data[0].height @property def size(self): '''Image size (width, height) ''' return (self._data[0].width, self._data[0].height) @property def texture(self): '''Get the image texture (created on the first call) ''' if self._textures is None: self.populate() if self._textures is None: return None return self._textures[0] @property def textures(self): '''Get the textures list (for mipmapped image or animated image) .. versionadded:: 1.0.8 ''' if self._textures is None: self.populate() return self._textures @property def nocache(self): '''Indicate if the texture will not be stored in the cache .. versionadded:: 1.6.0 ''' return self._nocache class ImageLoader(object): loaders = [] @staticmethod def zip_loader(filename, **kwargs): '''Read images from an zip file. .. versionadded:: 1.0.8 Returns an Image with a list of type ImageData stored in Image._data ''' # read zip in menory for faster access _file = BytesIO(open(filename, 'rb').read()) # read all images inside the zip z = zipfile.ZipFile(_file) image_data = [] # sort filename list znamelist = z.namelist() znamelist.sort() image = None for zfilename in znamelist: try: #read file and store it in mem with fileIO struct around it tmpfile = BytesIO(z.read(zfilename)) ext = zfilename.split('.')[-1].lower() im = None for loader in ImageLoader.loaders: if (ext not in loader.extensions() or not loader.can_load_memory()): continue Logger.debug('Image%s: Load <%s> from <%s>' % (loader.__name__[11:], zfilename, filename)) try: im = loader(zfilename, ext=ext, rawdata=tmpfile, inline=True, **kwargs) except: # Loader failed, continue trying. continue break if im is not None: # append ImageData to local variable before it's # overwritten image_data.append(im._data[0]) image = im #else: if not image file skip to next except: Logger.warning('Image: Unable to load image' '<%s> in zip <%s> trying to continue...' % (zfilename, filename)) z.close() if len(image_data) == 0: raise Exception('no images in zip <%s>' % filename) # replace Image.Data with the array of all the images in the zip image._data = image_data image.filename = filename return image @staticmethod def register(defcls): ImageLoader.loaders.append(defcls) @staticmethod def load(filename, **kwargs): # atlas ? if filename[:8] == 'atlas://': # remove the url rfn = filename[8:] # last field is the ID try: rfn, uid = rfn.rsplit('/', 1) except ValueError: raise ValueError( 'Image: Invalid %s name for atlas' % filename) # search if we already got the atlas loaded atlas = Cache.get('kv.atlas', rfn) # atlas already loaded, so reupload the missing texture in cache, # because when it's not in use, the texture can be removed from the # kv.texture cache. if atlas: texture = atlas[uid] fn = 'atlas://%s/%s' % (rfn, uid) cid = '{}|{:d}|{:d}'.format(fn, False, 0) Cache.append('kv.texture', cid, texture) return Image(texture) # search with resource afn = rfn if not afn.endswith('.atlas'): afn += '.atlas' afn = resource_find(afn) if not afn: raise Exception('Unable to found %r atlas' % afn) atlas = Atlas(afn) Cache.append('kv.atlas', rfn, atlas) # first time, fill our texture cache. for nid, texture in atlas.textures.items(): fn = 'atlas://%s/%s' % (rfn, nid) cid = '{}|{:d}|{:d}'.format(fn, False, 0) Cache.append('kv.texture', cid, texture) return Image(atlas[uid]) # extract extensions ext = filename.split('.')[-1].lower() # prevent url querystrings if filename.startswith((('http://', 'https://'))): ext = ext.split('?')[0] filename = resource_find(filename) # special case. When we are trying to load a "zip" file with image, we # will use the special zip_loader in ImageLoader. This might return a # sequence of images contained in the zip. if ext == 'zip': return ImageLoader.zip_loader(filename) else: im = None for loader in ImageLoader.loaders: if ext not in loader.extensions(): continue Logger.debug('Image%s: Load <%s>' % (loader.__name__[11:], filename)) im = loader(filename, **kwargs) break if im is None: raise Exception('Unknown <%s> type, no loader found.' % ext) return im class Image(EventDispatcher): '''Load an image and store the size and texture. .. versionchanged:: 1.0.7 `mipmap` attribute has been added. The `texture_mipmap` and `texture_rectangle` have been deleted. .. versionchanged:: 1.0.8 An Image widget can change its texture. A new event 'on_texture' has been introduced. New methods for handling sequenced animation have been added. :Parameters: `arg` : can be a string (str), Texture or Image object. A string is interpreted as a path to the image to be loaded. You can also provide a texture object or an already existing image object. In the latter case, a real copy of the given image object will be returned. `keep_data` : bool, defaults to False. Keep the image data when the texture is created. `scale` : float, defaults to 1.0 Scale of the image. `mipmap` : bool, defaults to False Create mipmap for the texture. `anim_delay`: float, defaults to .25 Delay in seconds between each animation frame. Lower values means faster animation. ''' copy_attributes = ('_size', '_filename', '_texture', '_image', '_mipmap', '_nocache') def __init__(self, arg, **kwargs): # this event should be fired on animation of sequenced img's self.register_event_type('on_texture') super(Image, self).__init__() self._mipmap = kwargs.get('mipmap', False) self._keep_data = kwargs.get('keep_data', False) self._nocache = kwargs.get('nocache', False) self._size = [0, 0] self._image = None self._filename = None self._texture = None self._anim_available = False self._anim_index = 0 self._anim_delay = 0 self.anim_delay = kwargs.get('anim_delay', .25) # indicator of images having been loded in cache self._iteration_done = False if isinstance(arg, Image): for attr in Image.copy_attributes: self.__setattr__(attr, arg.__getattribute__(attr)) elif type(arg) in (Texture, TextureRegion): if not hasattr(self, 'textures'): self.textures = [] self.textures.append(arg) self._texture = arg self._size = self.texture.size elif isinstance(arg, ImageLoaderBase): self.image = arg elif isinstance(arg, BytesIO): ext = kwargs.get('ext', None) if not ext: raise Exception('Inline loading require "ext" parameter') filename = kwargs.get('filename') if not filename: self._nocache = True filename = '__inline__' self.load_memory(arg, ext, filename) elif isinstance(arg, string_types): self.filename = arg else: raise Exception('Unable to load image type {0!r}'.format(arg)) def remove_from_cache(self): '''Remove the Image from cache. This facilitates re-loading of images from disk in case the image content has changed. .. versionadded:: 1.3.0 Usage:: im = CoreImage('1.jpg') # -- do something -- im.remove_from_cache() im = CoreImage('1.jpg') # this time image will be re-loaded from disk ''' count = 0 f = self.filename pat = type(f)(u'%s|%d|%d') uid = pat % (f, self._mipmap, count) Cache.remove("kv.image", uid) while Cache.get("kv.texture", uid): Cache.remove("kv.texture", uid) count += 1 uid = pat % (f, self._mipmap, count) def _anim(self, *largs): if not self._image: return textures = self.image.textures if self._anim_index >= len(textures): self._anim_index = 0 self._texture = self.image.textures[self._anim_index] self.dispatch('on_texture') self._anim_index += 1 self._anim_index %= len(self._image.textures) def anim_reset(self, allow_anim): '''Reset an animation if available. .. versionadded:: 1.0.8 :Parameters: `allow_anim`: bool Indicate whether the animation should restart playing or not. Usage:: # start/reset animation image.anim_reset(True) # or stop the animation image.anim_reset(False) You can change the animation speed whilst it is playing:: # Set to 20 FPS image.anim_delay = 1 / 20. ''' # stop animation Clock.unschedule(self._anim) if allow_anim and self._anim_available: Clock.schedule_interval(self._anim, self.anim_delay) self._anim() def _get_anim_delay(self): return self._anim_delay def _set_anim_delay(self, x): if self._anim_delay == x: return self._anim_delay = x if self._anim_available: Clock.unschedule(self._anim) if self._anim_delay >= 0: Clock.schedule_interval(self._anim, self._anim_delay) anim_delay = property(_get_anim_delay, _set_anim_delay) '''Delay between each animation frame. A lower value means faster animation. .. versionadded:: 1.0.8 ''' @property def anim_available(self): '''Return True if this Image instance has animation available. .. versionadded:: 1.0.8 ''' return self._anim_available @property def anim_index(self): '''Return the index number of the image currently in the texture. .. versionadded:: 1.0.8 ''' return self._anim_index def _img_iterate(self, *largs): if not self.image or self._iteration_done: return self._iteration_done = True imgcount = len(self.image.textures) if imgcount > 1: self._anim_available = True self.anim_reset(True) self._texture = self.image.textures[0] def on_texture(self, *largs): '''This event is fired when the texture reference or content has changed. It is normally used for sequenced images. .. versionadded:: 1.0.8 ''' pass @staticmethod def load(filename, **kwargs): '''Load an image :Parameters: `filename` : str Filename of the image. `keep_data` : bool, defaults to False Keep the image data when the texture is created. ''' kwargs.setdefault('keep_data', False) return Image(filename, **kwargs) def _get_image(self): return self._image def _set_image(self, image): self._image = image if hasattr(image, 'filename'): self._filename = image.filename if image: self._size = (self.image.width, self.image.height) image = property(_get_image, _set_image, doc='Get/set the data image object') def _get_filename(self): return self._filename def _set_filename(self, value): if value is None or value == self._filename: return self._filename = value # construct uid as a key for Cache f = self.filename uid = type(f)(u'%s|%d|%d') % (f, self._mipmap, 0) # in case of Image have been asked with keep_data # check the kv.image cache instead of texture. image = Cache.get('kv.image', uid) if image: # we found an image, yeah ! but reset the texture now. self.image = image # if image.__class__ is core image then it's a texture # from atlas or other sources and has no data so skip if (image.__class__ != self.__class__ and not image.keep_data and self._keep_data): self.remove_from_cache() self._filename = '' self._set_filename(value) else: self._texture = None self._img_iterate() return else: # if we already got a texture, it will be automatically reloaded. _texture = Cache.get('kv.texture', uid) if _texture: self._texture = _texture return # if image not already in cache then load tmpfilename = self._filename image = ImageLoader.load( self._filename, keep_data=self._keep_data, mipmap=self._mipmap, nocache=self._nocache) self._filename = tmpfilename # put the image into the cache if needed if isinstance(image, Texture): self._texture = image self._size = image.size else: self.image = image if not self._nocache: Cache.append('kv.image', uid, self.image) filename = property(_get_filename, _set_filename, doc='Get/set the filename of image') def load_memory(self, data, ext, filename='__inline__'): '''(internal) Method to load an image from raw data. ''' self._filename = filename # see if there is a available loader for it loaders = [loader for loader in ImageLoader.loaders if loader.can_load_memory() and ext in loader.extensions()] if not loaders: raise Exception('No inline loader found to load {}'.format(ext)) image = loaders[0](filename, ext=ext, rawdata=data, inline=True, nocache=self._nocache, mipmap=self._mipmap, keep_data=self._keep_data) if isinstance(image, Texture): self._texture = image self._size = image.size else: self.image = image @property def size(self): '''Image size (width, height) ''' return self._size @property def width(self): '''Image width ''' return self._size[0] @property def height(self): '''Image height ''' return self._size[1] @property def texture(self): '''Texture of the image''' if self.image: if not self._iteration_done: self._img_iterate() return self._texture @property def nocache(self): '''Indicate whether the texture will not be stored in the cache or not. .. versionadded:: 1.6.0 ''' return self._nocache def save(self, filename, flipped=False): '''Save image texture to file. The filename should have the '.png' extension because the texture data read from the GPU is in the RGBA format. '.jpg' might work but has not been heavilly tested so some providers might break when using it. Any other extensions are not officially supported. The flipped parameter flips the saved image vertically, and defaults to True. Example:: # Save an core image object from kivy.core.image import Image img = Image('hello.png') img.save('hello2.png') # Save a texture texture = Texture.create(...) img = Image(texture) img.save('hello3.png') .. versionadded:: 1.7.0 .. versionchanged:: 1.8.0 Parameter `flipped` added to flip the image before saving, default to False. ''' pixels = None size = None loaders = [x for x in ImageLoader.loaders if x.can_save()] if not loaders: return False loader = loaders[0] if self.image: # we might have a ImageData object to use data = self.image._data[0] if data.data is not None: if data.fmt not in ('rgba', 'rgb'): # fast path, use the "raw" data when keep_data is used size = data.width, data.height pixels = data.data else: # the format is not rgba, we need to convert it. # use texture for that. self.populate() if pixels is None and self._texture: # use the texture pixels size = self._texture.size pixels = self._texture.pixels if pixels is None: return False l_pixels = len(pixels) if l_pixels == size[0] * size[1] * 3: fmt = 'rgb' elif l_pixels == size[0] * size[1] * 4: fmt = 'rgba' else: raise Exception('Unable to determine the format of the pixels') return loader.save(filename, size[0], size[1], fmt, pixels, flipped) def read_pixel(self, x, y): '''For a given local x/y position, return the pixel color at that position. .. warning:: This function can only be used with images loaded with the keep_data=True keyword. For example:: m = Image.load('image.png', keep_data=True) color = m.read_pixel(150, 150) :Parameters: `x` : int Local x coordinate of the pixel in question. `y` : int Local y coordinate of the pixel in question. ''' data = self.image._data[0] # can't use this fonction without ImageData if data.data is None: raise EOFError('Image data is missing, make sure that image is' 'loaded with keep_data=True keyword.') # check bounds x, y = int(x), int(y) if not (0 <= x < data.width and 0 <= y < data.height): raise IndexError('Position (%d, %d) is out of range.' % (x, y)) assert data.fmt in ImageData._supported_fmts size = 3 if data.fmt in ('rgb', 'bgr') else 4 index = y * data.width * size + x * size raw = bytearray(data.data[index:index + size]) color = [c / 255.0 for c in raw] # conversion for BGR->RGB, BGR->RGBA format if data.fmt in ('bgr', 'bgra'): color[0], color[2] = color[2], color[0] return color def load(filename): '''Load an image''' return Image.load(filename) # load image loaders image_libs = [] if platform in ('macosx', 'ios'): image_libs += [('imageio', 'img_imageio')] image_libs += [ ('tex', 'img_tex'), ('dds', 'img_dds')] if USE_SDL2: image_libs += [('sdl2', 'img_sdl2')] else: image_libs += [('pygame', 'img_pygame')] image_libs += [ ('ffpy', 'img_ffpyplayer'), ('pil', 'img_pil'), ('gif', 'img_gif')] libs_loaded = core_register_libs('image', image_libs) from os import environ if not 'KIVY_DOC' in environ and not libs_loaded: import sys Logger.critical('App: Unable to get any Image provider, abort.') sys.exit(1) # resolve binding. from kivy.graphics.texture import Texture, TextureRegion ================================================ FILE: tickeys/kivy/core/image/img_dds.py ================================================ ''' DDS: DDS image loader ''' __all__ = ('ImageLoaderDDS', ) from kivy.lib.ddsfile import DDSFile from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader class ImageLoaderDDS(ImageLoaderBase): @staticmethod def extensions(): return ('dds', ) def load(self, filename): try: dds = DDSFile(filename=filename) except: Logger.warning('Image: Unable to load image <%s>' % filename) raise self.filename = filename width, height = dds.size im = ImageData(width, height, dds.dxt, dds.images[0], source=filename, flip_vertical=False) if len(dds.images) > 1: images = dds.images images_size = dds.images_size for index in range(1, len(dds.images)): w, h = images_size[index] data = images[index] im.add_mipmap(index, w, h, data) return [im] # register ImageLoader.register(ImageLoaderDDS) ================================================ FILE: tickeys/kivy/core/image/img_ffpyplayer.py ================================================ ''' FFPyPlayer: FFmpeg based image loader ''' __all__ = ('ImageLoaderFFPy', ) import ffpyplayer from ffpyplayer.pic import ImageLoader as ffImageLoader, SWScale from ffpyplayer.tools import set_log_callback, loglevels, get_log_callback from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader Logger.info('ImageLoaderFFPy: Using ffpyplayer {}'.format(ffpyplayer.version)) logger_func = {'quiet': Logger.critical, 'panic': Logger.critical, 'fatal': Logger.critical, 'error': Logger.error, 'warning': Logger.warning, 'info': Logger.info, 'verbose': Logger.debug, 'debug': Logger.debug} def _log_callback(message, level): message = message.strip() if message: logger_func[level]('ffpyplayer: {}'.format(message)) if not get_log_callback(): set_log_callback(_log_callback) class ImageLoaderFFPy(ImageLoaderBase): '''Image loader based on the ffpyplayer library. .. versionadded:: 1.9.0 .. note: This provider may support more formats than what is listed in :meth:`extensions`. ''' @staticmethod def extensions(): '''Return accepted extensions for this loader''' # See https://www.ffmpeg.org/general.html#Image-Formats return ('bmp', 'dpx', 'exr', 'gif', 'ico', 'jpeg', 'jpg2000', 'jpg', 'jls', 'pam', 'pbm', 'pcx', 'pgm', 'pgmyuv', 'pic', 'png', 'ppm', 'ptx', 'sgi', 'ras', 'tga', 'tiff', 'webp', 'xbm', 'xface', 'xwd') def load(self, filename): try: loader = ffImageLoader(filename) except: Logger.warning('Image: Unable to load image <%s>' % filename) raise # update internals self.filename = filename images = [] while True: frame, t = loader.next_frame() if frame is None: break images.append(frame) if not len(images): raise Exception('No image found in {}'.format(filename)) w, h = images[0].get_size() ifmt = images[0].get_pixel_format() if ifmt != 'rgba' and ifmt != 'rgb24': fmt = 'rgba' sws = SWScale(w, h, ifmt, ofmt=fmt) for i, image in enumerate(images): images[i] = sws.scale(image) else: fmt = ifmt if ifmt == 'rgba' else 'rgb' return [ImageData(w, h, fmt, img.to_memoryview()[0], source_image=img) for img in images] # register ImageLoader.register(ImageLoaderFFPy) ================================================ FILE: tickeys/kivy/core/image/img_gif.py ================================================ #-*- coding: utf-8 -*- # # this program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # this program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # The Graphics Interchange Format(c) is the Copyright property of # CompuServe Incorporated. GIF(sm) is a Service Mark property of # CompuServe Incorporated. # # The unisys/lzw patent has expired, yes. If anyone puts another patent # over this code, you must *burn* this file. '''pygif: gif implementation in python http://www.java2s.com/Open-Source/Python/Network/\ emesene/emesene-1.6.2/pygif/pygif.py.htm''' #TODO issues to fix #optimize for speed #partially done# a lot of room for improvement import struct from array import array KNOWN_FORMATS = ('GIF87a', 'GIF89a') from kivy.compat import PY2 from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader Debug = False class ImageLoaderGIF(ImageLoaderBase): '''Image loader for gif''' @staticmethod def extensions(): '''Return accepted extension for this loader''' return ('gif', ) def load(self, filename): try: try: im = GifDecoder(open(filename, 'rb').read()) except UnicodeEncodeError: if PY2: im = GifDecoder(open(filename.encode('utf8'), 'rb').read()) except: Logger.warning('Image: Unable to load Image <%s>' % filename) raise if Debug: print(im.print_info()) img_data = [] ls_width = im.ls_width ls_height = im.ls_height im_images = im.images im_palette = im.palette pixel_map = array('B', [0] * (ls_width * ls_height * 4)) for img in im_images: palette = img.palette if img.local_color_table_flag\ else im_palette have_transparent_color = img.has_transparent_color transparent_color = img.transparent_color #draw_method_restore_previous = 1 \ # if img.draw_method == 'restore previous' else 0 draw_method_replace = 1 \ if ((img.draw_method == 'replace') or (img.draw_method == 'restore background')) else 0 pixels = img.pixels img_height = img.height img_width = img.width left = img.left top = img.top if img_height > ls_height or img_width > ls_width or\ top > ls_height or left > ls_width: Logger.warning('Image_GIF: decoding error on frame <%s>' % len(img_data)) img_height = ls_height img_width = ls_width left = top = 0 #reverse top to bottom and left to right tmp_top = (ls_height - (img_height + top)) img_width_plus_left = (img_width + left) ls_width_multiply_4 = ls_width * 4 left_multiply_4 = left * 4 img_data_append = img_data.append while img_height > 0: i = left img_height -= 1 x = (img_height * img_width) - left rgba_pos = (tmp_top * ls_width_multiply_4) + (left_multiply_4) tmp_top += 1 while i < img_width_plus_left: #this should now display corrupted gif's #instead of crashing on gif's not decoded properly try: (r, g, b) = palette[pixels[x + i]] except: rgba_pos += 4 i += 1 continue # when not magic pink if (r, g, b) != (255, 0, 255): if have_transparent_color: if transparent_color == pixels[x + i]: if draw_method_replace: #transparent pixel draw method replace pixel_map[rgba_pos + 3] = 0 rgba_pos += 4 i += 1 continue #transparent pixel draw method combine rgba_pos += 4 i += 1 continue # this pixel isn't transparent #doesn't have transparent color (pixel_map[rgba_pos], pixel_map[rgba_pos + 1], pixel_map[rgba_pos + 2]) = (r, g, b) pixel_map[rgba_pos + 3] = 255 # if magic pink move to next pixel rgba_pos += 4 i += 1 img_data_append(ImageData(ls_width, ls_height, 'rgba', pixel_map.tostring(), flip_vertical=False)) if draw_method_replace: pixel_map = array('B', [0] * (ls_width * ls_height * 4)) self.filename = filename return img_data class Gif(object): '''Base class to decoder''' # struct format strings #17,18: FMT_HEADER = '<6sHHBBB' #20: FMT_IMGDESC = ' aode size <%d>' % (code, codesize)) string_table[code] = string_table[0] output_append(ord(string_table[code])) old = string_table[code] bitlen = len(bits) while self.bitpointer < bitlen: # read next code code = self_bits_to_int(pop(codesize, bits)) # special code? if code == clearcode: index = clear() codesize = initial_codesize + 1 code = self_bits_to_int(pop(codesize, bits)) if code in string_table: output_append(ord(string_table[code])) else: Logger.warning('Image_GIF: decoding error on code ' '<%d> aode size <%d>' % (code, codesize)) string_table[code] = string_table[0] output_append(ord(string_table[code])) old = string_table[code] continue elif code == end_of_info: break # code in stringtable? if code in string_table: c = string_table[code] string_table[index] = ''.join((old, c[0])) else: c = ''.join((old, old[0])) string_table[code] = c index += 1 old = c output_extend(list(map(ord, c))) if index == 2 ** codesize: codesize += 1 if codesize == 13: codesize = 12 if self.debug_enabled: print('Output stream len: %d' % len(output)) return output def get_bits(flags, reverse=False, bits=8): '''return a list with $bits items, one for each enabled bit''' mybits = (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048)[:bits] rev_num = 1 if reverse: rev_num = -1 ret = array('B') ret_append = ret.append for bit in mybits[::rev_num]: ret_append(flags & bit != 0) return ret def pack_bits(bits): '''convert a bit (bool or int) tuple into a int''' packed = 0 level = 0 for bit in bits: if bit: packed += 2 ** level level += 1 return packed # register ImageLoader.register(ImageLoaderGIF) ================================================ FILE: tickeys/kivy/core/image/img_pil.py ================================================ ''' PIL: PIL image loader ''' __all__ = ('ImageLoaderPIL', ) try: from PIL import Image as PILImage except: import Image as PILImage from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader class ImageLoaderPIL(ImageLoaderBase): '''Image loader based on the PIL library. .. versionadded:: 1.0.8 Support for GIF animation added. Gif animation has a lot of issues(transparency/color depths... etc). In order to keep it simple, what is implimented here is what is natively supported by the PIL library. As a general rule, try to use gifs that have no transparency. Gif's with transparency will work but be prepared for some artifacts until transparency support is improved. ''' @staticmethod def can_save(): return True @staticmethod def can_load_memory(): return True @staticmethod def extensions(): '''Return accepted extensions for this loader''' # See http://www.pythonware.com/library/pil/handbook/index.htm return ('bmp', 'bufr', 'cur', 'dcx', 'fits', 'fl', 'fpx', 'gbr', 'gd', 'gif', 'grib', 'hdf5', 'ico', 'im', 'imt', 'iptc', 'jpeg', 'jpg', 'jpe', 'mcidas', 'mic', 'mpeg', 'msp', 'pcd', 'pcx', 'pixar', 'png', 'ppm', 'psd', 'sgi', 'spider', 'tga', 'tiff', 'wal', 'wmf', 'xbm', 'xpm', 'xv') def _img_correct(self, _img_tmp): '''Convert image to the correct format and orientation. ''' # image loader work only with rgb/rgba image if _img_tmp.mode.lower() not in ('rgb', 'rgba'): try: imc = _img_tmp.convert('RGBA') except: Logger.warning( 'Image: Unable to convert image to rgba (was %s)' % (_img_tmp.mode.lower())) raise _img_tmp = imc return _img_tmp def _img_read(self, im): '''Read images from an animated file. ''' im.seek(0) # Read all images inside try: img_ol = None while True: img_tmp = im img_tmp = self._img_correct(img_tmp) if img_ol and (hasattr(im, 'dispose') and not im.dispose): # paste new frame over old so as to handle # transparency properly img_ol.paste(img_tmp, (0, 0), img_tmp) img_tmp = img_ol img_ol = img_tmp yield ImageData(img_tmp.size[0], img_tmp.size[1], img_tmp.mode.lower(), img_tmp.tostring()) im.seek(im.tell() + 1) except EOFError: pass def load(self, filename): try: im = PILImage.open(filename) except: Logger.warning('Image: Unable to load image <%s>' % filename) raise # update internals if not self._inline: self.filename = filename # returns an array of type ImageData len 1 if not a sequence image return list(self._img_read(im)) @staticmethod def save(filename, width, height, fmt, pixels, flipped=False): image = PILImage.fromstring(fmt.upper(), (width, height), pixels) if flipped: image = image.transpose(PILImage.FLIP_TOP_BOTTOM) image.save(filename) return True # register ImageLoader.register(ImageLoaderPIL) ================================================ FILE: tickeys/kivy/core/image/img_pygame.py ================================================ ''' Pygame: Pygame image loader ''' __all__ = ('ImageLoaderPygame', ) from kivy.compat import PY2 from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader from os.path import isfile try: import pygame except: raise class ImageLoaderPygame(ImageLoaderBase): '''Image loader based on the PIL library''' @staticmethod def extensions(): '''Return accepted extensions for this loader''' # under macosx, i got with "pygame.error: File is not a Windows BMP # file". documentation said: The image module is a required dependency # of Pygame, but it only optionally supports any extended file formats. # By default it can only load uncompressed BMP image if pygame.image.get_extended() == 0: return ('bmp', ) return ('jpg', 'jpeg', 'jpe', 'png', 'bmp', 'pcx', 'tga', 'tiff', 'tif', 'lbm', 'pbm', 'ppm', 'xpm') @staticmethod def can_save(): return True @staticmethod def can_load_memory(): return True def load(self, filename): if not filename: import traceback traceback.print_stack() return try: im = None if self._inline: im = pygame.image.load(filename, 'x.{}'.format(self._ext)) elif isfile(filename): with open(filename, 'rb') as fd: im = pygame.image.load(fd) elif isinstance(filename, bytes): try: fname = filename.decode() if isfile(fname): with open(fname, 'rb') as fd: im = pygame.image.load(fd) except UnicodeDecodeError: pass if im is None: im = pygame.image.load(filename) except: #Logger.warning(type(filename)('Image: Unable to load image <%s>') # % filename) raise fmt = '' if im.get_bytesize() == 3: fmt = 'rgb' elif im.get_bytesize() == 4: fmt = 'rgba' # image loader work only with rgb/rgba image if fmt not in ('rgb', 'rgba'): try: imc = im.convert(32) fmt = 'rgba' except: try: imc = im.convert_alpha() fmt = 'rgba' except: Logger.warning( 'Image: Unable to convert image %r to rgba (was %r)' % (filename, im.fmt)) raise im = imc # update internals if not self._inline: self.filename = filename data = pygame.image.tostring(im, fmt.upper()) return [ImageData(im.get_width(), im.get_height(), fmt, data, source=filename)] @staticmethod def save(filename, width, height, fmt, pixels, flipped): surface = pygame.image.fromstring( pixels, (width, height), fmt.upper(), flipped) pygame.image.save(surface, filename) return True # register ImageLoader.register(ImageLoaderPygame) ================================================ FILE: tickeys/kivy/core/image/img_sdl2.py ================================================ ''' SDL2 image loader ================= ''' __all__ = ('ImageLoaderSDL2', ) from kivy.compat import PY2 from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader from kivy.core.image import _img_sdl2 class ImageLoaderSDL2(ImageLoaderBase): '''Image loader based on the PIL library''' def _ensure_ext(self): _img_sdl2.init() @staticmethod def extensions(): '''Return accepted extensions for this loader''' return ('bmp', 'jpg', 'jpeg', 'lbm', 'pcx', 'png', 'pnm', 'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv') @staticmethod def can_save(): return True @staticmethod def can_load_memory(): return True def load(self, filename): if self._inline: data = filename.read() info = _img_sdl2.load_from_memory(data) else: info = _img_sdl2.load_from_filename(filename) if not info: Logger.warning('Image: Unable to load image <%s>' % filename) raise Exception('SDL2: Unable to load image') w, h, fmt, pixels, rowlength = info # update internals if not self._inline: self.filename = filename return [ImageData( w, h, fmt, pixels, source=filename, rowlength=rowlength)] @staticmethod def save(filename, width, height, fmt, pixels, flipped): _img_sdl2.save(filename, width, height, fmt, pixels, flipped) return True # register ImageLoader.register(ImageLoaderSDL2) ================================================ FILE: tickeys/kivy/core/image/img_tex.py ================================================ ''' Tex: Compressed texture ''' __all__ = ('ImageLoaderTex', ) import json from struct import unpack from kivy.logger import Logger from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader class ImageLoaderTex(ImageLoaderBase): @staticmethod def extensions(): return ('tex', ) def load(self, filename): try: fd = open(filename, 'rb') if fd.read(4) != 'KTEX': raise Exception('Invalid tex identifier') headersize = unpack('I', fd.read(4))[0] header = fd.read(headersize) if len(header) != headersize: raise Exception('Truncated tex header') info = json.loads(header) data = fd.read() if len(data) != info['datalen']: raise Exception('Truncated tex data') except: Logger.warning('Image: Image <%s> is corrupted' % filename) raise width, height = info['image_size'] tw, th = info['texture_size'] images = [data] im = ImageData(width, height, str(info['format']), images[0], source=filename) ''' if len(dds.images) > 1: images = dds.images images_size = dds.images_size for index in range(1, len(dds.images)): w, h = images_size[index] data = images[index] im.add_mipmap(index, w, h, data) ''' return [im] # register ImageLoader.register(ImageLoaderTex) ================================================ FILE: tickeys/kivy/core/spelling/__init__.py ================================================ ''' Spelling ======== Provides abstracted access to a range of spellchecking backends as well as word suggestions. The API is inspired by enchant but other backends can be added that implement the same API. Spelling currently requires `python-enchant` for all platforms except OSX, where a native implementation exists. :: >>> from kivy.core.spelling import Spelling >>> s = Spelling() >>> s.list_languages() ['en', 'en_CA', 'en_GB', 'en_US'] >>> s.select_language('en_US') >>> s.suggest('helo') [u'hole', u'help', u'helot', u'hello', u'halo', u'hero', u'hell', u'held', u'helm', u'he-lo'] ''' __all__ = ('Spelling', 'SpellingBase', 'NoSuchLangError', 'NoLanguageSelectedError') import sys from kivy.core import core_select_lib class NoSuchLangError(Exception): ''' Exception to be raised when a specific language could not be found. ''' pass class NoLanguageSelectedError(Exception): ''' Exception to be raised when a language-using method is called but no language was selected prior to the call. ''' pass class SpellingBase(object): ''' Base class for all spelling providers. Supports some abstract methods for checking words and getting suggestions. ''' def __init__(self, language=None): ''' If a `language` identifier (such as 'en_US') is provided and a matching language exists, it is selected. If an identifier is provided and no matching language exists, a NoSuchLangError exception is raised by self.select_language(). If no `language` identifier is provided, we just fall back to the first one that is available. :Parameters: `language` : str, defaults to None If provided, indicates the language to be used. This needs to be a language identifier understood by select_language(), i.e. one of the options returned by list_languages(). If nothing is provided, the first available language is used. If no language is available, NoLanguageSelectedError is raised. ''' langs = self.list_languages() try: # If no language was specified, we just use the first one # that is available. fallback_lang = langs[0] except IndexError: raise NoLanguageSelectedError("No languages available!") self.select_language(language or fallback_lang) def select_language(self, language): ''' From the set of registered languages, select the first language for `language`. :Parameters: `language` : str Language identifier. Needs to be one of the options returned by list_languages(). Sets the language used for spell checking and word suggestions. ''' raise NotImplementedError('select_language() method not implemented ' 'by abstract spelling base class!') def list_languages(self): ''' Return a list of all supported languages. E.g. ['en', 'en_GB', 'en_US', 'de', ...] ''' raise NotImplementedError('list_languages() is not implemented ' 'by abstract spelling base class!') def check(self, word): ''' If `word` is a valid word in `self._language` (the currently active language), returns True. If the word shouldn't be checked, returns None (e.g. for ''). If it is not a valid word in `self._language`, return False. :Parameters: `word` : str The word to check. ''' raise NotImplementedError('check() not implemented by abstract ' + 'spelling base class!') def suggest(self, fragment): ''' For a given `fragment` (i.e. part of a word or a word by itself), provide corrections (`fragment` may be misspelled) or completions as a list of strings. :Parameters: `fragment` : str The word fragment to get suggestions/corrections for. E.g. 'foo' might become 'of', 'food' or 'foot'. ''' raise NotImplementedError('suggest() not implemented by abstract ' + 'spelling base class!') _libs = (('enchant', 'spelling_enchant', 'SpellingEnchant'), ) if sys.platform == 'darwin': _libs += (('osxappkit', 'spelling_osxappkit', 'SpellingOSXAppKit'), ) Spelling = core_select_lib('spelling', _libs) ================================================ FILE: tickeys/kivy/core/spelling/spelling_enchant.py ================================================ ''' Enchant Spelling: Implements spelling backend based on enchant. ''' import enchant from kivy.core.spelling import SpellingBase, NoSuchLangError from kivy.compat import PY2 class SpellingEnchant(SpellingBase): ''' Spelling backend based on the enchant library. ''' def __init__(self, language=None): self._language = None super(SpellingEnchant, self).__init__(language) def select_language(self, language): try: self._language = enchant.Dict(language) except enchant.DictNotFoundError: err = 'Enchant Backend: No language for "%s"' % (language, ) raise NoSuchLangError(err) def list_languages(self): # Note: We do NOT return enchant.list_dicts because that also returns # the enchant dict objects and not only the language identifiers. return enchant.list_languages() def check(self, word): if not word: return None return self._language.check(word) def suggest(self, fragment): suggestions = self._language.suggest(fragment) # Don't show suggestions that are invalid suggestions = [s for s in suggestions if self.check(s)] if PY2: suggestions = [s.decode('utf-8') for s in suggestions] return suggestions ================================================ FILE: tickeys/kivy/core/spelling/spelling_osxappkit.py ================================================ ''' AppKit Spelling: Implements spelling backend based on OSX's spellchecking features provided by the ApplicationKit. NOTE: Requires pyobjc and setuptools to be installed! `sudo easy_install pyobjc setuptools` Developers should read: http://developer.apple.com/mac/library/documentation/ Cocoa/Conceptual/SpellCheck/SpellCheck.html http://developer.apple.com/cocoa/pyobjc.html ''' from AppKit import NSSpellChecker, NSMakeRange from kivy.core.spelling import SpellingBase, NoSuchLangError class SpellingOSXAppKit(SpellingBase): ''' Spelling backend based on OSX's spelling features provided by AppKit. ''' def __init__(self, language=None): self._language = NSSpellChecker.alloc().init() super(SpellingOSXAppKit, self).__init__(language) def select_language(self, language): success = self._language.setLanguage_(language) if not success: err = 'AppKit Backend: No language "%s" ' % (language, ) raise NoSuchLangError(err) def list_languages(self): return list(self._language.availableLanguages()) def check(self, word): # TODO Implement this! # NSSpellChecker provides several functions that look like what we # need, but they're a) slooow and b) return a strange result. # Might be a snow leopard bug. Have to test further. # See: http://paste.pocoo.org/show/217968/ if not word: return None err = 'check() not currently supported by the OSX AppKit backend' raise NotImplementedError(err) def suggest(self, fragment): l = self._language # XXX Both ways below work on OSX 10.6. It has not been tested on any # other version, but it should work. try: # This is deprecated as of OSX 10.6, hence the try-except return list(l.guessesForWord_(fragment)) except AttributeError: # From 10.6 onwards you're supposed to do it like this: checkrange = NSMakeRange(0, len(fragment)) g = l.guessesForWordRange_inString_language_inSpellDocumentWithTag_( checkrange, fragment, l.language(), 0) # Right, this was much easier, Apple! :-) return list(g) ================================================ FILE: tickeys/kivy/core/text/__init__.py ================================================ ''' Text ==== An abstraction of text creation. Depending of the selected backend, the accuracy of text rendering may vary. .. versionchanged:: 1.5.0 :attr:`LabelBase.line_height` added. .. versionchanged:: 1.0.7 The :class:`LabelBase` does not generate any texture if the text has a width <= 1. This is the backend layer for getting text out of different text providers, you should only be using this directly if your needs aren't fulfilled by the :class:`~kivy.uix.label.Label`. Usage example:: from kivy.core.label import Label as CoreLabel ... ... my_label = CoreLabel() my_label.text = 'hello' # the label is usually not drawn until needed, so force it to draw. my_label.refresh() # Now access the texture of the label and use it wherever and # however you may please. hello_texture = my_label.texture ''' __all__ = ('LabelBase', 'Label') import re import os from functools import partial from copy import copy from kivy import kivy_data_dir from kivy.utils import platform from kivy.graphics.texture import Texture from kivy.core import core_select_lib from kivy.core.text.text_layout import layout_text, LayoutWord from kivy.resources import resource_find, resource_add_path from kivy.compat import PY2 from kivy.setupconfig import USE_SDL2 DEFAULT_FONT = 'DroidSans' FONT_REGULAR = 0 FONT_ITALIC = 1 FONT_BOLD = 2 FONT_BOLDITALIC = 3 class LabelBase(object): '''Core text label. This is the abstract class used by different backends to render text. .. warning:: The core text label can't be changed at runtime. You must recreate one. :Parameters: `font_size`: int, defaults to 12 Font size of the text `font_name`: str, defaults to DEFAULT_FONT Font name of the text `bold`: bool, defaults to False Activate "bold" text style `italic`: bool, defaults to False Activate "italic" text style `text_size`: tuple, defaults to (None, None) Add constraint to render the text (inside a bounding box). If no size is given, the label size will be set to the text size. `padding`: float, defaults to None If it's a float, it will set padding_x and padding_y `padding_x`: float, defaults to 0.0 Left/right padding `padding_y`: float, defaults to 0.0 Top/bottom padding `halign`: str, defaults to "left" Horizontal text alignment inside the bounding box `valign`: str, defaults to "bottom" Vertical text alignment inside the bounding box `shorten`: bool, defaults to False Indicate whether the label should attempt to shorten its textual contents as much as possible if a `size` is given. Setting this to True without an appropriately set size will lead to unexpected results. `shorten_from`: str, defaults to `center` The side from which we should shorten the text from, can be left, right, or center. E.g. if left, the ellipsis will appear towards the left side and it will display as much text starting from the right as possible. `split_str`: string, defaults to `' '` (space) The string to use to split the words by when shortening. If empty, we can split after every character filling up the line as much as possible. `max_lines`: int, defaults to 0 (unlimited) If set, this indicate how maximum line are allowed to render the text. Works only if a limitation on text_size is set. `mipmap` : bool, defaults to False Create a mipmap for the texture `strip` : bool, defaults to False Whether each row of text has its leading and trailing spaces stripped. If `halign` is `justify` it is implicitly True. `strip_reflow` : bool, defaults to True Whether text that has been reflowed into a second line should be striped, even if `strip` is False. This is only in effect when `size_hint_x` is not None, because otherwise lines are never split. `unicode_errors` : str, defaults to `'replace'` How to handle unicode decode errors. Can be `'strict'`, `'replace'` or `'ignore'`. .. versionchanged:: 1.9.0 `strip`, `strip_reflow`, `shorten_from`, `split_str`, and `unicode_errors` were added. .. versionchanged:: 1.9.0 `padding_x` and `padding_y` has been fixed to work as expected. In the past, the text was padded by the negative of their values. .. versionchanged:: 1.8.0 `max_lines` parameters has been added. .. versionchanged:: 1.0.8 `size` have been deprecated and replaced with `text_size`. .. versionchanged:: 1.0.7 The `valign` is now respected. This wasn't the case previously so you might have an issue in your application if you have not considered this. ''' __slots__ = ('options', 'texture', '_label', '_text_size') _cached_lines = [] _fonts = {} _fonts_cache = {} _fonts_dirs = [] _texture_1px = None def __init__( self, text='', font_size=12, font_name=DEFAULT_FONT, bold=False, italic=False, halign='left', valign='bottom', shorten=False, text_size=None, mipmap=False, color=None, line_height=1.0, strip=False, strip_reflow=True, shorten_from='center', split_str=' ', unicode_errors='replace', **kwargs): # Include system fonts_dir in resource paths. # This allows us to specify a font from those dirs. LabelBase.get_system_fonts_dir() options = {'text': text, 'font_size': font_size, 'font_name': font_name, 'bold': bold, 'italic': italic, 'halign': halign, 'valign': valign, 'shorten': shorten, 'mipmap': mipmap, 'line_height': line_height, 'strip': strip, 'strip_reflow': strip_reflow, 'shorten_from': shorten_from, 'split_str': split_str, 'unicode_errors': unicode_errors} options['color'] = color or (1, 1, 1, 1) options['padding'] = kwargs.get('padding', (0, 0)) if not isinstance(options['padding'], (list, tuple)): options['padding'] = (options['padding'], options['padding']) options['padding_x'] = kwargs.get('padding_x', options['padding'][0]) options['padding_y'] = kwargs.get('padding_y', options['padding'][1]) if 'size' in kwargs: options['text_size'] = kwargs['size'] else: if text_size is None: options['text_size'] = (None, None) else: options['text_size'] = text_size self._text_size = options['text_size'] self._text = options['text'] self._internal_size = 0, 0 # the real computed text size (inclds pad) self._cached_lines = [] self.options = options self.texture = None self.resolve_font_name() @staticmethod def register(name, fn_regular, fn_italic=None, fn_bold=None, fn_bolditalic=None): '''Register an alias for a Font. .. versionadded:: 1.1.0 If you're using a ttf directly, you might not be able to use the bold/italic properties of the ttf version. If the font is delivered in multiple files (one regular, one italic and one bold), then you need to register these files and use the alias instead. All the fn_regular/fn_italic/fn_bold parameters are resolved with :func:`kivy.resources.resource_find`. If fn_italic/fn_bold are None, fn_regular will be used instead. ''' fonts = [] for font_type in fn_regular, fn_italic, fn_bold, fn_bolditalic: if font_type is not None: font = resource_find(font_type) if font is None: raise IOError('File {0}s not found'.format(font_type)) else: fonts.append(font) else: fonts.append(fonts[-1]) # add regular font to list again LabelBase._fonts[name] = tuple(fonts) def resolve_font_name(self): options = self.options fontname = options['font_name'] fonts = self._fonts fontscache = self._fonts_cache # is the font is registered ? if fontname in fonts: # return the prefered font for the current bold/italic combinaison italic = int(options['italic']) if options['bold']: bold = FONT_BOLD else: bold = FONT_REGULAR options['font_name_r'] = fonts[fontname][italic | bold] elif fontname in fontscache: options['font_name_r'] = fontscache[fontname] else: filename = resource_find(fontname) if not filename: fontname = fontname + \ ('' if fontname.endswith('.ttf') else '.ttf') filename = resource_find(fontname) if filename is None: # XXX for compatibility, check directly in the data dir filename = os.path.join(kivy_data_dir, fontname) if not os.path.exists(filename): raise IOError('Label: File %r not found' % fontname) fontscache[fontname] = filename options['font_name_r'] = filename @staticmethod def get_system_fonts_dir(): '''Return the Directory used by the system for fonts. ''' if LabelBase._fonts_dirs: return LabelBase._fonts_dirs fdirs = [] if platform == 'linux': fdirs = [ '/usr/share/fonts/truetype', '/usr/local/share/fonts', os.path.expanduser('~/.fonts'), os.path.expanduser('~/.local/share/fonts')] elif platform == 'macosx': fdirs = ['/Library/Fonts', '/System/Library/Fonts', os.path.expanduser('~/Library/Fonts')] elif platform == 'win': fdirs = [os.environ['SYSTEMROOT'] + os.sep + 'Fonts'] elif platform == 'ios': fdirs = ['/System/Library/Fonts'] elif platform == 'android': fdirs = ['/system/fonts'] if fdirs: fdirs.append(kivy_data_dir + os.sep + 'fonts') # let's register the font dirs rdirs = [] for _dir in fdirs: if os.path.exists(_dir): resource_add_path(_dir) rdirs.append(_dir) LabelBase._fonts_dirs = rdirs return rdirs raise Exception("Unknown Platform {}".format(platform)) def get_extents(self, text): '''Return a tuple (width, height) indicating the size of the specified text''' return (0, 0) def get_cached_extents(self): '''Returns a cached version of the :meth:`get_extents` function. :: >>> func = self._get_cached_extents() >>> func >>> func('a line') (36, 18) .. warning:: This method returns a size measuring function that is valid for the font settings used at the time :meth:`get_cached_extents` was called. Any change in the font settings will render the returned function incorrect. You should only use this if you know what you're doing. .. versionadded:: 1.9.0 ''' return self.get_extents def _render_begin(self): pass def _render_text(self, text, x, y): pass def _render_end(self): pass def shorten(self, text, margin=2): ''' Shortens the text to fit into a single line by the width specified by :attr:`text_size` [0]. If :attr:`text_size` [0] is None, it returns text text unchanged. :attr:`split_str` and :attr:`shorten_from` determines how the text is shortened. :params: `text` str, the text to be shortened. `margin` int, the amount of space to leave between the margins and the text. This is in addition to :attr:`padding_x`. :retruns: the text shortened to fit into a single line. ''' textwidth = self.get_cached_extents() uw = self.text_size[0] if uw is None or not text: return text opts = self.options uw = max(0, int(uw - opts['padding_x'] * 2 - margin)) # if larger, it won't fit so don't even try extents chr = type(text) text = text.replace(chr('\n'), chr(' ')) if len(text) <= uw and textwidth(text)[0] <= uw: return text c = opts['split_str'] offset = 0 if len(c) else 1 dir = opts['shorten_from'][0] elps = textwidth('...')[0] if elps > uw: if textwidth('..')[0] <= uw: return '..' else: return '.' uw -= elps f = partial(text.find, c) f_rev = partial(text.rfind, c) # now find the first and last word e1, s2 = f(), f_rev() if dir != 'l': # center or right # no split, or the first word doesn't even fit if e1 != -1: l1 = textwidth(text[:e1])[0] l2 = textwidth(text[s2 + 1:])[0] if e1 == -1 or l1 + l2 > uw: if len(c): opts['split_str'] = '' res = self.shorten(text, margin) opts['split_str'] = c return res # at this point we do char by char so e1 must be zero if l1 <= uw: return chr('{0}...').format(text[:e1]) return chr('...') # both word fits, and there's at least on split_str if s2 == e1: # there's only on split_str return chr('{0}...{1}').format(text[:e1], text[s2 + 1:]) # both the first and last word fits, and they start/end at diff pos if dir == 'r': ee1 = f(e1 + 1) while l2 + textwidth(text[:ee1])[0] <= uw: e1 = ee1 if e1 == s2: break ee1 = f(e1 + 1) else: while True: if l1 <= l2: ee1 = f(e1 + 1) l1 = textwidth(text[:ee1])[0] if l2 + l1 > uw: break e1 = ee1 if e1 == s2: break else: ss2 = f_rev(0, s2 - offset) l2 = textwidth(text[ss2 + 1:])[0] if l2 + l1 > uw: break s2 = ss2 if e1 == s2: break else: # left # no split, or the last word doesn't even fit if s2 != -1: l2 = textwidth(text[s2 + (1 if len(c) else -1):])[0] l1 = textwidth(text[:max(0, e1)])[0] # if split_str if s2 == -1 or l2 + l1 > uw: if len(c): opts['split_str'] = '' res = self.shorten(text, margin) opts['split_str'] = c return res return chr('...') # both word fits, and there's at least on split_str if s2 == e1: # there's only on split_str return chr('{0}...{1}').format(text[:e1], text[s2 + 1:]) # both the first and last word fits, and they start/end at diff pos ss2 = f_rev(0, s2 - offset) while l1 + textwidth(text[ss2 + 1:])[0] <= uw: s2 = ss2 if s2 == e1: break ss2 = f_rev(0, s2 - offset) return chr('{0}...{1}').format(text[:e1], text[s2 + 1:]) def _render_real(self): lines = self._cached_lines options = None for line in lines: if len(line.words): # get opts from first line, first word options = line.words[0].options break if not options: # there was no text to render self._render_begin() data = self._render_end() assert(data) if data is not None and data.width > 1: self.texture.blit_data(data) return render_text = self._render_text get_extents = self.get_cached_extents() uw, uh = options['text_size'] xpad, ypad = options['padding_x'], options['padding_y'] x, y = xpad, ypad # pos in the texture iw, ih = self._internal_size # the real size of text, not texture if uw is not None: uww = uw - 2 * xpad # real width of just text w, h = self.size sw = options['space_width'] halign = options['halign'] valign = options['valign'] split = re.split pat = re.compile('( +)') self._render_begin() if valign == 'bottom': y = h - ih + ypad elif valign == 'middle': y = int((h - ih) / 2 + ypad) for layout_line in lines: # for plain label each line has only one str lw, lh = layout_line.w, layout_line.h line = '' assert len(layout_line.words) < 2 if len(layout_line.words): last_word = layout_line.words[0] line = last_word.text x = xpad if halign[0] == 'c': # center x = int((w - lw) / 2.) elif halign[0] == 'r': # right x = max(0, int(w - lw - xpad)) # right left justify # divide left over space between `spaces` # TODO implement a better method of stretching glyphs? if (uw is not None and halign[-1] == 'y' and line and not layout_line.is_last_line): # number spaces needed to fill, and remainder n, rem = divmod(max(uww - lw, 0), sw) n = int(n) words = None if n or rem: # there's no trailing space when justify is selected words = split(pat, line) if words is not None and len(words) > 1: space = type(line)(' ') # words: every even index is spaces, just add ltr n spaces for i in range(n): idx = (2 * i + 1) % (len(words) - 1) words[idx] = words[idx] + space if rem: # render the last word at the edge, also add it to line ext = get_extents(words[-1]) word = LayoutWord(last_word.options, ext[0], ext[1], words[-1]) layout_line.words.append(word) last_word.lw = uww - ext[0] # word was stretched render_text(words[-1], x + last_word.lw, y) last_word.text = line = ''.join(words[:-2]) else: last_word.lw = uww # word was stretched last_word.text = line = ''.join(words) layout_line.w = uww # the line occupies full width if len(line): layout_line.x = x layout_line.y = y render_text(line, x, y) y += lh # get data from provider data = self._render_end() assert(data) # If the text is 1px width, usually, the data is black. # Don't blit that kind of data, otherwise, you have a little black bar. if data is not None and data.width > 1: self.texture.blit_data(data) def render(self, real=False): '''Return a tuple (width, height) to create the image with the user constraints. (width, height) includes the padding. ''' if real: return self._render_real() options = copy(self.options) options['space_width'] = self.get_extents(' ')[0] options['strip'] = strip = (options['strip'] or options['halign'][-1] == 'y') uw, uh = options['text_size'] = self._text_size text = self.text if strip: text = text.strip() if uw is not None and options['shorten']: text = self.shorten(text) self._cached_lines = lines = [] if not text: return 0, 0 if uh is not None and options['valign'][-1] == 'e': # middle center = -1 # pos of newline if len(text) > 1: middle = int(len(text) // 2) l, r = text.rfind('\n', 0, middle), text.find('\n', middle) if l != -1 and r != -1: center = l if center - l <= r - center else r elif l != -1: center = l elif r != -1: center = r # if a newline split text, render from center down and up til uh if center != -1: # layout from center down until half uh w, h, clipped = layout_text(text[center + 1:], lines, (0, 0), (uw, uh / 2), options, self.get_cached_extents(), True, True) # now layout from center upwards until uh is reached w, h, clipped = layout_text(text[:center + 1], lines, (w, h), (uw, uh), options, self.get_cached_extents(), False, True) else: # if there's no new line, layout everything w, h, clipped = layout_text(text, lines, (0, 0), (uw, None), options, self.get_cached_extents(), True, True) else: # top or bottom w, h, clipped = layout_text(text, lines, (0, 0), (uw, uh), options, self.get_cached_extents(), options['valign'][-1] == 'p', True) self._internal_size = w, h if uw: w = uw if uh: h = uh if h > 1 and w < 2: w = 2 return int(w), int(h) def _texture_refresh(self, *l): self.refresh() def _texture_fill(self, texture): # second pass, render for real self.render(real=True) def refresh(self): '''Force re-rendering of the text ''' self.resolve_font_name() # first pass, calculating width/height sz = self.render() self._size_texture = sz self._size = (sz[0], sz[1]) # if no text are rendered, return nothing. width, height = self._size if width <= 1 or height <= 1: self.texture = self.texture_1px return # create a delayed texture texture = self.texture if texture is None or \ width != texture.width or \ height != texture.height: texture = Texture.create(size=(width, height), mipmap=self.options['mipmap'], callback=self._texture_fill) texture.flip_vertical() texture.add_reload_observer(self._texture_refresh) self.texture = texture else: texture.ask_update(self._texture_fill) def _get_text(self): if PY2: try: if isinstance(self._text, unicode): return self._text return self._text.decode('utf8') except AttributeError: # python 3 support return str(self._text) except UnicodeDecodeError: return self._text else: return self._text def _set_text(self, text): if text != self._text: self._text = text text = property(_get_text, _set_text, doc='Get/Set the text') label = property(_get_text, _set_text, doc='Get/Set the text') @property def texture_1px(self): if LabelBase._texture_1px is None: tex = Texture.create(size=(1, 1), colorfmt='rgba') tex.blit_buffer(b'\x00\x00\x00\x00', colorfmt='rgba') LabelBase._texture_1px = tex return LabelBase._texture_1px @property def size(self): return self._size @property def width(self): return self._size[0] @property def height(self): return self._size[1] @property def content_width(self): '''Return the content width; i.e. the width of the text without any padding.''' if self.texture is None: return 0 return self.texture.width - 2 * self.options['padding_x'] @property def content_height(self): '''Return the content height; i.e. the height of the text without any padding.''' if self.texture is None: return 0 return self.texture.height - 2 * self.options['padding_y'] @property def content_size(self): '''Return the content size (width, height)''' if self.texture is None: return (0, 0) return (self.content_width, self.content_height) @property def fontid(self): '''Return a unique id for all font parameters''' return str([self.options[x] for x in ( 'font_size', 'font_name_r', 'bold', 'italic')]) def _get_text_size(self): return self._text_size def _set_text_size(self, x): self._text_size = x text_size = property(_get_text_size, _set_text_size, doc='''Get/set the (width, height) of the ' 'contrained rendering box''') usersize = property(_get_text_size, _set_text_size, doc='''(deprecated) Use text_size instead.''') # Load the appropriate provider label_libs = [] if USE_SDL2: label_libs += [('sdl2', 'text_sdl2', 'LabelSDL2')] else: label_libs += [('pygame', 'text_pygame', 'LabelPygame')] label_libs += [ ('pil', 'text_pil', 'LabelPIL')] Label = core_select_lib('text', label_libs) if 'KIVY_DOC' not in os.environ: if not Label: from kivy.logger import Logger import sys Logger.critical('App: Unable to get a Text provider, abort.') sys.exit(1) # For the first initalization, register the default font Label.register('DroidSans', 'data/fonts/DroidSans.ttf', 'data/fonts/DroidSans-Italic.ttf', 'data/fonts/DroidSans-Bold.ttf', 'data/fonts/DroidSans-BoldItalic.ttf') ================================================ FILE: tickeys/kivy/core/text/markup.py ================================================ ''' Text Markup =========== .. versionadded:: 1.1.0 We provide a simple text-markup for inline text styling. The syntax look the same as the `BBCode `_. A tag is defined as ``[tag]``, and might have a closed tag associated: ``[/tag]``. Example of a markup text:: [b]Hello [color=ff0000]world[/b][/color] The following tags are availables: ``[b][/b]`` Activate bold text ``[i][/i]`` Activate italic text ``[font=][/font]`` Change the font ``[size=][/size]`` Change the font size ``[color=#][/color]`` Change the text color ``[ref=][/ref]`` Add an interactive zone. The reference + all the word box inside the reference will be available in :attr:`MarkupLabel.refs` ``[anchor=]`` Put an anchor in the text. You can get the position of your anchor within the text with :attr:`MarkupLabel.anchors` ``[sub][/sub]`` Display the text at a subscript position relative to the text before it. ``[sup][/sup]`` Display the text at a superscript position relative to the text before it. If you need to escape the markup from the current text, use :func:`kivy.utils.escape_markup`. ''' __all__ = ('MarkupLabel', ) import re from kivy.properties import dpi2px from kivy.parser import parse_color from kivy.logger import Logger from kivy.core.text import Label, LabelBase from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine from copy import copy from math import ceil from functools import partial # We need to do this trick when documentation is generated MarkupLabelBase = Label if Label is None: MarkupLabelBase = LabelBase class MarkupLabel(MarkupLabelBase): '''Markup text label. See module documentation for more informations. ''' def __init__(self, *largs, **kwargs): self._style_stack = {} self._refs = {} self._anchors = {} super(MarkupLabel, self).__init__(*largs, **kwargs) self._internal_size = 0, 0 self._cached_lines = [] @property def refs(self): '''Get the bounding box of all the ``[ref=...]``:: { 'refA': ((x1, y1, x2, y2), (x1, y1, x2, y2)), ... } ''' return self._refs @property def anchors(self): '''Get the position of all the ``[anchor=...]``:: { 'anchorA': (x, y), 'anchorB': (x, y), ... } ''' return self._anchors @property def markup(self): '''Return the text with all the markup splitted:: >>> MarkupLabel('[b]Hello world[/b]').markup >>> ('[b]', 'Hello world', '[/b]') ''' s = re.split('(\[.*?\])', self.label) s = [x for x in s if x != ''] return s def _push_style(self, k): if not k in self._style_stack: self._style_stack[k] = [] self._style_stack[k].append(self.options[k]) def _pop_style(self, k): if k not in self._style_stack or len(self._style_stack[k]) == 0: Logger.warning('Label: pop style stack without push') return v = self._style_stack[k].pop() self.options[k] = v def render(self, real=False): options = copy(self.options) if not real: ret = self._pre_render() else: ret = self._real_render() self.options = options return ret def _pre_render(self): # split markup, words, and lines # result: list of word with position and width/height # during the first pass, we don't care about h/valign self._cached_lines = lines = [] self._refs = {} self._anchors = {} clipped = False w = h = 0 uw, uh = self.text_size spush = self._push_style spop = self._pop_style opts = options = self.options options['_ref'] = None options['_anchor'] = None options['script'] = 'normal' shorten = options['shorten'] # if shorten, then don't split lines to fit uw, because it will be # flattened later when shortening and broken up lines if broken # mid-word will have space mid-word when lines are joined uw_temp = None if shorten else uw xpad = options['padding_x'] uhh = (None if uh is not None and options['valign'][-1] != 'p' or options['shorten'] else uh) options['strip'] = options['strip'] or options['halign'][-1] == 'y' for item in self.markup: if item == '[b]': spush('bold') options['bold'] = True self.resolve_font_name() elif item == '[/b]': spop('bold') self.resolve_font_name() elif item == '[i]': spush('italic') options['italic'] = True self.resolve_font_name() elif item == '[/i]': spop('italic') self.resolve_font_name() elif item[:6] == '[size=': item = item[6:-1] try: if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'): size = dpi2px(item[:-2], item[-2:]) else: size = int(item) except ValueError: raise size = options['font_size'] spush('font_size') options['font_size'] = size elif item == '[/size]': spop('font_size') elif item[:7] == '[color=': color = parse_color(item[7:-1]) spush('color') options['color'] = color elif item == '[/color]': spop('color') elif item[:6] == '[font=': fontname = item[6:-1] spush('font_name') options['font_name'] = fontname self.resolve_font_name() elif item == '[/font]': spop('font_name') self.resolve_font_name() elif item[:5] == '[sub]': spush('font_size') spush('script') options['font_size'] = options['font_size'] * .5 options['script'] = 'subscript' elif item == '[/sub]': spop('font_size') spop('script') elif item[:5] == '[sup]': spush('font_size') spush('script') options['font_size'] = options['font_size'] * .5 options['script'] = 'superscript' elif item == '[/sup]': spop('font_size') spop('script') elif item[:5] == '[ref=': ref = item[5:-1] spush('_ref') options['_ref'] = ref elif item == '[/ref]': spop('_ref') elif not clipped and item[:8] == '[anchor=': options['_anchor'] = item[8:-1] elif not clipped: item = item.replace('&bl;', '[').replace( '&br;', ']').replace('&', '&') opts = copy(options) extents = self.get_cached_extents() opts['space_width'] = extents(' ')[0] w, h, clipped = layout_text(item, lines, (w, h), (uw_temp, uhh), opts, extents, True, False) if len(lines): # remove any trailing spaces from the last line old_opts = self.options self.options = copy(opts) w, h, clipped = layout_text('', lines, (w, h), (uw_temp, uhh), self.options, self.get_cached_extents(), True, True) self.options = old_opts if shorten: options['_ref'] = None # no refs for you! options['_anchor'] = None w, h, lines = self.shorten_post(lines, w, h) self._cached_lines = lines # when valign is not top, for markup we layout everything (text_size[1] # is temporarily set to None) and after layout cut to size if too tall elif uh != uhh and h > uh and len(lines) > 1: if options['valign'][-1] == 'm': # bottom i = 0 while i < len(lines) - 1 and h > uh: h -= lines[i].h i += 1 del lines[:i] else: # middle i = 0 top = int(h / 2. + uh / 2.) # remove extra top portion while i < len(lines) - 1 and h > top: h -= lines[i].h i += 1 del lines[:i] i = len(lines) - 1 # remove remaining bottom portion while i and h > uh: h -= lines[i].h i -= 1 del lines[i + 1:] # now justify the text if options['halign'][-1] == 'y' and uw is not None: # XXX: update refs to justified pos # when justify, each line shouldv'e been stripped already split = partial(re.split, re.compile('( +)')) uww = uw - 2 * xpad chr = type(self.text) space = chr(' ') empty = chr('') for i in range(len(lines)): line = lines[i] words = line.words # if there's nothing to justify, we're done if (not line.w or int(uww - line.w) <= 0 or not len(words) or line.is_last_line): continue done = False parts = [None, ] * len(words) # contains words split by space idxs = [None, ] * len(words) # indices of the space in parts # break each word into spaces and add spaces until it's full # do first round of split in case we don't need to split all for w in range(len(words)): word = words[w] sw = word.options['space_width'] p = parts[w] = split(word.text) idxs[w] = [v for v in range(len(p)) if p[v].startswith(' ')] # now we have the indices of the spaces in split list for k in idxs[w]: # try to add single space at each space if line.w + sw > uww: done = True break line.w += sw word.lw += sw p[k] += space if done: break # there's not a single space in the line? if not any(idxs): continue # now keep adding spaces to already split words until done while not done: for w in range(len(words)): if not idxs[w]: continue word = words[w] sw = word.options['space_width'] p = parts[w] for k in idxs[w]: # try to add single space at each space if line.w + sw > uww: done = True break line.w += sw word.lw += sw p[k] += space if done: break # if not completely full, push last words to right edge diff = int(uww - line.w) if diff > 0: # find the last word that had a space for w in range(len(words) - 1, -1, -1): if not idxs[w]: continue break old_opts = self.options self.options = word.options word = words[w] # split that word into left/right and push right till uww l_text = empty.join(parts[w][:idxs[w][-1]]) r_text = empty.join(parts[w][idxs[w][-1]:]) left = LayoutWord(word.options, self.get_extents(l_text)[0], word.lh, l_text) right = LayoutWord(word.options, self.get_extents(r_text)[0], word.lh, r_text) left.lw = max(left.lw, word.lw + diff - right.lw) self.options = old_opts # now put words back together with right/left inserted for k in range(len(words)): if idxs[k]: words[k].text = empty.join(parts[k]) words[w] = right words.insert(w, left) else: for k in range(len(words)): if idxs[k]: words[k].text = empty.join(parts[k]) line.w = uww w = max(w, uww) self._internal_size = w, h if uw: w = uw if uh: h = uh if h > 1 and w < 2: w = 2 if w < 1: w = 1 if h < 1: h = 1 return int(w), int(h) def _real_render(self): lines = self._cached_lines options = None for line in lines: if len(line.words): # get opts from first line, first word options = line.words[0].options break if not options: # there was no text to render self._render_begin() data = self._render_end() assert(data) if data is not None and data.width > 1: self.texture.blit_data(data) return old_opts = self.options render_text = self._render_text xpad, ypad = options['padding_x'], options['padding_y'] x, y = xpad, ypad # pos in the texture iw, ih = self._internal_size # the real size of text, not texture w, h = self.size halign = options['halign'] valign = options['valign'] refs = self._refs anchors = self._anchors self._render_begin() if valign == 'bottom': y = h - ih + ypad elif valign == 'middle': y = int((h - ih) / 2 + ypad) for layout_line in lines: # for plain label each line has only one str lw, lh = layout_line.w, layout_line.h x = xpad if halign[0] == 'c': # center x = int((w - lw) / 2.) elif halign[0] == 'r': # right x = max(0, int(w - lw - xpad)) layout_line.x = x layout_line.y = y psp = pph = 0 for word in layout_line.words: options = self.options = word.options # the word height is not scaled by line_height, only lh was wh = options['line_height'] * word.lh # calculate sub/super script pos if options['script'] == 'superscript': script_pos = max(0, psp if psp else self.get_descent()) psp = script_pos pph = wh elif options['script'] == 'subscript': script_pos = min(lh - wh, ((psp + pph) - wh) if pph else (lh - wh)) pph = wh psp = script_pos else: script_pos = (lh - wh) / 1.25 psp = pph = 0 if len(word.text): render_text(word.text, x, y + script_pos) # should we record refs ? ref = options['_ref'] if ref is not None: if not ref in refs: refs[ref] = [] refs[ref].append((x, y, x + word.lw, y + wh)) # Should we record anchors? anchor = options['_anchor'] if anchor is not None: if not anchor in anchors: anchors[anchor] = (x, y) x += word.lw y += lh self.options = old_opts # get data from provider data = self._render_end() assert(data) # If the text is 1px width, usually, the data is black. # Don't blit that kind of data, otherwise, you have a little black bar. if data is not None and data.width > 1: self.texture.blit_data(data) def shorten_post(self, lines, w, h, margin=2): ''' Shortens the text to a single line according to the label options. This function operates on a text that has already been laid out because for markup, parts of text can have different size and options. If :attr:`text_size` [0] is None, the lines are returned unchanged. Otherwise, the lines are converted to a single line fitting within the constrained width, :attr:`text_size` [0]. :params: `lines`: list of `LayoutLine` instances describing the text. `w`: int, the width of the text in lines, including padding. `h`: int, the height of the text in lines, including padding. `margin` int, the additional space left on the sides. This is in addition to :attr:`padding_x`. :returns: 3-tuple of (xw, h, lines), where w, and h is similar to the input and contains the resulting width / height of the text, including padding. lines, is a list containing a single `LayoutLine`, which contains the words for the line. ''' def n(line, c): ''' A function similar to text.find, except it's an iterator that returns successive occurrences of string c in list line. line is not a string, but a list of LayoutWord instances that we walk from left to right returning the indices of c in the words as we encounter them. Note that the options can be different among the words. :returns: 3-tuple: the index of the word in line, the index of the occurrence in word, and the extents (width) of the combined words until this occurrence, not including the occurrence char. If no more are found it returns (-1, -1, total_w) where total_w is the full width of all the words. ''' total_w = 0 for w in range(len(line)): word = line[w] if not word.lw: continue f = partial(word.text.find, c) i = f() while i != -1: self.options = word.options yield w, i, total_w + self.get_extents(word.text[:i])[0] i = f(i + 1) self.options = word.options total_w += self.get_extents(word.text)[0] yield -1, -1, total_w # this should never be reached, really def p(line, c): ''' Similar to the `n` function, except it returns occurrences of c from right to left in the list, line, similar to rfind. ''' total_w = 0 offset = 0 if len(c) else 1 for w in range(len(line) - 1, -1, -1): word = line[w] if not word.lw: continue f = partial(word.text.rfind, c) i = f() while i != -1: self.options = word.options yield (w, i, total_w + self.get_extents(word.text[i + 1:])[0]) if i: i = f(0, i - offset) else: if not c: self.options = word.options yield (w, -1, total_w + self.get_extents(word.text)[0]) break self.options = word.options total_w += self.get_extents(word.text)[0] yield -1, -1, total_w # this should never be reached, really def n_restricted(line, uw, c): ''' Similar to the function `n`, except it only returns the first occurrence and it's not an iterator. Furthermore, if the first occurrence doesn't fit within width uw, it returns the index of whatever amount of text will still fit in uw. :returns: similar to the function `n`, except it's a 4-tuple, with the last element a boolean, indicating if we had to clip the text to fit in uw (True) or if the whole text until the first occurrence fitted in uw (False). ''' total_w = 0 if not len(line): return 0, 0, 0 for w in range(len(line)): word = line[w] f = partial(word.text.find, c) self.options = word.options extents = self.get_cached_extents() i = f() if i != -1: ww = extents(word.text[:i])[0] if i != -1 and total_w + ww <= uw: # found and it fits return w, i, total_w + ww, False elif i == -1: ww = extents(word.text)[0] if total_w + ww <= uw: # wasn't found and all fits total_w += ww continue i = len(word.text) # now just find whatever amount of the word does fit e = 0 while e != i and total_w + extents(word.text[:e])[0] <= uw: e += 1 e = max(0, e - 1) return w, e, total_w + extents(word.text[:e])[0], True return -1, -1, total_w, False def p_restricted(line, uw, c): ''' Similar to `n_restricted`, except it returns the first occurrence starting from the right, like `p`. ''' total_w = 0 if not len(line): return 0, 0, 0 for w in range(len(line) - 1, -1, -1): word = line[w] f = partial(word.text.rfind, c) self.options = word.options extents = self.get_cached_extents() i = f() if i != -1: ww = extents(word.text[i + 1:])[0] if i != -1 and total_w + ww <= uw: # found and it fits return w, i, total_w + ww, False elif i == -1: ww = extents(word.text)[0] if total_w + ww <= uw: # wasn't found and all fits total_w += ww continue # now just find whatever amount of the word does fit s = len(word.text) - 1 while s >= 0 and total_w + extents(word.text[s:])[0] <= uw: s -= 1 return w, s, total_w + extents(word.text[s + 1:])[0], True return -1, -1, total_w, False textwidth = self.get_cached_extents() uw = self.text_size[0] if uw is None: return w, h, lines old_opts = copy(self.options) uw = max(0, int(uw - old_opts['padding_x'] * 2 - margin)) chr = type(self.text) ssize = textwidth(' ') c = old_opts['split_str'] line_height = old_opts['line_height'] xpad, ypad = old_opts['padding_x'], old_opts['padding_y'] dir = old_opts['shorten_from'][0] # flatten lines into single line line = [] last_w = 0 for l in range(len(lines)): # concatenate (non-empty) inside lines with a space this_line = lines[l] if last_w and this_line.w and not this_line.line_wrap: line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' '))) last_w = this_line.w or last_w for word in this_line.words: if word.lw: line.append(word) # if that fits, just return the flattened line lw = sum([word.lw for word in line]) if lw <= uw: lh = max([word.lh for word in line] + [0]) * line_height return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line)] # find the size of ellipsis that'll fit elps_s = textwidth('...') if elps_s[0] > uw: # even ellipsis didn't fit... s = textwidth('..') if s[0] <= uw: return (s[0] + 2 * xpad, s[1] * line_height + 2 * ypad, [LayoutLine(0, 0, s[0], s[1], 1, 0, [LayoutWord(old_opts, s[0], s[1], '..')])]) else: s = textwidth('.') return (s[0] + 2 * xpad, s[1] * line_height + 2 * ypad, [LayoutLine(0, 0, s[0], s[1], 1, 0, [LayoutWord(old_opts, s[0], s[1], '.')])]) elps = LayoutWord(old_opts, elps_s[0], elps_s[1], '...') uw -= elps_s[0] # now find the first left and right words that fit w1, e1, l1, clipped1 = n_restricted(line, uw, c) w2, s2, l2, clipped2 = p_restricted(line, uw, c) if dir != 'l': # center or right line1 = None if clipped1 or clipped2 or l1 + l2 > uw: # if either was clipped or both don't fit, just take first if len(c): self.options = old_opts old_opts['split_str'] = '' res = self.shorten_post(lines, w, h, margin) self.options['split_str'] = c return res line1 = line[:w1] last_word = line[w1] last_text = last_word.text[:e1] self.options = last_word.options s = self.get_extents(last_text) line1.append(LayoutWord(last_word.options, s[0], s[1], last_text)) elif (w1, e1) == (-1, -1): # this shouldn't occur line1 = line if line1: line1.append(elps) lw = sum([word.lw for word in line1]) lh = max([word.lh for word in line1]) * line_height self.options = old_opts return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)] # now we know that both the first and last word fit, and that # there's at least one instances of the split_str in the line if (w1, e1) != (w2, s2): # more than one split_str if dir == 'r': f = n(line, c) # iterator assert next(f)[:-1] == (w1, e1) # first word should match ww1, ee1, l1 = next(f) while l2 + l1 <= uw: w1, e1 = ww1, ee1 ww1, ee1, l1 = next(f) if (w1, e1) == (w2, s2): break else: # center f = n(line, c) # iterator f_inv = p(line, c) # iterator assert next(f)[:-1] == (w1, e1) assert next(f_inv)[:-1] == (w2, s2) while True: if l1 <= l2: ww1, ee1, l1 = next(f) # hypothesize that next fit if l2 + l1 > uw: break w1, e1 = ww1, ee1 if (w1, e1) == (w2, s2): break else: ww2, ss2, l2 = next(f_inv) if l2 + l1 > uw: break w2, s2 = ww2, ss2 if (w1, e1) == (w2, s2): break else: # left line1 = [elps] if clipped1 or clipped2 or l1 + l2 > uw: # if either was clipped or both don't fit, just take last if len(c): self.options = old_opts old_opts['split_str'] = '' res = self.shorten_post(lines, w, h, margin) self.options['split_str'] = c return res first_word = line[w2] first_text = first_word.text[s2 + 1:] self.options = first_word.options s = self.get_extents(first_text) line1.append(LayoutWord(first_word.options, s[0], s[1], first_text)) line1.extend(line[w2 + 1:]) elif (w1, e1) == (-1, -1): # this shouldn't occur line1 = line if len(line1) != 1: lw = sum([word.lw for word in line1]) lh = max([word.lh for word in line1]) * line_height self.options = old_opts return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)] # now we know that both the first and last word fit, and that # there's at least one instances of the split_str in the line if (w1, e1) != (w2, s2): # more than one split_str f_inv = p(line, c) # iterator assert next(f_inv)[:-1] == (w2, s2) # last word should match ww2, ss2, l2 = next(f_inv) while l2 + l1 <= uw: w2, s2 = ww2, ss2 ww2, ss2, l2 = next(f_inv) if (w1, e1) == (w2, s2): break # now add back the left half line1 = line[:w1] last_word = line[w1] last_text = last_word.text[:e1] self.options = last_word.options s = self.get_extents(last_text) if len(last_text): line1.append(LayoutWord(last_word.options, s[0], s[1], last_text)) line1.append(elps) # now add back the right half first_word = line[w2] first_text = first_word.text[s2 + 1:] self.options = first_word.options s = self.get_extents(first_text) if len(first_text): line1.append(LayoutWord(first_word.options, s[0], s[1], first_text)) line1.extend(line[w2 + 1:]) lw = sum([word.lw for word in line1]) lh = max([word.lh for word in line1]) * line_height self.options = old_opts return lw + 2 * xpad, lh + 2 * ypad, [LayoutLine(0, 0, lw, lh, 1, 0, line1)] ================================================ FILE: tickeys/kivy/core/text/text_layout.pxd ================================================ cdef class LayoutWord: cdef public object text cdef public int lw, lh cdef public dict options cdef class LayoutLine: cdef public int x, y, w, h cdef public int line_wrap # whether this line wraps from last line cdef public int is_last_line # in a paragraph cdef public list words ================================================ FILE: tickeys/kivy/core/text/text_pil.py ================================================ ''' Text PIL: Draw text with PIL ''' __all__ = ('LabelPIL', ) try: from PIL import Image, ImageFont, ImageDraw except: raise from kivy.compat import text_type from kivy.core.text import LabelBase from kivy.core.image import ImageData # used for fetching extends before creature image surface default_font = ImageFont.load_default() class LabelPIL(LabelBase): _cache = {} def _select_font(self): fontsize = int(self.options['font_size']) fontname = self.options['font_name_r'] try: id = '%s.%s' % (text_type(fontname), text_type(fontsize)) except UnicodeDecodeError: id = '%s.%s' % (fontname, fontsize) if not id in self._cache: font = ImageFont.truetype(fontname, fontsize) self._cache[id] = font return self._cache[id] def get_extents(self, text): font = self._select_font() w, h = font.getsize(text) return w, h def get_cached_extents(self): return self._select_font().getsize def _render_begin(self): # create a surface, context, font... self._pil_im = Image.new('RGBA', self._size) self._pil_draw = ImageDraw.Draw(self._pil_im) def _render_text(self, text, x, y): color = tuple([int(c * 255) for c in self.options['color']]) self._pil_draw.text((int(x), int(y)), text, font=self._select_font(), fill=color) def _render_end(self): data = ImageData(self._size[0], self._size[1], self._pil_im.mode.lower(), self._pil_im.tostring()) del self._pil_im del self._pil_draw return data ================================================ FILE: tickeys/kivy/core/text/text_pygame.py ================================================ ''' Text Pygame: Draw text with pygame ''' __all__ = ('LabelPygame', ) from kivy.compat import PY2 from kivy.core.text import LabelBase from kivy.core.image import ImageData try: import pygame except: raise pygame_cache = {} pygame_font_handles = {} pygame_cache_order = [] # init pygame font try: pygame.ftfont.init() except: pygame.font.init() class LabelPygame(LabelBase): def _get_font_id(self): if PY2: try: return '|'.join([unicode(self.options[x]) for x in ('font_size', 'font_name_r', 'bold', 'italic')]) except UnicodeDecodeError: pass return '|'.join([str(self.options[x]) for x in ('font_size', 'font_name_r', 'bold', 'italic')]) def _get_font(self): fontid = self._get_font_id() if fontid not in pygame_cache: # try first the file if it's a filename font_handle = fontobject = None fontname = self.options['font_name_r'] ext = fontname.rsplit('.', 1) if len(ext) == 2: # try to open the font if it has an extension font_handle = open(fontname, 'rb') fontobject = pygame.font.Font(font_handle, int(self.options['font_size'])) # fallback to search a system font if fontobject is None: # try to search the font font = pygame.font.match_font( self.options['font_name_r'].replace(' ', ''), bold=self.options['bold'], italic=self.options['italic']) # fontobject fontobject = pygame.font.Font(font, int(self.options['font_size'])) pygame_cache[fontid] = fontobject pygame_font_handles[fontid] = font_handle pygame_cache_order.append(fontid) # to prevent too much file open, limit the number of opened fonts to 64 while len(pygame_cache_order) > 64: popid = pygame_cache_order.pop(0) del pygame_cache[popid] font_handle = pygame_font_handles.pop(popid) if font_handle is not None: font_handle.close() return pygame_cache[fontid] def get_ascent(self): return self._get_font().get_ascent() def get_descent(self): return self._get_font().get_descent() def get_extents(self, text): return self._get_font().size(text) def get_cached_extents(self): return self._get_font().size def _render_begin(self): self._pygame_surface = pygame.Surface(self._size, pygame.SRCALPHA, 32) self._pygame_surface.fill((0, 0, 0, 0)) def _render_text(self, text, x, y): font = self._get_font() color = [c * 255 for c in self.options['color']] color[0], color[2] = color[2], color[0] try: text = font.render(text, True, color) self._pygame_surface.blit(text, (x, y), None, pygame.BLEND_RGBA_ADD) except pygame.error: pass def _render_end(self): w, h = self._size data = ImageData(w, h, 'rgba', self._pygame_surface.get_buffer().raw) del self._pygame_surface return data ================================================ FILE: tickeys/kivy/core/text/text_sdl2.py ================================================ ''' SDL2 text provider ================== Based on SDL2 + SDL2_ttf ''' __all__ = ('LabelSDL2', ) from kivy.compat import PY2 from kivy.core.text import LabelBase from kivy.core.text._text_sdl2 import (_SurfaceContainer, _get_extents, _get_fontdescent, _get_fontascent) class LabelSDL2(LabelBase): def _get_font_id(self): if PY2: try: return '|'.join([unicode(self.options[x]) for x in ('font_size', 'font_name_r', 'bold', 'italic')]) except UnicodeDecodeError: pass return '|'.join([str(self.options[x]) for x in ('font_size', 'font_name_r', 'bold', 'italic')]) def get_extents(self, text): try: if PY2: text = text.encode('UTF-8') except: pass return _get_extents(self, text) def get_descent(self): return _get_fontdescent(self) def get_ascent(self): return _get_fontascent(self) def _render_begin(self): self._surface = _SurfaceContainer(self._size[0], self._size[1]) def _render_text(self, text, x, y): self._surface.render(self, text, x, y) def _render_end(self): return self._surface.get_data() ================================================ FILE: tickeys/kivy/core/video/__init__.py ================================================ ''' Video ===== Core class for reading video files and managing the :class:`kivy.graphics.texture.Texture` video. .. versionchanged:: 1.8.0 There is now 2 distinct Gstreamer implementation: one using Gi/Gst working for both Python 2+3 with Gstreamer 1.0, and one using PyGST working only for Python 2 + Gstreamer 0.10. If you have issue with GStreamer, have a look at :ref:`gstreamer-compatibility` .. note:: Recording is not supported. ''' __all__ = ('VideoBase', 'Video') from kivy.clock import Clock from kivy.core import core_select_lib from kivy.event import EventDispatcher from kivy.logger import Logger from kivy.compat import PY2 class VideoBase(EventDispatcher): '''VideoBase, a class used to implement a video reader. :Parameters: `filename` : str Filename of the video. Can be a file or an URI. `eos` : str, defaults to 'pause' Action to take when EOS is hit. Can be one of 'pause', 'stop' or 'loop'. .. versionchanged:: unknown added 'pause' `async` : bool, defaults to True Load the video asynchronously (may be not supported by all providers). `autoplay` : bool, defaults to False Auto play the video on init. :Events: `on_eos` Fired when EOS is hit. `on_load` Fired when the video is loaded and the texture is available. `on_frame` Fired when a new frame is written to the texture. ''' __slots__ = ('_wantplay', '_buffer', '_filename', '_texture', '_volume', 'eos', '_state', '_async', '_autoplay') __events__ = ('on_eos', 'on_load', 'on_frame') def __init__(self, **kwargs): kwargs.setdefault('filename', None) kwargs.setdefault('eos', 'stop') kwargs.setdefault('async', True) kwargs.setdefault('autoplay', False) super(VideoBase, self).__init__() self._wantplay = False self._buffer = None self._filename = None self._texture = None self._volume = 1. self._state = '' self._autoplay = kwargs.get('autoplay') self._async = kwargs.get('async') self.eos = kwargs.get('eos') if self.eos == 'pause': Logger.warning("'pause' is deprecated. Use 'stop' instead.") self.eos = 'stop' self.filename = kwargs.get('filename') Clock.schedule_interval(self._update, 1 / 30.) if self._autoplay: self.play() def __del__(self): self.unload() def on_eos(self): pass def on_load(self): pass def on_frame(self): pass def _get_filename(self): return self._filename def _set_filename(self, filename): if filename == self._filename: return self.unload() self._filename = filename if self._filename is None: return self.load() filename = property(lambda self: self._get_filename(), lambda self, x: self._set_filename(x), doc='Get/set the filename/uri of the current video') def _get_position(self): return 0 def _set_position(self, pos): self.seek(pos) position = property(lambda self: self._get_position(), lambda self, x: self._set_position(x), doc='Get/set the position in the video (in seconds)') def _get_volume(self): return self._volume def _set_volume(self, volume): self._volume = volume volume = property(lambda self: self._get_volume(), lambda self, x: self._set_volume(x), doc='Get/set the volume in the video (1.0 = 100%)') def _get_duration(self): return 0 duration = property(lambda self: self._get_duration(), doc='Get the video duration (in seconds)') def _get_texture(self): return self._texture texture = property(lambda self: self._get_texture(), doc='Get the video texture') def _get_state(self): return self._state state = property(lambda self: self._get_state(), doc='Get the video playing status') def _do_eos(self, *args): ''' .. versionchanged:: 1.4.0 Now dispatches the `on_eos` event. ''' if self.eos == 'pause': self.pause() elif self.eos == 'stop': self.stop() elif self.eos == 'loop': self.position = 0 self.play() self.dispatch('on_eos') def _update(self, dt): '''Update the video content to texture. ''' pass def seek(self, percent): '''Move on percent position''' pass def stop(self): '''Stop the video playing''' self._state = '' def pause(self): '''Pause the video .. versionadded:: 1.4.0 ''' self._state = 'paused' def play(self): '''Play the video''' self._state = 'playing' def load(self): '''Load the video from the current filename''' pass def unload(self): '''Unload the actual video''' self._state = '' # Load the appropriate provider video_providers = [] try: from kivy.lib.gstplayer import GstPlayer # NOQA video_providers += [('gstplayer', 'video_gstplayer', 'VideoGstplayer')] except ImportError: #video_providers += [('gi', 'video_gi', 'VideoGi')] if PY2: # if peoples do not have gi, fallback on pygst, only for python2 video_providers += [ ('pygst', 'video_pygst', 'VideoPyGst')] video_providers += [ ('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'), ('ffpyplayer', 'video_ffpyplayer', 'VideoFFPy'), ('pyglet', 'video_pyglet', 'VideoPyglet'), ('null', 'video_null', 'VideoNull')] Video = core_select_lib('video', video_providers) ================================================ FILE: tickeys/kivy/core/video/video_ffmpeg.py ================================================ ''' FFmpeg video abstraction ======================== .. versionadded:: 1.0.8 This abstraction requires ffmpeg python extensions. We have made a special extension that is used for the android platform but can also be used on x86 platforms. The project is available at:: http://github.com/tito/ffmpeg-android The extension is designed for implementing a video player. Refer to the documentation of the ffmpeg-android project for more information about the requirements. ''' try: import ffmpeg except: raise from kivy.core.video import VideoBase from kivy.graphics.texture import Texture class VideoFFMpeg(VideoBase): def __init__(self, **kwargs): self._do_load = False self._player = None super(VideoFFMpeg, self).__init__(**kwargs) def unload(self): if self._player: self._player.stop() self._player = None self._state = '' self._do_load = False def load(self): self.unload() def play(self): if self._player: self.unload() self._player = ffmpeg.FFVideo(self._filename) self._do_load = True def stop(self): self.unload() def seek(self, percent): if self._player is None: return self._player.seek(percent) def _do_eos(self): self.unload() self.dispatch('on_eos') super(VideoFFMpeg, self)._do_eos() def _update(self, dt): if self._do_load: self._player.open() self._do_load = False return player = self._player if player is None: return if player.is_open is False: self._do_eos() return frame = player.get_next_frame() if frame is None: return # first time we got a frame, we know that video is readed now. if self._texture is None: self._texture = Texture.create(size=( player.get_width(), player.get_height()), colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') if self._texture: self._texture.blit_buffer(frame) self.dispatch('on_frame') def _get_duration(self): if self._player is None: return 0 return self._player.get_duration() def _get_position(self): if self._player is None: return 0 return self._player.get_position() def _get_volume(self): if self._player is None: return 0 self._volume = self._player.get_volume() return self._volume def _set_volume(self, volume): if self._player is None: return self._player.set_volume(volume) ================================================ FILE: tickeys/kivy/core/video/video_ffpyplayer.py ================================================ ''' FFmpeg based video abstraction ============================== To use, you need to install ffpyplyaer and have a compiled ffmpeg shared library. https://github.com/matham/ffpyplayer The docs there describe how to set this up. But briefly, first you need to compile ffmpeg using the shared flags while disabling the static flags (you'll probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/. Similarly, you should download SDL. Now, you should a ffmpeg and sdl directory. In each, you should have a include, bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files, while bin contains the actual dlls. The include directory holds the headers. The bin directory is only needed if the shared libraries are not already on the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to the ffmpeg, and SDL directories, respectively. (If you're using SDL2, the include directory will contain a directory called SDL2, which then holds the headers). Once defined, download the ffpyplayer git and run python setup.py build_ext --inplace Finally, before running you need to ensure that ffpyplayer is in python's path. ..Note:: When kivy exits by closing the window while the video is playing, it appears that the __del__method of VideoFFPy is not called. Because of this the VideoFFPy object is not properly deleted when kivy exits. The consequence is that because MediaPlayer creates internal threads which do not have their daemon flag set, when the main threads exists it'll hang and wait for the other MediaPlayer threads to exit. But since __del__ is not called to delete the MediaPlayer object, those threads will remain alive hanging kivy. What this means is that you have to be sure to delete the MediaPlayer object before kivy exits by setting it to None. ''' __all__ = ('VideoFFPy', ) try: import ffpyplayer from ffpyplayer.player import MediaPlayer from ffpyplayer.tools import set_log_callback, loglevels, get_log_callback except: raise from threading import Thread from kivy.clock import Clock, mainthread from kivy.logger import Logger from kivy.core.video import VideoBase from kivy.graphics import Rectangle, BindTexture from kivy.graphics.texture import Texture from kivy.graphics.fbo import Fbo from kivy.weakmethod import WeakMethod import time Logger.info('VideoFFPy: Using ffpyplayer {}'.format(ffpyplayer.version)) logger_func = {'quiet': Logger.critical, 'panic': Logger.critical, 'fatal': Logger.critical, 'error': Logger.error, 'warning': Logger.warning, 'info': Logger.info, 'verbose': Logger.debug, 'debug': Logger.debug} def _log_callback(message, level): message = message.strip() if message: logger_func[level]('ffpyplayer: {}'.format(message)) if not get_log_callback(): set_log_callback(_log_callback) class VideoFFPy(VideoBase): YUV_RGB_FS = """ $HEADER$ uniform sampler2D tex_y; uniform sampler2D tex_u; uniform sampler2D tex_v; void main(void) { float y = texture2D(tex_y, tex_coord0).r; float u = texture2D(tex_u, tex_coord0).r - 0.5; float v = texture2D(tex_v, tex_coord0).r - 0.5; float r = y + 1.402 * v; float g = y - 0.344 * u - 0.714 * v; float b = y + 1.772 * u; gl_FragColor = vec4(r, g, b, 1.0); } """ def __init__(self, **kwargs): self._ffplayer = None self._thread = None self._next_frame = None self._ffplayer_need_quit = False self._callback_ref = WeakMethod(self._player_callback) self._trigger = Clock.create_trigger(self._redraw) super(VideoFFPy, self).__init__(**kwargs) def __del__(self): self.unload() if self._log_callback_set: set_log_callback(None) def _player_callback(self, selector, value): if self._ffplayer is None: return if selector == 'quit': def close(*args): self.unload() Clock.schedule_once(close, 0) def _get_position(self): if self._ffplayer is not None: return self._ffplayer.get_pts() return 0 def _set_position(self, pos): self.seek(pos) def _get_volume(self): if self._ffplayer is not None: self._volume = self._ffplayer.get_volume() return self._volume def _set_volume(self, volume): self._volume = volume if self._ffplayer is not None: self._ffplayer.set_volume(volume) def _get_duration(self): if self._ffplayer is None: return 0 return self._ffplayer.get_metadata()['duration'] @mainthread def _do_eos(self): if self.eos == 'pause': self.pause() elif self.eos == 'stop': self.stop() elif self.eos == 'loop': self.position = 0 self.dispatch('on_eos') @mainthread def _change_state(self, state): self._state = state def _redraw(self, *args): if not self._ffplayer: return next_frame = self._next_frame if not next_frame: return img, pts = next_frame if img.get_size() != self._size or self._texture is None: self._size = w, h = img.get_size() if self._out_fmt == 'yuv420p': w2 = int(w / 2) h2 = int(h / 2) self._tex_y = Texture.create( size=(w, h), colorfmt='luminance') self._tex_u = Texture.create( size=(w2, h2), colorfmt='luminance') self._tex_v = Texture.create( size=(w2, h2), colorfmt='luminance') self._fbo = fbo = Fbo(size=self._size) with fbo: BindTexture(texture=self._tex_u, index=1) BindTexture(texture=self._tex_v, index=2) Rectangle(size=fbo.size, texture=self._tex_y) fbo.shader.fs = VideoFFPy.YUV_RGB_FS fbo['tex_y'] = 0 fbo['tex_u'] = 1 fbo['tex_v'] = 2 self._texture = fbo.texture else: self._texture = Texture.create(size=self._size, colorfmt='rgba') # XXX FIXME #self.texture.add_reload_observer(self.reload_buffer) self._texture.flip_vertical() self.dispatch('on_load') if self._texture: if self._out_fmt == 'yuv420p': dy, du, dv, _ = img.to_memoryview() self._tex_y.blit_buffer(dy, colorfmt='luminance') self._tex_u.blit_buffer(du, colorfmt='luminance') self._tex_v.blit_buffer(dv, colorfmt='luminance') else: self._texture.blit_buffer( img.to_memoryview()[0], colorfmt='rgba') self._fbo.ask_update() self._fbo.draw() self.dispatch('on_frame') def _next_frame_run(self): ffplayer = self._ffplayer sleep = time.sleep trigger = self._trigger did_dispatch_eof = False # fast path, if the source video is yuv420p, we'll use a glsl shader for # buffer conversion to rgba while not self._ffplayer_need_quit: src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt') if not src_pix_fmt: sleep(0.005) continue if src_pix_fmt == 'yuv420p': self._out_fmt = 'yuv420p' ffplayer.set_output_pix_fmt(self._out_fmt) self._ffplayer.toggle_pause() break if self._ffplayer_need_quit: return # wait until loaded or failed, shouldn't take long, but just to make # sure metadata is available. s = time.clock() while not self._ffplayer_need_quit: if ffplayer.get_metadata()['src_vid_size'] != (0, 0): break # XXX if will fail later then? if time.clock() - s > 10.: break sleep(0.005) if self._ffplayer_need_quit: return # we got all the informations, now, get the frames :) self._change_state('playing') while not self._ffplayer_need_quit: t1 = time.time() frame, val = ffplayer.get_frame() t2 = time.time() if val == 'eof': sleep(0.2) if not did_dispatch_eof: self._do_eos() did_dispatch_eof = True elif val == 'paused': did_dispatch_eof = False sleep(0.2) else: did_dispatch_eof = False if frame: self._next_frame = frame trigger() else: val = val if val else (1 / 30.) sleep(val) def seek(self, percent): if self._ffplayer is None: return self._ffplayer.seek(percent * self._ffplayer.get_metadata() ['duration'], relative=False) self._next_frame = None def stop(self): self.unload() def pause(self): if self._ffplayer and self._state != 'paused': self._ffplayer.toggle_pause() self._state = 'paused' def play(self): if self._ffplayer and self._state == 'paused': self._ffplayer.toggle_pause() self._state = 'playing' return self.load() self._out_fmt = 'rgba' ff_opts = { 'paused': True, 'out_fmt': self._out_fmt } self._ffplayer = MediaPlayer( self._filename, callback=self._callback_ref, thread_lib='SDL', loglevel='info', ff_opts=ff_opts) self._thread = Thread(target=self._next_frame_run, name='Next frame') self._thread.daemon = True self._thread.start() def load(self): self.unload() def unload(self): Clock.unschedule(self._redraw) self._ffplayer_need_quit = True if self._thread: self._thread.join() self._thread = None if self._ffplayer: self._ffplayer = None self._next_frame = None self._size = (0, 0) self._state = '' self._ffplayer_need_quit = False ================================================ FILE: tickeys/kivy/core/video/video_gi.py ================================================ ''' Video GI ======== Implementation of VideoBase with using pygi / gstreamer. Pygi is both compatible with Python 2 and 3. ''' # # Important notes: you must take care of glib event + python. If you connect() # directly an event to a python object method, the object will be ref, and will # be never unref. # To prevent memory leak, you must connect() to a func, and you might want to # pass the referenced object with weakref() # from gi.repository import Gst from functools import partial from os.path import realpath from threading import Lock from weakref import ref from kivy.compat import PY2 from kivy.core.video import VideoBase from kivy.graphics.texture import Texture from kivy.logger import Logger from kivy.support import install_gobject_iteration from ctypes import Structure, c_void_p, c_int, string_at import atexit if PY2: from urllib import pathname2url else: from urllib.request import pathname2url # initialize the video/gi. if the older version is used, don't use video_gi. Gst.init(None) version = Gst.version() if version < (1, 0, 0, 0): raise Exception('Cannot use video_gi, Gstreamer < 1.0 is not supported.') Logger.info('VideoGi: Using Gstreamer {}'.format( '.'.join(['{}'.format(x) for x in Gst.version()]))) install_gobject_iteration() class _MapInfo(Structure): _fields_ = [ ('memory', c_void_p), ('flags', c_int), ('data', c_void_p)] # we don't care about the rest def _gst_new_buffer(obj, appsink): obj = obj() if not obj: return with obj._buffer_lock: obj._buffer = obj._appsink.emit('pull-sample') return False def _on_gst_message(bus, message): Logger.trace('VideoGi: (bus) {}'.format(message)) # log all error messages if message.type == Gst.MessageType.ERROR: error, debug = list(map(str, message.parse_error())) Logger.error('VideoGi: {}'.format(error)) Logger.debug('VideoGi: {}'.format(debug)) def _on_gst_eos(obj, *largs): obj = obj() if not obj: return obj._do_eos() def _on_videogi_unref(obj): if obj in VideoGi._instances: VideoGi._instances.remove(obj) class VideoGi(VideoBase): _instances = [] def __init__(self, **kwargs): self._buffer_lock = Lock() self._buffer = None self._texture = None self._gst_init() wk = ref(self, _on_videogi_unref) VideoGi._instances.append(wk) super(VideoGi, self).__init__(**kwargs) def _gst_init(self): # self._appsink will receive the buffers so we can upload them to GPU self._appsink = Gst.ElementFactory.make('appsink', '') self._appsink.props.caps = Gst.caps_from_string( 'video/x-raw,format=RGB') self._appsink.props.async = True self._appsink.props.drop = True self._appsink.props.qos = True self._appsink.props.emit_signals = True self._appsink.connect('new-sample', partial( _gst_new_buffer, ref(self))) # playbin, takes care of all, loading, playing, etc. self._playbin = Gst.ElementFactory.make('playbin', 'playbin') self._playbin.props.video_sink = self._appsink # gstreamer bus, to attach and listen to gst messages self._bus = self._playbin.get_bus() self._bus.add_signal_watch() self._bus.connect('message', _on_gst_message) self._bus.connect('message::eos', partial( _on_gst_eos, ref(self))) def _update_texture(self, sample): # texture will be updated with newest buffer/frame # read the data from the buffer memory mapinfo = data = None try: buf = sample.get_buffer() result, mapinfo = buf.map(Gst.MapFlags.READ) # We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0 # related bug report: # https://bugzilla.gnome.org/show_bug.cgi?id=678663 # ie: mapinfo.data is normally a char*, but here, we have an int # So right now, we use ctypes instead to read the mapinfo ourself. addr = mapinfo.__hash__() c_mapinfo = _MapInfo.from_address(addr) # now get the memory data = string_at(c_mapinfo.data, mapinfo.size) finally: if mapinfo is not None: buf.unmap(mapinfo) # upload the data to the GPU info = sample.get_caps().get_structure(0) size = info.get_value('width'), info.get_value('height') # texture is not allocated yet, create it first if not self._texture: self._texture = Texture.create(size=size, colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') if self._texture: self._texture.blit_buffer(data, size=size, colorfmt='rgb') def _update(self, dt): buf = None with self._buffer_lock: buf = self._buffer self._buffer = None if buf is not None: self._update_texture(buf) self.dispatch('on_frame') def unload(self): self._playbin.set_state(Gst.State.NULL) self._buffer = None self._texture = None def load(self): Logger.debug('VideoGi: Load <{}>'.format(self._filename)) self._playbin.set_state(Gst.State.NULL) self._playbin.props.uri = self._get_uri() self._playbin.set_state(Gst.State.READY) def stop(self): self._state = '' self._playbin.set_state(Gst.State.PAUSED) def pause(self): self._state = 'paused' self._playbin.set_state(Gst.State.PAUSED) def play(self): self._state = 'playing' self._playbin.set_state(Gst.State.PLAYING) def seek(self, percent): seek_t = percent * self._get_duration() * 10e8 seek_format = Gst.Format.TIME seek_flags = Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT self._playbin.seek_simple(seek_format, seek_flags, seek_t) #if pipeline is not playing, we need to pull pre-roll to update frame if not self._state == 'playing': with self._buffer_lock: self._buffer = self._appsink.emit('pull-preroll') def _get_uri(self): uri = self.filename if not uri: return if not '://' in uri: uri = 'file:' + pathname2url(realpath(uri)) return uri def _get_position(self): try: ret, value = self._appsink.query_position(Gst.Format.TIME) if ret: return value / float(Gst.SECOND) except: pass return -1 def _get_duration(self): try: ret, value = self._playbin.query_duration(Gst.Format.TIME) if ret: return value / float(Gst.SECOND) except: pass return -1 def _get_volume(self): self._volume = self._playbin.props.volume return self._volume def _set_volume(self, volume): self._playbin.props.volume = volume self._volume = volume @atexit.register def video_gi_clean(): # if we leave the python process with some video running, we can hit a # segfault. This is forcing the stop/unload of all remaining videos before # exiting the python process. for weakvideo in VideoGi._instances: video = weakvideo() if video: video.stop() video.unload() ================================================ FILE: tickeys/kivy/core/video/video_gstplayer.py ================================================ ''' Video Gstplayer =============== .. versionadded:: 1.8.0 Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer` This player is the prefered player, using Gstreamer 1.0, working on both Python 2 and 3. ''' from kivy.lib.gstplayer import GstPlayer, get_gst_version from kivy.graphics.texture import Texture from kivy.core.video import VideoBase from kivy.logger import Logger from kivy.clock import Clock from kivy.compat import PY2 from threading import Lock from functools import partial from os.path import realpath from weakref import ref if PY2: from urllib import pathname2url else: from urllib.request import pathname2url Logger.info('VideoGstplayer: Using Gstreamer {}'.format( '.'.join(map(str, get_gst_version())))) def _on_gstplayer_buffer(video, width, height, data): video = video() # if we still receive the video but no more player, remove it. if not video: return with video._buffer_lock: video._buffer = (width, height, data) def _on_gstplayer_message(mtype, message): if mtype == 'error': Logger.error('VideoGstplayer: {}'.format(message)) elif mtype == 'warning': Logger.warning('VideoGstplayer: {}'.format(message)) elif mtype == 'info': Logger.info('VideoGstplayer: {}'.format(message)) class VideoGstplayer(VideoBase): def __init__(self, **kwargs): self.player = None self._buffer = None self._buffer_lock = Lock() super(VideoGstplayer, self).__init__(**kwargs) def _on_gst_eos_sync(self): Clock.schedule_once(self._do_eos, 0) def load(self): Logger.debug('VideoGstplayer: Load <{}>'.format(self._filename)) uri = self._get_uri() wk_self = ref(self) self.player_callback = partial(_on_gstplayer_buffer, wk_self) self.player = GstPlayer(uri, self.player_callback, self._on_gst_eos_sync, _on_gstplayer_message) self.player.load() def unload(self): if self.player: self.player.unload() self.player = None with self._buffer_lock: self._buffer = None self._texture = None def stop(self): super(VideoGstplayer, self).stop() self.player.stop() def pause(self): super(VideoGstplayer, self).pause() self.player.pause() def play(self): super(VideoGstplayer, self).play() self.player.set_volume(self.volume) self.player.play() def seek(self, percent): self.player.seek(percent) def _get_position(self): return self.player.get_position() def _get_duration(self): return self.player.get_duration() def _get_volume(self): return self._volume def _set_volume(self, value): self._volume = value if self.player: self.player.set_volume(self._volume) def _update(self, dt): buf = None with self._buffer_lock: buf = self._buffer self._buffer = None if buf is not None: self._update_texture(buf) self.dispatch('on_frame') def _update_texture(self, buf): width, height, data = buf # texture is not allocated yet, create it first if not self._texture: self._texture = Texture.create(size=(width, height), colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') if self._texture: self._texture.blit_buffer( data, size=(width, height), colorfmt='rgb') def _get_uri(self): uri = self.filename if not uri: return if not '://' in uri: uri = 'file:' + pathname2url(realpath(uri)) return uri ================================================ FILE: tickeys/kivy/core/video/video_null.py ================================================ ''' VideoNull: empty implementation of VideoBase for the no provider case ''' from kivy.core.video import VideoBase class VideoNull(VideoBase): '''VideoBase implementation when there is no provider. ''' pass ================================================ FILE: tickeys/kivy/core/video/video_pyglet.py ================================================ ''' VideoPyglet: implementation of VideoBase with Pyglet ''' import pyglet from kivy.core.video import VideoBase #have to set these before importing pyglet.gl #otherwise pyglet creates a seperate gl context and fails # on error checks becasue we use pygame window pyglet.options['shadow_window'] = False pyglet.options['debug_gl'] = False import pyglet.gl class FakePygletContext: # another pyglet fix, because pyglet has a bugfix which is a bad hacked, # it checks for context._workaround_unpack_row_length..but we're using # the implicit context form pyglet or glut window # this means we cant have a pyglet window provider though! if we do, # this will break pyglet window context _workaround_unpack_row_length = False pyglet.gl.current_context = FakePygletContext() class VideoPyglet(VideoBase): '''VideoBase implementation using Pyglet ''' def unload(self): self.player = None self._source = None self._fbo = None def load(self): self.unload() # make sure we unload an resources #load media file and set size of video self._source = source = pyglet.media.load(self._filename) self._format = source.video_format self.size = (self._format.width, self._format.height) #load pyglet player and have it play teh video we loaded self._player = None self._player = pyglet.media.Player() self._player.queue(source) self.play() self.stop() # we have to keep track of tie ourselves.. # at least its the only way i can get pyglet player to restart, # _player.time does not get reset when you do seek(0) for soe reason, # and is read only self.time = self._player.time def _update(self, dt): if self._source.duration - self.time < 0.1: # we are at the end self.seek(0) if self.state == 'playing': # keep track of time into video self.time += dt # required by pyglet video if not in pyglet window self._player.dispatch_events(dt) if self._player.get_texture(): # TODO: blit the pyglet texture to our own texture. assert('TODO') def stop(self): self._player.pause() super(VideoPyglet, self).stop() def play(self): self._player.play() super(VideoPyglet, self).play() def seek(self, percent): t = self._source.duration * percent self.time = t self._player.seek(t) self.stop() def _get_position(self): if self._player: return self.time def _get_duration(self): if self._source: return self._source.duration def _get_volume(self): if self._player: return self._player.volume return 0 def _set_volume(self, volume): if self._player: self._player.volume = volume self.dispatch('on_frame') ================================================ FILE: tickeys/kivy/core/video/video_pygst.py ================================================ ''' Video PyGst =========== Implementation of a VideoBase using PyGST. This module is compatible only with Python 2. ''' # # Important notes: you must take care of glib event + python. If you connect() # directly an event to a python object method, the object will be ref, and will # be never unref. # To prevent memory leak, you must connect() to a func, and you might want to # pass the referenced object with weakref() # import pygst if not hasattr(pygst, '_gst_already_checked'): found = False for version in ('1.0', '0.10'): try: pygst.require(version) found = True break except: continue if found: pygst._gst_already_checked = True else: raise Exception('Unable to find a valid Gstreamer version to use') import gst from functools import partial from os import path from threading import Lock from urllib import pathname2url from weakref import ref from kivy.core.video import VideoBase from kivy.graphics.texture import Texture from kivy.logger import Logger from kivy.support import install_gobject_iteration install_gobject_iteration() def _gst_new_buffer(obj, appsink): obj = obj() if not obj: return with obj._buffer_lock: obj._buffer = obj._appsink.emit('pull-buffer') def _on_gst_message(bus, message): Logger.trace('VideoPyGst: (bus) %s' % str(message)) # log all error messages if message.type == gst.MESSAGE_ERROR: error, debug = list(map(str, message.parse_error())) Logger.error('VideoPyGst: %s' % error) Logger.debug('VideoPyGst: %s' % debug) def _on_gst_eos(obj, *largs): obj = obj() if not obj: return obj._do_eos() class VideoPyGst(VideoBase): def __init__(self, **kwargs): self._buffer_lock = Lock() self._buffer = None self._texture = None self._gst_init() super(VideoPyGst, self).__init__(**kwargs) def _gst_init(self): # self._appsink will receive the buffers so we can upload them to GPU self._appsink = gst.element_factory_make('appsink', '') self._appsink.set_property('caps', gst.Caps( 'video/x-raw-rgb,red_mask=(int)0xff0000,' 'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff')) self._appsink.set_property('async', True) self._appsink.set_property('drop', True) self._appsink.set_property('qos', True) self._appsink.set_property('emit-signals', True) self._appsink.connect('new-buffer', partial( _gst_new_buffer, ref(self))) # playbin, takes care of all, loading, playing, etc. # XXX playbin2 have some issue when playing some video or streaming :/ self._playbin = gst.element_factory_make('playbin', 'playbin') self._playbin.set_property('video-sink', self._appsink) # gstreamer bus, to attach and listen to gst messages self._bus = self._playbin.get_bus() self._bus.add_signal_watch() self._bus.connect('message', _on_gst_message) self._bus.connect('message::eos', partial( _on_gst_eos, ref(self))) def _update_texture(self, buf): # texture will be updated with newest buffer/frame size = None caps = buf.get_caps() _s = caps.get_structure(0) size = _s['width'], _s['height'] if not self._texture: # texture is not allocated yet, so create it first self._texture = Texture.create(size=size, colorfmt='rgb') self._texture.flip_vertical() self.dispatch('on_load') # upload texture data to GPU if self._texture: self._texture.blit_buffer(buf.data, size=size, colorfmt='rgb') def _update(self, dt): buf = None with self._buffer_lock: buf = self._buffer self._buffer = None if buf is not None: self._update_texture(buf) self.dispatch('on_frame') def unload(self): self._playbin.set_state(gst.STATE_NULL) self._buffer = None self._texture = None def load(self): Logger.debug('VideoPyGst: Load <%s>' % self._filename) self._playbin.set_state(gst.STATE_NULL) self._playbin.set_property('uri', self._get_uri()) self._playbin.set_state(gst.STATE_READY) def stop(self): '''.. versionchanged:: 1.4.0''' self._state = '' self._playbin.set_state(gst.STATE_PAUSED) def pause(self): '''.. versionadded:: 1.4.0''' self._state = 'paused' self._playbin.set_state(gst.STATE_PAUSED) def play(self): self._state = 'playing' self._playbin.set_state(gst.STATE_PLAYING) def seek(self, percent): seek_t = percent * self._get_duration() * 10e8 seek_format = gst.FORMAT_TIME seek_flags = gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT self._playbin.seek_simple(seek_format, seek_flags, seek_t) #if pipeline is not playing, we need to pull pre-roll to update frame if not self._state == 'playing': with self._buffer_lock: self._buffer = self._appsink.emit('pull-preroll') def _get_uri(self): uri = self.filename if not uri: return if not '://' in uri: uri = 'file:' + pathname2url(path.realpath(uri)) return uri def _get_position(self): try: value, fmt = self._appsink.query_position(gst.FORMAT_TIME) return value / 10e8 except: return -1 def _get_duration(self): try: return self._playbin.query_duration(gst.FORMAT_TIME)[0] / 10e8 except: return -1 def _get_volume(self): self._volume = self._playbin.get_property('volume') return self._volume def _set_volume(self, volume): self._playbin.set_property('volume', volume) self._volume = volume ================================================ FILE: tickeys/kivy/core/window/__init__.py ================================================ # pylint: disable=W0611 # coding: utf-8 ''' Window ====== Core class for creating the default Kivy window. Kivy supports only one window per application: please don't try to create more than one. ''' __all__ = ('Keyboard', 'WindowBase', 'Window') from os.path import join, exists from os import getcwd from kivy.core import core_select_lib from kivy.clock import Clock from kivy.config import Config from kivy.logger import Logger from kivy.base import EventLoop, stopTouchApp from kivy.modules import Modules from kivy.event import EventDispatcher from kivy.properties import ListProperty, ObjectProperty, AliasProperty, \ NumericProperty, OptionProperty, StringProperty, BooleanProperty from kivy.utils import platform, reify from kivy.context import get_current_context from kivy.uix.behaviors import FocusBehavior from kivy.setupconfig import USE_SDL2 from kivy.graphics.transformation import Matrix # late import VKeyboard = None android = None class Keyboard(EventDispatcher): '''Keyboard interface that is returned by :meth:`WindowBase.request_keyboard`. When you request a keyboard, you'll get an instance of this class. Whatever the keyboard input is (system or virtual keyboard), you'll receive events through this instance. :Events: `on_key_down`: keycode, text, modifiers Fired when a new key is pressed down `on_key_up`: keycode Fired when a key is released (up) Here is an example of how to request a Keyboard in accordance with the current configuration: .. include:: ../../examples/widgets/keyboardlistener.py :literal: ''' # Keycodes mapping, between str <-> int. Theses keycode are # currently taken from pygame.key. But when a new provider will be # used, it must do the translation to these keycodes too. keycodes = { # specials keys 'backspace': 8, 'tab': 9, 'enter': 13, 'rshift': 303, 'shift': 304, 'alt': 308, 'rctrl': 306, 'lctrl': 305, 'super': 309, 'alt-gr': 307, 'compose': 311, 'pipe': 310, 'capslock': 301, 'escape': 27, 'spacebar': 32, 'pageup': 280, 'pagedown': 281, 'end': 279, 'home': 278, 'left': 276, 'up': 273, 'right': 275, 'down': 274, 'insert': 277, 'delete': 127, 'numlock': 300, 'print': 144, 'screenlock': 145, 'pause': 19, # a-z keys 'a': 97, 'b': 98, 'c': 99, 'd': 100, 'e': 101, 'f': 102, 'g': 103, 'h': 104, 'i': 105, 'j': 106, 'k': 107, 'l': 108, 'm': 109, 'n': 110, 'o': 111, 'p': 112, 'q': 113, 'r': 114, 's': 115, 't': 116, 'u': 117, 'v': 118, 'w': 119, 'x': 120, 'y': 121, 'z': 122, # 0-9 keys '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, # numpad 'numpad0': 256, 'numpad1': 257, 'numpad2': 258, 'numpad3': 259, 'numpad4': 260, 'numpad5': 261, 'numpad6': 262, 'numpad7': 263, 'numpad8': 264, 'numpad9': 265, 'numpaddecimal': 266, 'numpaddivide': 267, 'numpadmul': 268, 'numpadsubstract': 269, 'numpadadd': 270, 'numpadenter': 271, # F1-15 'f1': 282, 'f2': 283, 'f3': 284, 'f4': 285, 'f5': 286, 'f6': 287, 'f7': 288, 'f8': 289, 'f9': 290, 'f10': 291, 'f11': 292, 'f12': 293, 'f13': 294, 'f14': 295, 'f15': 296, # other keys '(': 40, ')': 41, '[': 91, ']': 93, '{': 123, '}': 125, ':': 59, ';': 59, '=': 61, '+': 43, '-': 45, '_': 95, '/': 47, '*': 42, '?': 47, '`': 96, '~': 126, '´': 180, '¦': 166, '\\': 92, '|': 124, '"': 34, "'": 39, ',': 44, '.': 46, '<': 60, '>': 62, '@': 64, '!': 33, '#': 35, '$': 36, '%': 37, '^': 94, '&': 38, '¬': 172, '¨': 168, '…': 8230, 'ù': 249, 'à': 224, 'é': 233, 'è': 232, } __events__ = ('on_key_down', 'on_key_up', 'on_textinput') def __init__(self, **kwargs): super(Keyboard, self).__init__() #: Window which the keyboard is attached too self.window = kwargs.get('window', None) #: Callback that will be called when the keyboard is released self.callback = kwargs.get('callback', None) #: Target that have requested the keyboard self.target = kwargs.get('target', None) #: VKeyboard widget, if allowed by the configuration self.widget = kwargs.get('widget', None) def on_key_down(self, keycode, text, modifiers): pass def on_key_up(self, keycode): pass def on_textinput(self, text): pass def release(self): '''Call this method to release the current keyboard. This will ensure that the keyboard is no longer attached to your callback.''' if self.window: self.window.release_keyboard(self.target) def _on_window_textinput(self, instance, text): return self.dispatch('on_textinput', text) def _on_window_key_down(self, instance, keycode, scancode, text, modifiers): keycode = (keycode, self.keycode_to_string(keycode)) if text == '\x04': Window.trigger_keyboard_height() return return self.dispatch('on_key_down', keycode, text, modifiers) def _on_window_key_up(self, instance, keycode, *largs): keycode = (keycode, self.keycode_to_string(keycode)) return self.dispatch('on_key_up', keycode) def _on_vkeyboard_key_down(self, instance, keycode, text, modifiers): if keycode is None: keycode = text.lower() keycode = (self.string_to_keycode(keycode), keycode) return self.dispatch('on_key_down', keycode, text, modifiers) def _on_vkeyboard_key_up(self, instance, keycode, text, modifiers): if keycode is None: keycode = text keycode = (self.string_to_keycode(keycode), keycode) return self.dispatch('on_key_up', keycode) def _on_vkeyboard_textinput(self, instance, text): return self.dispatch('on_textinput', text) def string_to_keycode(self, value): '''Convert a string to a keycode number according to the :attr:`Keyboard.keycodes`. If the value is not found in the keycodes, it will return -1. ''' return Keyboard.keycodes.get(value, -1) def keycode_to_string(self, value): '''Convert a keycode number to a string according to the :attr:`Keyboard.keycodes`. If the value is not found in the keycodes, it will return ''. ''' keycodes = list(Keyboard.keycodes.values()) if value in keycodes: return list(Keyboard.keycodes.keys())[keycodes.index(value)] return '' class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `borderless`: str, one of ('0', '1') Set the window border state. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. versionadded:: 1.9.0 `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. ''' __instance = None __initialized = False _fake_fullscreen = False _density = 1 # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently active. ''' def _get_size(self): r = self._rotation w, h = self._size if self._density != 1: w, h = self._win._get_gl_size() if self.softinput_mode == 'resize': h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): _size = self._size if self._density != 1: _size = self._win._get_gl_size() r = self._rotation if r == 0 or r == 180: return _size[0] return _size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation _size = self._size if self._density != 1: _size = self._win._get_gl_size() kb = self.keyboard_height if self.softinput_mode == 'resize' else 0 if r == 0 or r == 180: return _size[1] - kb return _size[0] - kb height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' softinput_mode = OptionProperty('', options=('', 'pan', 'scale', 'resize')) '''This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.9.0 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. ''' _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == 'android': return self._get_android_kheight() if platform == 'ios': return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=('_keyboard_changed',)) '''Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.9.0 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == 'resize': return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' borderless = BooleanProperty(False) '''When set to True, this property removes the window border/decoration. .. versionadded:: 1.9.0 :attr:`borderless` is a :class:`BooleanProperty`, defaults to False. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 .. note:: The 'fake' option has been deprecated, use the :attr:`borderless` property instead. ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' @property def __self__(self): return self top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ( 'on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_textinput', 'on_dropfile', 'on_request_close', 'on_joy_axis', 'on_joy_hat', 'on_joy_ball', 'on_joy_button_down', "on_joy_button_up") def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): force = kwargs.pop('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not force: return self.initialized = False self._is_desktop = Config.getboolean('kivy', 'desktop') # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger( self._upd_kbd_height, .5) # set the default window parameter according to the configuration if 'borderless' not in kwargs: kwargs['borderless'] = Config.getboolean('graphics', 'borderless') if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window self._bind_create_window() self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() # mark as initialized self.initialized = True def _bind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) def _unbind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.unbind(**{prop: self.trigger_create_window}) def toggle_fullscreen(self): '''Toggle between fullscreen and windowed mode. .. deprecated:: 1.9.0 Use :attr:`fullscreen` instead. ''' pass def maximize(self): '''Maximizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: maximize() is not implemented in the current ' 'window provider.') def minimize(self): '''Minimizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: minimize() is not implemented in the current ' 'window provider.') def restore(self): '''Restores the size and position of a maximized or minimized window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: restore() is not implemented in the current ' 'window provider.') def hide(self): '''Hides the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: hide() is not implemented in the current ' 'window provider.') def show(self): '''Shows the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: show() is not implemented in the current ' 'window provider.') def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! ''' # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) # ensure the window creation will not be called twice if platform in ('android', 'ios'): self._unbind_create_window() if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == 'linux' or Window.__class__.__name__ == 'WindowSDL': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget, canvas=None): '''Add a widget to a window''' widget.parent = self self.children.insert(0, widget) canvas = self.canvas.before if canvas == 'before' else \ self.canvas.after if canvas == 'after' else self.canvas canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from a window ''' if not widget in self.children: return self.children.remove(widget) if widget.canvas in self.canvas.children: self.canvas.remove(widget.canvas) elif widget.canvas in self.canvas.after.children: self.canvas.after.remove(widget.canvas) elif widget.canvas in self.canvas.before.children: self.canvas.before.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with the background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window. .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def _apply_transform(self, m): return m def get_window_matrix(self, x=0, y=0): m = Matrix() m.translate(x, y, 0) return m def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. ''' if me.is_touch: w, h = self.system_size if platform == 'ios': w, h = self.size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) FocusBehavior._handle_post_on_touch_up(me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch event moves (changes location). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch event is released (terminated). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized.''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size if self._density != 1: w, h = self.size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2., h / 2. r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == 'scale': _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot{:04d}.png'): '''Save the actual displayed image in a file ''' i = 0 path = None if name != 'screenshot{:04d}.png': _ext = name.split('.')[-1] name = ''.join((name[:-(len(_ext) + 1)], '{:04d}.', _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen has been rotated. ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): '''Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. ''' pass def on_mouse_down(self, x, y, button, modifiers): '''Event called when the mouse is used (pressed/released)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_joy_axis(self, stickid, axisid, value): '''Event called when a joystick has a stick or other axis moved .. versionadded:: 1.9.0''' pass def on_joy_hat(self, stickid, hatid, value): '''Event called when a joystick has a hat/dpad moved .. versionadded:: 1.9.0''' pass def on_joy_ball(self, stickid, ballid, value): '''Event called when a joystick has a ball moved .. versionadded:: 1.9.0''' pass def on_joy_button_down(self, stickid, buttonid): '''Event called when a joystick has a button pressed .. versionadded:: 1.9.0''' pass def on_joy_button_up(self, stickid, buttonid): '''Event called when a joystick has a button released .. versionadded:: 1.9.0''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == 'darwin' if WindowBase.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch('on_request_close', source='keyboard'): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean('kivy', 'exit_on_escape') def __exit(section, name, value): WindowBase.__dict__['on_keyboard'].exit_on_escape = \ Config.getboolean('kivy', 'exit_on_escape') Config.add_callback(__exit, 'kivy', 'exit_on_escape') def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is released (same arguments as on_keyboard) ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_textinput(self, text): '''Event called whem text: i.e. alpha numeric non control keys or set of keys is entered. As it is not gaurenteed whether we get one character or multiple ones, this event supports handling multiple characters. ..versionadded:: 1.9.0 ''' pass def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event currently works with sdl2 window provider, on pygame window provider and MacOSX with a patched version of pygame. This event is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up, on_textinput=sk._on_window_textinput) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi', 'systemanddock', 'systemandmulti'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == 'systemanddock': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'systemandmulti': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( 'Window: virtual keyboard %sallowed, %s, %s' % ( '' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type='text'): '''.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. .. note:: The behavior of this function is heavily influenced by the current `keyboard_mode`. Please see the Config's :ref:`configuration tokens ` section for more information. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up, on_textinput=keyboard._on_vkeyboard_textinput) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up, on_textinput=keyboard._on_window_textinput) self.bind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up, on_textinput=keyboard._on_window_textinput) return keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True #: Instance of a :class:`WindowBase` implementation window_impl = [] if platform == 'linux': window_impl += [('egl_rpi', 'window_egl_rpi', 'WindowEglRpi')] if USE_SDL2: window_impl += [('sdl2', 'window_sdl2', 'WindowSDL')] else: window_impl += [ ('pygame', 'window_pygame', 'WindowPygame')] if platform == 'linux': window_impl += [('x11', 'window_x11', 'WindowX11')] Window = core_select_lib('window', window_impl, True) ================================================ FILE: tickeys/kivy/core/window/window_egl_rpi.py ================================================ ''' EGL Rpi Window: EGL Window provider, specialized for the Pi Inspired by: rpi_vid_core + JF002 rpi kivy repo ''' __all__ = ('WindowEglRpi', ) from kivy.logger import Logger from kivy.core.window import WindowBase from kivy.base import EventLoop from kivy.lib.vidcore_lite import bcm, egl class WindowEglRpi(WindowBase): def create_window(self): bcm.host_init() w, h = bcm.graphics_get_display_size(0) Logger.debug('Window: Actual display size: {}x{}'.format( w, h)) self._size = w, h self._create_window(w, h) self._create_egl_context(self.win, 0) super(WindowEglRpi, self).create_window() def _create_window(self, w, h): dst = bcm.Rect(0, 0, w, h) src = bcm.Rect(0, 0, w << 16, h << 16) display = egl.bcm_display_open(0) update = egl.bcm_update_start(0) element = egl.bcm_element_add(update, display, 0, dst, src) self.win = egl.NativeWindow(element, w, h) egl.bcm_update_submit_sync(update) def _create_egl_context(self, win, flags): api = egl._constants.EGL_OPENGL_ES_API c = egl._constants attribs = [ c.EGL_RED_SIZE, 8, c.EGL_GREEN_SIZE, 8, c.EGL_BLUE_SIZE, 8, c.EGL_ALPHA_SIZE, 8, c.EGL_DEPTH_SIZE, 16, c.EGL_STENCIL_SIZE, 8, c.EGL_SURFACE_TYPE, c.EGL_WINDOW_BIT, c.EGL_NONE] attribs_context = [c.EGL_CONTEXT_CLIENT_VERSION, 2, c.EGL_NONE] display = egl.GetDisplay(c.EGL_DEFAULT_DISPLAY) egl.Initialise(display) egl.BindAPI(c.EGL_OPENGL_ES_API) egl.GetConfigs(display) config = egl.ChooseConfig(display, attribs, 1)[0] surface = egl.CreateWindowSurface(display, config, win) context = egl.CreateContext(display, config, None, attribs_context) egl.MakeCurrent(display, surface, surface, context) self.egl_info = (display, surface, context) egl.MakeCurrent(display, surface, surface, context) def close(self): egl.Terminate(self.egl_info[0]) def flip(self): egl.SwapBuffers(self.egl_info[0], self.egl_info[1]) def _mainloop(self): EventLoop.idle() def mainloop(self): while not EventLoop.quit and EventLoop.status == 'started': try: self._mainloop() except BaseException as inst: raise ''' # use exception manager first r = ExceptionManager.handle_exception(inst) if r == ExceptionManager.RAISE: #stopTouchApp() raise else: pass ''' ================================================ FILE: tickeys/kivy/core/window/window_pygame.py ================================================ ''' Window Pygame: windowing provider based on Pygame ''' __all__ = ('WindowPygame', ) # fail early if possible import pygame from kivy.compat import PY2 from kivy.core.window import WindowBase from kivy.core import CoreCriticalException from os import environ from os.path import exists, join from kivy.config import Config from kivy import kivy_data_dir from kivy.base import ExceptionManager from kivy.logger import Logger from kivy.base import stopTouchApp, EventLoop from kivy.utils import platform, deprecated from kivy.resources import resource_find from kivy.clock import Clock try: android = None if platform == 'android': import android except ImportError: pass # late binding glReadPixels = GL_RGBA = GL_UNSIGNED_BYTE = None class WindowPygame(WindowBase): def create_window(self, *largs): # ensure the mouse is still not up after window creation, otherwise, we # have some weird bugs self.dispatch('on_mouse_up', 0, 0, 'all', []) # force display to show (available only for fullscreen) displayidx = Config.getint('graphics', 'display') if not 'SDL_VIDEO_FULLSCREEN_HEAD' in environ and displayidx != -1: environ['SDL_VIDEO_FULLSCREEN_HEAD'] = '%d' % displayidx # init some opengl, same as before. self.flags = pygame.HWSURFACE | pygame.OPENGL | pygame.DOUBLEBUF # right now, activate resizable window only on linux. # on window / macosx, the opengl context is lost, and we need to # reconstruct everything. Check #168 for a state of the work. if platform in ('linux', 'macosx', 'win') and \ Config.getboolean('graphics', 'resizable'): self.flags |= pygame.RESIZABLE try: pygame.display.init() except pygame.error as e: raise CoreCriticalException(e.message) multisamples = Config.getint('graphics', 'multisamples') if multisamples > 0: pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1) pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, multisamples) pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 16) pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 1) pygame.display.set_caption(self.title) if self.position == 'auto': self._pos = None elif self.position == 'custom': self._pos = self.left, self.top else: raise ValueError('position token in configuration accept only ' '"auto" or "custom"') if self._fake_fullscreen: if not self.borderless: self.fullscreen = self._fake_fullscreen = False elif not self.fullscreen or self.fullscreen == 'auto': self.borderless = self._fake_fullscreen = False if self.fullscreen == 'fake': self.borderless = self._fake_fullscreen = True Logger.warning("The 'fake' fullscreen option has been " "deprecated, use Window.borderless or the " "borderless Config option instead.") if self.fullscreen == 'fake' or self.borderless: Logger.debug('WinPygame: Set window to borderless mode.') self.flags |= pygame.NOFRAME # If no position set in borderless mode, we always need # to set the position. So use 0, 0. if self._pos is None: self._pos = (0, 0) environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos elif self.fullscreen in ('auto', True): Logger.debug('WinPygame: Set window to fullscreen mode') self.flags |= pygame.FULLSCREEN elif self._pos is not None: environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos # never stay with a None pos, application using w.center will be fired. self._pos = (0, 0) # prepare keyboard repeat_delay = int(Config.get('kivy', 'keyboard_repeat_delay')) repeat_rate = float(Config.get('kivy', 'keyboard_repeat_rate')) pygame.key.set_repeat(repeat_delay, int(1000. / repeat_rate)) # set window icon before calling set_mode try: filename_icon = self.icon or Config.get('kivy', 'window_icon') if filename_icon == '': logo_size = 32 if platform == 'macosx': logo_size = 512 elif platform == 'win': logo_size = 64 filename_icon = 'kivy-icon-{}.png'.format(logo_size) filename_icon = resource_find( join(kivy_data_dir, 'logo', filename_icon)) self.set_icon(filename_icon) except: Logger.exception('Window: cannot set icon') # try to use mode with multisamples try: self._pygame_set_mode() except pygame.error as e: if multisamples: Logger.warning('WinPygame: Video: failed (multisamples=%d)' % multisamples) Logger.warning('WinPygame: trying without antialiasing') pygame.display.gl_set_attribute( pygame.GL_MULTISAMPLEBUFFERS, 0) pygame.display.gl_set_attribute( pygame.GL_MULTISAMPLESAMPLES, 0) multisamples = 0 try: self._pygame_set_mode() except pygame.error as e: raise CoreCriticalException(e.message) else: raise CoreCriticalException(e.message) info = pygame.display.Info() self._size = (info.current_w, info.current_h) #self.dispatch('on_resize', *self._size) # in order to debug futur issue with pygame/display, let's show # more debug output. Logger.debug('Window: Display driver ' + pygame.display.get_driver()) Logger.debug('Window: Actual window size: %dx%d', info.current_w, info.current_h) if platform != 'android': # unsupported platform, such as android that doesn't support # gl_get_attribute. Logger.debug( 'Window: Actual color bits r%d g%d b%d a%d', pygame.display.gl_get_attribute(pygame.GL_RED_SIZE), pygame.display.gl_get_attribute(pygame.GL_GREEN_SIZE), pygame.display.gl_get_attribute(pygame.GL_BLUE_SIZE), pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE)) Logger.debug( 'Window: Actual depth bits: %d', pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE)) Logger.debug( 'Window: Actual stencil bits: %d', pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE)) Logger.debug( 'Window: Actual multisampling samples: %d', pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES)) super(WindowPygame, self).create_window() # set mouse visibility pygame.mouse.set_visible( Config.getboolean('graphics', 'show_cursor')) # if we are on android platform, automaticly create hooks if android: from kivy.support import install_android install_android() def close(self): pygame.display.quit() self.dispatch('on_close') def on_title(self, instance, value): if self.initialized: pygame.display.set_caption(self.title) def set_icon(self, filename): if not exists(filename): return False try: if platform == 'win': try: if self._set_icon_win(filename): return True except: # fallback on standard loading then. pass # for all others platform, or if the ico is not available, use the # default way to set it. self._set_icon_standard(filename) super(WindowPygame, self).set_icon(filename) except: Logger.exception('WinPygame: unable to set icon') def _set_icon_standard(self, filename): if PY2: try: im = pygame.image.load(filename) except UnicodeEncodeError: im = pygame.image.load(filename.encode('utf8')) else: im = pygame.image.load(filename) if im is None: raise Exception('Unable to load window icon (not found)') pygame.display.set_icon(im) def _set_icon_win(self, filename): # ensure the window ico is ended by ico if not filename.endswith('.ico'): filename = '{}.ico'.format(filename.rsplit('.', 1)[0]) if not exists(filename): return False import win32api import win32gui import win32con hwnd = pygame.display.get_wm_info()['window'] icon_big = win32gui.LoadImage( None, filename, win32con.IMAGE_ICON, 48, 48, win32con.LR_LOADFROMFILE) icon_small = win32gui.LoadImage( None, filename, win32con.IMAGE_ICON, 16, 16, win32con.LR_LOADFROMFILE) win32api.SendMessage( hwnd, win32con.WM_SETICON, win32con.ICON_SMALL, icon_small) win32api.SendMessage( hwnd, win32con.WM_SETICON, win32con.ICON_BIG, icon_big) return True def screenshot(self, *largs, **kwargs): global glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE filename = super(WindowPygame, self).screenshot(*largs, **kwargs) if filename is None: return None if glReadPixels is None: from kivy.graphics.opengl import (glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE) width, height = self.system_size data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE) if PY2: data = str(buffer(data)) else: data = bytes(bytearray(data)) surface = pygame.image.fromstring(data, (width, height), 'RGBA', True) pygame.image.save(surface, filename) Logger.debug('Window: Screenshot saved at <%s>' % filename) return filename def flip(self): pygame.display.flip() super(WindowPygame, self).flip() @deprecated def toggle_fullscreen(self): if self.flags & pygame.FULLSCREEN: self.flags &= ~pygame.FULLSCREEN else: self.flags |= pygame.FULLSCREEN self._pygame_set_mode() def _mainloop(self): EventLoop.idle() for event in pygame.event.get(): # kill application (SIG_TERM) if event.type == pygame.QUIT: if self.dispatch('on_request_close'): continue EventLoop.quit = True self.close() # mouse move elif event.type == pygame.MOUSEMOTION: x, y = event.pos self.mouse_pos = x, self.system_size[1] - y # don't dispatch motion if no button are pressed if event.buttons == (0, 0, 0): continue self._mouse_x = x self._mouse_y = y self._mouse_meta = self.modifiers self.dispatch('on_mouse_move', x, y, self.modifiers) # mouse action elif event.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP): self._pygame_update_modifiers() x, y = event.pos btn = 'left' if event.button == 3: btn = 'right' elif event.button == 2: btn = 'middle' elif event.button == 4: btn = 'scrolldown' elif event.button == 5: btn = 'scrollup' elif event.button == 6: btn = 'scrollright' elif event.button == 7: btn = 'scrollleft' eventname = 'on_mouse_down' if event.type == pygame.MOUSEBUTTONUP: eventname = 'on_mouse_up' self._mouse_x = x self._mouse_y = y self._mouse_meta = self.modifiers self._mouse_btn = btn self._mouse_down = eventname == 'on_mouse_down' self.dispatch(eventname, x, y, btn, self.modifiers) # joystick action elif event.type == pygame.JOYAXISMOTION: self.dispatch('on_joy_axis', event.joy, event.axis, event.value) elif event.type == pygame.JOYHATMOTION: self.dispatch('on_joy_hat', event.joy, event.hat, event.value) elif event.type == pygame.JOYBALLMOTION: self.dispatch('on_joy_ball', event.joy, event.ballid, event.rel[0], event.rel[1]) elif event.type == pygame.JOYBUTTONDOWN: self.dispatch('on_joy_button_down', event.joy, event.button) elif event.type == pygame.JOYBUTTONUP: self.dispatch('on_joy_button_up', event.joy, event.button) # keyboard action elif event.type in (pygame.KEYDOWN, pygame.KEYUP): self._pygame_update_modifiers(event.mod) # atm, don't handle keyup if event.type == pygame.KEYUP: self.dispatch('on_key_up', event.key, event.scancode) continue # don't dispatch more key if down event is accepted if self.dispatch('on_key_down', event.key, event.scancode, event.unicode, self.modifiers): continue self.dispatch('on_keyboard', event.key, event.scancode, event.unicode, self.modifiers) # video resize elif event.type == pygame.VIDEORESIZE: self._size = event.size self.update_viewport() elif event.type == pygame.VIDEOEXPOSE: self.canvas.ask_update() # ignored event elif event.type == pygame.ACTIVEEVENT: pass # drop file (pygame patch needed) elif event.type == pygame.USEREVENT and \ hasattr(pygame, 'USEREVENT_DROPFILE') and \ event.code == pygame.USEREVENT_DROPFILE: self.dispatch('on_dropfile', event.filename) ''' # unhandled event ! else: Logger.debug('WinPygame: Unhandled event %s' % str(event)) ''' def mainloop(self): while not EventLoop.quit and EventLoop.status == 'started': try: self._mainloop() if not pygame.display.get_active(): pygame.time.wait(100) except BaseException as inst: # use exception manager first r = ExceptionManager.handle_exception(inst) if r == ExceptionManager.RAISE: stopTouchApp() raise else: pass # # Pygame wrapper # def _pygame_set_mode(self, size=None): if size is None: size = self.size if self.fullscreen == 'auto': pygame.display.set_mode((0, 0), self.flags) else: pygame.display.set_mode(size, self.flags) def _pygame_update_modifiers(self, mods=None): # Available mod, from dir(pygame) # 'KMOD_ALT', 'KMOD_CAPS', 'KMOD_CTRL', 'KMOD_LALT', # 'KMOD_LCTRL', 'KMOD_LMETA', 'KMOD_LSHIFT', 'KMOD_META', # 'KMOD_MODE', 'KMOD_NONE' if mods is None: mods = pygame.key.get_mods() self._modifiers = [] if mods & (pygame.KMOD_SHIFT | pygame.KMOD_LSHIFT): self._modifiers.append('shift') if mods & (pygame.KMOD_ALT | pygame.KMOD_LALT): self._modifiers.append('alt') if mods & (pygame.KMOD_CTRL | pygame.KMOD_LCTRL): self._modifiers.append('ctrl') if mods & (pygame.KMOD_META | pygame.KMOD_LMETA): self._modifiers.append('meta') def request_keyboard(self, callback, target, input_type='text'): keyboard = super(WindowPygame, self).request_keyboard( callback, target, input_type) if android and not self.allow_vkeyboard: android.show_keyboard(target, input_type) return keyboard def release_keyboard(self, *largs): super(WindowPygame, self).release_keyboard(*largs) if android: android.hide_keyboard() return True ================================================ FILE: tickeys/kivy/core/window/window_sdl2.py ================================================ # found a way to include it more easily. ''' SDL2 Window =========== Windowing provider directly based on our own wrapped version of SDL. TODO: - fix keys - support scrolling - clean code - manage correctly all sdl events ''' __all__ = ('WindowSDL2', ) from os.path import join from kivy import kivy_data_dir from kivy.logger import Logger from kivy import metrics from kivy.base import EventLoop, ExceptionManager, stopTouchApp from kivy.clock import Clock from kivy.config import Config from kivy.core.window import WindowBase from kivy.core.window._window_sdl2 import _WindowSDL2Storage from kivy.input.provider import MotionEventProvider from kivy.input.motionevent import MotionEvent from kivy.resources import resource_find from kivy.utils import platform, deprecated from kivy.compat import unichr from collections import deque KMOD_LCTRL = 64 KMOD_RCTRL = 128 KMOD_RSHIFT = 2 KMOD_LSHIFT = 1 KMOD_RALT = 512 KMOD_LALT = 256 KMOD_LMETA = 1024 KMOD_RMETA = 2048 SDLK_SHIFTL = 1073742049 SDLK_SHIFTR = 1073742053 SDLK_LCTRL = 1073742048 SDLK_RCTRL = 1073742052 SDLK_LALT = 1073742050 SDLK_RALT = 1073742054 SDLK_LEFT = 1073741904 SDLK_RIGHT = 1073741903 SDLK_UP = 1073741906 SDLK_DOWN = 1073741905 SDLK_HOME = 1073741898 SDLK_END = 1073741901 SDLK_PAGEUP = 1073741899 SDLK_PAGEDOWN = 1073741902 SDLK_SUPER = 1073742051 SDLK_CAPS = 1073741881 SDLK_INSERT = 1073741897 SDLK_KEYPADNUM = 1073741907 SDLK_F1 = 1073741882 SDLK_F2 = 1073741883 SDLK_F3 = 1073741884 SDLK_F4 = 1073741885 SDLK_F5 = 1073741886 SDLK_F6 = 1073741887 SDLK_F7 = 1073741888 SDLK_F8 = 1073741889 SDLK_F9 = 1073741890 SDLK_F10 = 1073741891 SDLK_F11 = 1073741892 SDLK_F12 = 1073741893 SDLK_F13 = 1073741894 SDLK_F14 = 1073741895 SDLK_F15 = 1073741896 class SDL2MotionEvent(MotionEvent): def depack(self, args): self.is_touch = True self.profile = ('pos', ) self.sx, self.sy = args win = EventLoop.window super(SDL2MotionEvent, self).depack(args) class SDL2MotionEventProvider(MotionEventProvider): win = None q = deque() touchmap = {} def update(self, dispatch_fn): touchmap = self.touchmap while True: try: value = self.q.pop() except IndexError: return action, fid, x, y = value y = 1 - y if fid not in touchmap: touchmap[fid] = me = SDL2MotionEvent('sdl', fid, (x, y)) else: me = touchmap[fid] me.move((x, y)) if action == 'fingerdown': dispatch_fn('begin', me) elif action == 'fingerup': me.update_time_end() dispatch_fn('end', me) del touchmap[fid] else: dispatch_fn('update', me) class WindowSDL(WindowBase): def __init__(self, **kwargs): self._win = _WindowSDL2Storage() super(WindowSDL, self).__init__() self._mouse_x = self._mouse_y = -1 self._meta_keys = (KMOD_LCTRL, KMOD_RCTRL, KMOD_RSHIFT, KMOD_LSHIFT, KMOD_RALT, KMOD_LALT, KMOD_LMETA, KMOD_RMETA) self.command_keys = { 27: 'escape', 9: 'tab', 8: 'backspace', 13: 'enter', 127: 'del', 271: 'enter', 273: 'up', 274: 'down', 275: 'right', 276: 'left', 278: 'home', 279: 'end', 280: 'pgup', 281: 'pgdown'} self._mouse_buttons_down = set() def create_window(self, *largs): if self._fake_fullscreen: if not self.borderless: self.fullscreen = self._fake_fullscreen = False elif not self.fullscreen or self.fullscreen == 'auto': self.borderless = self._fake_fullscreen = False if self.fullscreen == 'fake': self.borderless = self._fake_fullscreen = True Logger.warning("The 'fake' fullscreen option has been " "deprecated, use Window.borderless or the " "borderless Config option instead.") if not self.initialized: if self.position == 'auto': pos = None, None elif self.position == 'custom': pos = self.left, self.top # setup ! w, h = self.system_size resizable = Config.getboolean('graphics', 'resizable') state = (Config.get('graphics', 'window_state') if self._is_desktop else None) self.system_size = _size = self._win.setup_window( pos[0], pos[1], w, h, self.borderless, self.fullscreen, resizable, state) # calculate density sz = self._win._get_gl_size()[0] self._density = density = sz / _size[0] if self._is_desktop and self.size[0] != _size[0]: self.dpi = density * 96. # never stay with a None pos, application using w.center # will be fired. self._pos = (0, 0) else: w, h = self.system_size self._win.resize_window(w, h) self._win.set_border_state(self.borderless) self._win.set_fullscreen_mode(self.fullscreen) super(WindowSDL, self).create_window() if self.initialized: return # auto add input provider Logger.info('Window: auto add sdl2 input provider') from kivy.base import EventLoop SDL2MotionEventProvider.win = self EventLoop.add_input_provider(SDL2MotionEventProvider('sdl', '')) # set window icon before calling set_mode try: filename_icon = self.icon or Config.get('kivy', 'window_icon') if filename_icon == '': logo_size = 32 if platform == 'macosx': logo_size = 512 elif platform == 'win': logo_size = 64 filename_icon = 'kivy-icon-{}.png'.format(logo_size) filename_icon = resource_find( join(kivy_data_dir, 'logo', filename_icon)) self.set_icon(filename_icon) except: Logger.exception('Window: cannot set icon') def close(self): self._win.teardown_window() self.dispatch('on_close') def maximize(self): if self._is_desktop: self._win.maximize_window() else: Logger.warning('Window: maximize() is used only on desktop OSes.') def minimize(self): if self._is_desktop: self._win.minimize_window() else: Logger.warning('Window: minimize() is used only on desktop OSes.') def restore(self): if self._is_desktop: self._win.restore_window() else: Logger.warning('Window: restore() is used only on desktop OSes.') def hide(self): if self._is_desktop: self._win.hide_window() else: Logger.warning('Window: hide() is used only on desktop OSes.') def show(self): if self._is_desktop: self._win.show_window() else: Logger.warning('Window: show() is used only on desktop OSes.') @deprecated def toggle_fullscreen(self): if self.fullscreen in (True, 'auto'): self.fullscreen = False else: self.fullscreen = 'auto' def set_title(self, title): self._win.set_window_title(title) def set_icon(self, filename): self._win.set_window_icon(str(filename)) def screenshot(self, *largs, **kwargs): filename = super(WindowSDL, self).screenshot(*largs, **kwargs) if filename is None: return from kivy.graphics.opengl import glReadPixels, GL_RGB, GL_UNSIGNED_BYTE width, height = self.size data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE) self._win.save_bytes_in_png(filename, data, width, height) Logger.debug('Window: Screenshot saved at <%s>' % filename) return filename def flip(self): self._win.flip() super(WindowSDL, self).flip() def _fix_mouse_pos(self, x, y): density = self._density y -= 1 if self._is_desktop and self.size[0] != self._size[0]: x, y = x * density, (y * density) - self.system_size[1] #y = self.system_size[1] - y self.mouse_pos = x, y else: self.mouse_pos = x, self.system_size[1] - y return x, y def _mainloop(self): EventLoop.idle() while True: event = self._win.poll() if event is False: break if event is None: continue action, args = event[0], event[1:] if action == 'quit': EventLoop.quit = True self.close() break elif action in ('fingermotion', 'fingerdown', 'fingerup'): # for finger, pass the raw event to SDL motion event provider # XXX this is problematic. On OSX, it generates touches with 0, # 0 coordinates, at the same times as mouse. But it works. # We have a conflict of using either the mouse or the finger. # Right now, we have no mechanism that we could use to know # which is the preferred one for the application. if platform == "ios": SDL2MotionEventProvider.q.appendleft(event) pass elif action == 'mousemotion': x, y = args x, y = self._fix_mouse_pos(x, y) self._mouse_x = x self._mouse_y = y # don't dispatch motion if no button are pressed if len(self._mouse_buttons_down) == 0: continue self._mouse_meta = self.modifiers self.dispatch('on_mouse_move', x, y, self.modifiers) elif action in ('mousebuttondown', 'mousebuttonup'): x, y, button = args x, y = self._fix_mouse_pos(x, y) btn = 'left' if button == 3: btn = 'right' elif button == 2: btn = 'middle' eventname = 'on_mouse_down' self._mouse_buttons_down.add(button) if action == 'mousebuttonup': eventname = 'on_mouse_up' self._mouse_buttons_down.remove(button) self._mouse_x = x self._mouse_y = y self.dispatch(eventname, x, y, btn, self.modifiers) elif action.startswith('mousewheel'): self._update_modifiers() x, y, button = args btn = 'scrolldown' if action.endswith('up'): btn = 'scrollup' elif action.endswith('right'): btn = 'scrollright' elif action.endswith('left'): btn = 'scrollleft' self._mouse_meta = self.modifiers self._mouse_btn = btn #times = x if y == 0 else y #times = min(abs(times), 100) #for k in range(times): self._mouse_down = True self.dispatch('on_mouse_down', self._mouse_x, self._mouse_y, btn, self.modifiers) self._mouse_down = False self.dispatch('on_mouse_up', self._mouse_x, self._mouse_y, btn, self.modifiers) elif action == 'dropfile': dropfile = args self.dispatch('on_dropfile', dropfile[0]) # video resize elif action == 'windowresized': self._size = self._win.window_size # don't use trigger here, we want to delay the resize event cb = self._do_resize Clock.unschedule(cb) Clock.schedule_once(cb, .1) elif action == 'windowresized': self.canvas.ask_update() elif action == 'windowrestored': self.canvas.ask_update() elif action == 'windowexposed': self.canvas.ask_update() elif action == 'windowminimized': if Config.getboolean('kivy', 'pause_on_minimize'): self.do_pause() elif action == 'joyaxismotion': stickid, axisid, value = args self.dispatch('on_joy_axis', stickid, axisid, value) elif action == 'joyhatmotion': stickid, hatid, value = args self.dispatch('on_joy_hat', stickid, hatid, value) elif action == 'joyballmotion': stickid, ballid, xrel, yrel = args self.dispatch('on_joy_ball', stickid, ballid, xrel, yrel) elif action == 'joybuttondown': stickid, buttonid = args self.dispatch('on_joy_button_down', stickid, buttonid) elif action == 'joybuttonup': stickid, buttonid = args self.dispatch('on_joy_button_up', stickid, buttonid) elif action in ('keydown', 'keyup'): mod, key, scancode, kstr = args key_swap = { SDLK_LEFT: 276, SDLK_RIGHT: 275, SDLK_UP: 273, SDLK_DOWN: 274, SDLK_HOME: 278, SDLK_END: 279, SDLK_PAGEDOWN: 281, SDLK_PAGEUP: 280, SDLK_SHIFTR: 303, SDLK_SHIFTL: 304, SDLK_SUPER: 309, SDLK_LCTRL: 305, SDLK_RCTRL: 306, SDLK_LALT: 308, SDLK_RALT: 307, SDLK_CAPS: 301, SDLK_INSERT: 277, SDLK_F1: 282, SDLK_F2: 283, SDLK_F3: 284, SDLK_F4: 285, SDLK_F5: 286, SDLK_F6: 287, SDLK_F7: 288, SDLK_F8: 289, SDLK_F9: 290, SDLK_F10: 291, SDLK_F11: 292, SDLK_F12: 293, SDLK_F13: 294, SDLK_F14: 295, SDLK_F15: 296, SDLK_KEYPADNUM: 300} if platform == 'ios': # XXX ios keyboard suck, when backspace is hit, the delete # keycode is sent. fix it. key_swap[127] = 8 # back try: key = key_swap[key] except KeyError: pass if action == 'keydown': self._update_modifiers(mod, key) else: self._update_modifiers(mod) # ignore the key, it # has been released # if mod in self._meta_keys: if (key not in self._modifiers and key not in self.command_keys.keys()): try: kstr = unichr(key) except ValueError: pass #if 'shift' in self._modifiers and key\ # not in self.command_keys.keys(): # return if action == 'keyup': self.dispatch('on_key_up', key, scancode) continue # don't dispatch more key if down event is accepted if self.dispatch('on_key_down', key, scancode, kstr, self.modifiers): continue self.dispatch('on_keyboard', key, scancode, kstr, self.modifiers) elif action == 'textinput': text = args[0] self.dispatch('on_textinput', text) # XXX on IOS, keydown/up don't send unicode anymore. # With latest sdl, the text is sent over textinput # Right now, redo keydown/up, but we need to seperate both call # too. (and adapt on_key_* API.) #self.dispatch() #self.dispatch('on_key_down', key, None, args[0], # self.modifiers) #self.dispatch('on_keyboard', None, None, args[0], # self.modifiers) #self.dispatch('on_key_up', key, None, args[0], # self.modifiers) # unhandled event ! else: Logger.trace('WindowSDL: Unhandled event %s' % str(event)) def _do_resize(self, dt): Logger.debug('Window: Resize window to %s' % str(self.size)) self._win.resize_window(*self._size) self.dispatch('on_resize', *self.size) def do_pause(self): # should go to app pause mode. from kivy.app import App from kivy.base import stopTouchApp app = App.get_running_app() if not app: Logger.info('WindowSDL: No running App found, exit.') stopTouchApp() return if not app.dispatch('on_pause'): Logger.info('WindowSDL: App doesn\'t support pause mode, stop.') stopTouchApp() return # XXX FIXME wait for sdl resume while True: event = self._win.poll() if event is False: continue if event is None: continue action, args = event[0], event[1:] if action == 'quit': EventLoop.quit = True self.close() break elif action == 'windowrestored': break app.dispatch('on_resume') def mainloop(self): # don't known why, but pygame required a resize event # for opengl, before mainloop... window reinit ? #self.dispatch('on_resize', *self.size) while not EventLoop.quit and EventLoop.status == 'started': try: self._mainloop() except BaseException as inst: # use exception manager first r = ExceptionManager.handle_exception(inst) if r == ExceptionManager.RAISE: stopTouchApp() raise else: pass # # Pygame wrapper # def _update_modifiers(self, mods=None, key=None): # Available mod, from dir(pygame) # 'KMOD_ALT', 'KMOD_CAPS', 'KMOD_CTRL', 'KMOD_LALT', # 'KMOD_LCTRL', 'KMOD_LMETA', 'KMOD_LSHIFT', 'KMOD_META', # 'KMOD_MODE', 'KMOD_NONE' if mods is None and key is None: return modifiers = set() if mods is not None: if mods & (KMOD_RSHIFT | KMOD_LSHIFT): modifiers.add('shift') if mods & (KMOD_RALT | KMOD_LALT): modifiers.add('alt') if mods & (KMOD_RCTRL | KMOD_LCTRL): modifiers.add('ctrl') if mods & (KMOD_RMETA | KMOD_LMETA): modifiers.add('meta') if key is not None: if key in (KMOD_RSHIFT, KMOD_LSHIFT): modifiers.add('shift') if key in (KMOD_RALT, KMOD_LALT): modifiers.add('alt') if key in (KMOD_RCTRL, KMOD_LCTRL): modifiers.add('ctrl') if key in (KMOD_RMETA, KMOD_LMETA): modifiers.add('meta') self._modifiers = list(modifiers) return def request_keyboard(self, callback, target, input_type='text'): self._sdl_keyboard = super(WindowSDL, self).\ request_keyboard(callback, target, input_type) self._win.show_keyboard() Clock.schedule_interval(self._check_keyboard_shown, 1 / 5.) return self._sdl_keyboard def release_keyboard(self, *largs): super(WindowSDL, self).release_keyboard(*largs) self._win.hide_keyboard() self._sdl_keyboard = None return True def _check_keyboard_shown(self, dt): if self._sdl_keyboard is None: return False if not self._win.is_keyboard_shown(): self._sdl_keyboard.release() ================================================ FILE: tickeys/kivy/data/glsl/default.fs ================================================ $HEADER$ void main (void){ gl_FragColor = frag_color * texture2D(texture0, tex_coord0); } ================================================ FILE: tickeys/kivy/data/glsl/default.vs ================================================ $HEADER$ void main (void) { frag_color = color * vec4(1.0, 1.0, 1.0, opacity); tex_coord0 = vTexCoords0; gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0); } ================================================ FILE: tickeys/kivy/data/glsl/header.fs ================================================ #ifdef GL_ES precision highp float; #endif /* Outputs from the vertex shader */ varying vec4 frag_color; varying vec2 tex_coord0; /* uniform texture samplers */ uniform sampler2D texture0; ================================================ FILE: tickeys/kivy/data/glsl/header.vs ================================================ #ifdef GL_ES precision highp float; #endif /* Outputs to the fragment shader */ varying vec4 frag_color; varying vec2 tex_coord0; /* vertex attributes */ attribute vec2 vPosition; attribute vec2 vTexCoords0; /* uniform variables */ uniform mat4 modelview_mat; uniform mat4 projection_mat; uniform vec4 color; uniform float opacity; ================================================ FILE: tickeys/kivy/data/images/defaulttheme.atlas ================================================ {"defaulttheme-0.png": {"progressbar_background": [391, 227, 24, 24], "tab_btn_disabled": [264, 137, 32, 32], "tab_btn_pressed": [366, 137, 32, 32], "image-missing": [152, 171, 48, 48], "splitter_h": [174, 123, 32, 7], "splitter_down": [501, 253, 7, 32], "splitter_disabled_down": [503, 291, 7, 32], "vkeyboard_key_down": [468, 137, 32, 32], "vkeyboard_disabled_key_down": [400, 137, 32, 32], "selector_right": [248, 223, 55, 62], "player-background": [2, 287, 103, 103], "selector_middle": [191, 223, 55, 62], "spinner": [235, 82, 29, 37], "tab_btn_disabled_pressed": [298, 137, 32, 32], "switch-button_disabled": [277, 291, 43, 32], "textinput_disabled_active": [372, 326, 64, 64], "splitter_grip": [36, 50, 12, 26], "vkeyboard_key_normal": [2, 44, 32, 32], "button_disabled": [80, 82, 29, 37], "media-playback-stop": [302, 171, 48, 48], "splitter": [501, 87, 7, 32], "splitter_down_h": [140, 123, 32, 7], "sliderh_background_disabled": [72, 132, 41, 37], "modalview-background": [464, 456, 45, 54], "button": [142, 82, 29, 37], "splitter_disabled": [502, 137, 7, 32], "checkbox_radio_disabled_on": [433, 87, 32, 32], "slider_cursor": [402, 171, 48, 48], "vkeyboard_disabled_background": [68, 221, 64, 64], "checkbox_disabled_on": [297, 87, 32, 32], "sliderv_background_disabled": [2, 78, 37, 41], "button_disabled_pressed": [111, 82, 29, 37], "audio-volume-muted": [102, 171, 48, 48], "close": [417, 231, 20, 20], "action_group_disabled": [452, 171, 33, 48], "vkeyboard_background": [2, 221, 64, 64], "checkbox_off": [331, 87, 32, 32], "tab_disabled": [305, 253, 96, 32], "sliderh_background": [115, 132, 41, 37], "switch-button": [322, 291, 43, 32], "tree_closed": [439, 231, 20, 20], "bubble_btn_pressed": [435, 291, 32, 32], "selector_left": [134, 223, 55, 62], "filechooser_file": [174, 326, 64, 64], "checkbox_radio_disabled_off": [399, 87, 32, 32], "checkbox_radio_on": [196, 137, 32, 32], "checkbox_on": [365, 87, 32, 32], "button_pressed": [173, 82, 29, 37], "audio-volume-high": [464, 406, 48, 48], "audio-volume-low": [2, 171, 48, 48], "progressbar": [305, 227, 32, 24], "previous_normal": [487, 187, 19, 32], "separator": [504, 342, 5, 48], "filechooser_folder": [240, 326, 64, 64], "checkbox_radio_off": [467, 87, 32, 32], "textinput_active": [306, 326, 64, 64], "textinput": [438, 326, 64, 64], "player-play-overlay": [122, 395, 117, 115], "media-playback-pause": [202, 171, 48, 48], "sliderv_background": [41, 78, 37, 41], "ring": [354, 402, 108, 108], "bubble_arrow": [487, 175, 16, 10], "slider_cursor_disabled": [352, 171, 48, 48], "checkbox_disabled_off": [469, 291, 32, 32], "action_group_down": [2, 121, 33, 48], "spinner_disabled": [204, 82, 29, 37], "splitter_disabled_h": [106, 123, 32, 7], "bubble": [107, 325, 65, 65], "media-playback-start": [252, 171, 48, 48], "vkeyboard_disabled_key_normal": [434, 137, 32, 32], "overflow": [230, 137, 32, 32], "tree_opened": [461, 231, 20, 20], "action_item": [339, 227, 24, 24], "bubble_btn": [401, 291, 32, 32], "audio-volume-medium": [52, 171, 48, 48], "action_group": [37, 121, 33, 48], "spinner_pressed": [266, 82, 29, 37], "filechooser_selected": [2, 392, 118, 118], "tab": [403, 253, 96, 32], "action_bar": [158, 133, 36, 36], "action_view": [365, 227, 24, 24], "tab_btn": [332, 137, 32, 32], "switch-background": [192, 291, 83, 32], "splitter_disabled_down_h": [72, 123, 32, 7], "action_item_down": [367, 291, 32, 32], "switch-background_disabled": [107, 291, 83, 32], "textinput_disabled": [241, 399, 111, 111], "splitter_grip_h": [483, 239, 26, 12]}} ================================================ FILE: tickeys/kivy/data/keyboards/azerty.json ================================================ { "title" : "Azerty", "description" : "A French keyboard without international keys", "cols" : 15, "rows": 5, "normal_1" : [ ["@", "@", "`", 1], ["&", "&", "1", 1], ["\u00e9", "\u00e9", "2", 1], ["'", "'", "3", 1], ["\"", "\"", "4", 1], ["[", "[", "5", 1], ["-", "-", "6", 1], ["\u00e8", "\u00e8", "7", 1], ["_", "_", "8", 1], ["\u00e7", "\u00e7", "9", 1], ["\u00e0", "\u00e0", "0", 1], ["]", "]", "+", 1], ["=", "=", "=", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["a", "a", "a", 1], ["z", "z", "z", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["y", "y", "y", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["^", "^", "^", 1], ["$", "$", "}", 1], ["\u23ce", null, "enter", 1.5] ], "normal_3" : [ ["\u21ea", null, "capslock", 1.8], ["q", "q", "q", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], ["m", "m", "m", 1], ["\u00f9", "\u00f9", "%", 1], ["*", "*", "*", 1], ["\u23ce", null, "enter", 1.2] ], "normal_4" : [ ["\u21e7", null, "shift", 1.5], ["<", "<", null, 1], ["w", "w", null, 1], ["x", "x", null, 1], ["c", "c", null, 1], ["v", "v", null, 1], ["b", "b", null, 1], ["n", "n", null, 1], [",", ",", null, 1], [";", ";", null, 1], [":", ":", null, 1], ["!", "!", null, 1], ["\u21e7", null, "shift", 2.5] ], "normal_5" : [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ], "shift_1" : [ ["|", "|", "|", 1], ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["#", "#", "#", 1], ["+", "+", "+", 1], ["\u232b", null, "backspace", 2] ], "shift_2" : [ ["\u21B9", "\t", "tab", 1.5], ["A", "A", "a", 1], ["Z", "Z", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Y", "Y", "y", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["[", "[", "[", 1], ["]", "]", "]", 1], ["\u23ce", null, "enter", 1.5] ], "shift_3" : [ ["\u21ea", null, "capslock", 1.8], ["Q", "Q", "q", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], ["M", "M", "m", 1], ["%", "%", "%", 1], ["\u00b5", "\u00b5", "*", 1], ["\u23ce", null, "enter", 1.2] ], "shift_4" : [ ["\u21e7", null, "shift", 1.5], [">", ">", ">", 1], ["W", "W", "w", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["?", "?", "?", 1], [".", ".", ".", 1], ["/", "/", "/", 1], ["\u00a7", "\u00a7", "!", 1], ["\u21e7", null, "shift", 2.5] ], "shift_5" : [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/keyboards/de_CH.json ================================================ { "title": "de_CH", "description": "A Swiss German keyboard, touch optimized (no shift+caps lock)", "cols": 15, "rows": 5, "normal_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["z", "z", "z", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["ü", "ü", "ü", 1], [":", ":", ":", 1], ["$", "$", "$", 1.5] ], "normal_3": [ ["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], ["ö", "ö", "ö", 1], ["ä", "ä", "ä", 1], ["\u23ce", null, "enter", 2.2] ], "normal_4": [ ["\u21e7", null, "shift", 2.5], ["y", "y", "y", 1], ["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1], [".", ".", ".", 1], ["-", "-", "-", 1], ["\u21e7", null, "shift", 2.5] ], "normal_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "shift_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "shift_2": [ ["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Z", "Z", "z", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["Ü", "Ü", "Ü", 1], [":", ":", ":", 1], ["/", "/", "/", 1.5] ], "shift_3": [ ["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], ["Ö", "Ö", "Ö", 1], ["Ä", "Ä", "Ä", 1], ["\u23ce", null, "enter", 2.2] ], "shift_4": [ ["\u21e7", null, "shift", 2.5], ["Y", "Y", "y", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["M", "M", "m", 1], [";", ";", ";", 1], [":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5] ], "shift_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "special_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "special_2": [ ["\u21B9", "\t", "tab", 1.5], ["(", "(", "(", 1], [")", ")", ")", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["[", "[", "[", 1], ["]", "]", "]", 1], ["€", "€", "€", 1], ["$", "$", "$", 1], ["£", "£", "£", 1], ["¥", "¥", "¥", 1], ["è", "è", "è", 1], ["•", "•", "•", 1], ["|", "|", "|", 1.5] ], "special_3": [ ["\u21ea", null, "capslock", 1.8], ["“", "“", "“", 1], ["`", "`", "`", 1], ["«", "«", "«", 1], ["»", "»", "»", 1], ["#", "#", "#", 1], ["%", "%", "%", 1], ["^", "^", "^", 1], ["°", "°", "°", 1], ["&", "&", "&", 1], ["é", "é", "é", 1], ["à", "à", "à", 1], ["\u23ce", null, "enter", 2.2] ], "special_4": [ ["\u21e7", null, "shift", 2.5], ["+", "+", "+", 1], ["=", "=", "=", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["*", "*", "*", 1], ["È", "È", "È", 1], ["É", "É", "É", 1], ["À", "À", "À", 1], [":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5] ], "special_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/keyboards/en_US.json ================================================ { "title": "en_US", "description": "A US Keyboard, touch optimized (no shift+caps lock)", "cols": 15, "rows": 5, "normal_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["y", "y", "y", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["[", "[", "[", 1], ["]", "]", "]", 1], ["\\", "\\", "\\", 1.5] ], "normal_3": [ ["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], [";", ";", ";", 1], ["'", "'", "'", 1], ["\u23ce", null, "enter", 2.2] ], "normal_4": [ ["\u21e7", null, "shift", 2.5], ["z", "z", "z", 1], ["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1], [".", ".", ".", 1], ["/", "/", "/", 1], ["\u21e7", null, "shift", 2.5] ], "normal_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "shift_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "shift_2": [ ["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Y", "Y", "y", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["|", "|", "|", 1.5] ], "shift_3": [ ["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], [":", ":", ":", 1], ["\"", "\"", "\"", 1], ["\u23ce", null, "enter", 2.2] ], "shift_4": [ ["\u21e7", null, "shift", 2.5], ["Z", "Z", "z", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["M", "M", "m", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["?", "?", "?", 1], ["\u21e7", null, "shift", 2.5] ], "shift_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "special_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "special_2": [ ["\u21B9", "\t", "tab", 1.5], ["(", "(", "(", 1], [")", ")", ")", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["[", "[", "[", 1], ["]", "]", "]", 1], ["€", "€", "€", 1], ["$", "$", "$", 1], ["£", "£", "£", 1], ["¥", "¥", "¥", 1], ["˘", "˘", "˘", 1], ["•", "•", "•", 1], ["|", "|", "|", 1.5] ], "special_3": [ ["\u21ea", null, "capslock", 1.8], ["“", "“", "“", 1], ["`", "`", "`", 1], ["«", "«", "«", 1], ["»", "»", "»", 1], ["#", "#", "#", 1], ["%", "%", "%", 1], ["^", "^", "^", 1], ["°", "°", "°", 1], ["&", "&", "&", 1], ["ÿ", "ÿ", "ÿ", 1], ["Æ", "Æ", "Æ", 1], ["\u23ce", null, "enter", 2.2] ], "special_4": [ ["\u21e7", null, "shift", 2.5], ["+", "+", "+", 1], ["=", "=", "=", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["*", "*", "*", 1], ["Ù", "Ù", "Ù", 1], ["~", "~", "~", 1], ["À", "À", "À", 1], [":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5] ], "special_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/keyboards/fr_CH.json ================================================ { "title": "fr_CH", "description": "A Swiss French keyboard, touch optimized (no shift+caps lock)", "cols": 15, "rows": 5, "normal_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["z", "z", "z", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["è", "è", "è", 1], [":", ":", ":", 1], ["$", "$", "$", 1.5] ], "normal_3": [ ["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], ["é", "é", "é", 1], ["à", "à", "à", 1], ["\u23ce", null, "enter", 2.2] ], "normal_4": [ ["\u21e7", null, "shift", 2.5], ["y", "y", "y", 1], ["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1], [".", ".", ".", 1], ["-", "-", "-", 1], ["\u21e7", null, "shift", 2.5] ], "normal_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "shift_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "shift_2": [ ["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Z", "Z", "z", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["È", "È", "È", 1], [":", ":", ":", 1], ["/", "/", "/", 1.5] ], "shift_3": [ ["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], ["É", "É", "É", 1], ["À", "À", "À", 1], ["\u23ce", null, "enter", 2.2] ], "shift_4": [ ["\u21e7", null, "shift", 2.5], ["Y", "Y", "y", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["M", "M", "m", 1], [";", ";", ";", 1], [":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5] ], "shift_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ], "special_1": [ ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1], ["!", "!", "!", 1], ["\u232b", null, "backspace", 2] ], "special_2": [ ["\u21B9", "\t", "tab", 1.5], ["(", "(", "(", 1], [")", ")", ")", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["[", "[", "[", 1], ["]", "]", "]", 1], ["€", "€", "€", 1], ["$", "$", "$", 1], ["£", "£", "£", 1], ["¥", "¥", "¥", 1], ["ü", "ü", "ü", 1], ["•", "•", "•", 1], ["|", "|", "|", 1.5] ], "special_3": [ ["\u21ea", null, "capslock", 1.8], ["“", "“", "“", 1], ["`", "`", "`", 1], ["«", "«", "«", 1], ["»", "»", "»", 1], ["#", "#", "#", 1], ["%", "%", "%", 1], ["^", "^", "^", 1], ["°", "°", "°", 1], ["&", "&", "&", 1], ["ö", "ö", "ö", 1], ["ä", "ä", "ä", 1], ["\u23ce", null, "enter", 2.2] ], "special_4": [ ["\u21e7", null, "shift", 2.5], ["+", "+", "+", 1], ["=", "=", "=", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["*", "*", "*", 1], ["Ö", "Ö", "Ö", 1], ["Ä", "Ä", "Ä", 1], ["Ü", "Ü", "Ü", 1], [":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5] ], "special_5": [ ["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/keyboards/qwerty.json ================================================ { "title": "Qwerty", "description": "A classical US Keyboard", "cols": 15, "rows": 5, "normal_1": [ ["`", "`", "`", 1], ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["-", "-", "-", 1], ["=", "=", "=", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["y", "y", "y", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["[", "[", "[", 1], ["]", "]", "j", 1], ["\\", "\\", "\\", 1] ], "normal_3": [ ["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], [":", ":", ":", 1], ["'", "'", "'", 1], ["\u23ce", null, "enter", 2.2] ], "normal_4": [ ["\u21e7", null, "shift", 2.5], ["z", "z", null, 1], ["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1], [".", ".", ".", 1], ["/", "/", "/", 1], ["\u21e7", null, "shift", 2.5] ], "normal_5": [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ], "shift_1": [ ["~", "~", "~", 1], ["!", "!", "!", 1], ["@", "@", "@", 1], ["#", "#", "#", 1], ["$", "$", "$", 1], ["%", "%", "%", 1], ["^", "^", null, 1], ["&", "&", "&", 1], ["*", "*", "*", 1], ["(", "(", "(", 1], [")", ")", ")", 1], ["_", "_", "_", 1], ["+", "+", "+", 1], ["\u232b", null, "backspace", 2] ], "shift_2": [ ["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Y", "Y", "y", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["|", "|", "|", 1.5] ], "shift_3": [ ["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], [";", ";", ";", 1], ["\"", "\"", "\"", 1], ["\u23ce", null, "enter", 2.2] ], "shift_4": [ ["\u21e7", null, "shift", 2.5], ["Z", "Z", "z", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["M", "M", "m", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["?", "?", "?", 1.5], ["\u21e7", null, "shift", 2.5] ], "shift_5": [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/keyboards/qwertz.json ================================================ { "title": "Qwerty", "description": "A german Keyboard", "cols": 15, "rows": 5, "normal_1": [ ["!", "!", "!", 1], ["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1], ["0", "0", "0", 1], ["ß", "ß", "ß", 1], ["?", "?", "?", 1], ["\u232b", null, "backspace", 2] ], "normal_2" : [ ["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1], ["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1], ["z", "z", "z", 1], ["u", "u", "u", 1], ["i", "i", "i", 1], ["o", "o", "o", 1], ["p", "p", "p", 1], ["ü", "ü", "ü", 1], [":", ":", ":", 1], ["/", "/", "/", 1] ], "normal_3": [ ["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1], ["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1], ["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1], ["l", "l", "l", 1], ["ö", "ö", "ö", 1], ["ä", "ä", "ä", 1], ["\u23ce", null, "enter", 2.2] ], "normal_4": [ ["\u21e7", null, "shift", 2.5], ["y", "y", null, 1], ["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1], [".", ".", ".", 1], ["-", "-", "-", 1], ["\u21e7", null, "shift", 2.5] ], "normal_5": [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ], "shift_1": [ ["\"", "\"", "\"", 1], ["+", "+", "+", 1], ["@", "@", "@", 1], ["#", "#", "#", 1], ["$", "$", "$", 1], ["€", "€", "€", 1], ["%", "%", "%", 1], ["&", "&", "&", 1], ["*", "*", "*", 1], ["(", "(", "(", 1], [")", ")", ")", 1], ["<", "<", "<", 1], [">", ">", ">", 1], ["\u232b", null, "backspace", 2] ], "shift_2": [ ["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1], ["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1], ["Z", "Z", "z", 1], ["U", "U", "u", 1], ["I", "I", "i", 1], ["O", "O", "o", 1], ["P", "P", "p", 1], ["{", "{", "{", 1], ["}", "}", "}", 1], ["|", "|", "|", 1.5] ], "shift_3": [ ["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1], ["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1], ["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1], ["L", "L", "l", 1], ["=", "=", "=", 1], ["°", "°", "°", 1], ["\u23ce", null, "enter", 2.2] ], "shift_4": [ ["\u21e7", null, "shift", 2.5], ["Y", "Y", "y", 1], ["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1], ["N", "N", "n", 1], ["M", "M", "m", 1], [";", ";", ";", 1], [":", ":", ":", 1], ["_", "_", "_", 1.5], ["\u21e7", null, "shift", 2.5] ], "shift_5": [ [" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5] ] } ================================================ FILE: tickeys/kivy/data/settings_kivy.json ================================================ [ { "type": "title", "title": "Windows" }, { "type": "bool", "title": "Fullscreen", "desc": "Set the window in windowed or fullscreen", "section": "graphics", "key": "fullscreen", "values": ["0", "auto"] }, { "type": "numeric", "title": "FPS Limit", "desc": "Maximum FPS limit if set, 0 for unlimited", "section": "graphics", "key": "maxfps" }, { "type": "bool", "title": "Mouse cursor", "desc": "Show/hide the mouse cursor on the window", "section": "graphics", "key": "show_cursor" }, { "type": "options", "title": "Rotation", "desc": "Rotation of the window", "section": "graphics", "key": "rotation", "options": ["0", "90", "180", "270"] }, { "type": "title", "title": "Logging" }, { "type": "bool", "title": "File logging", "desc": "If activated, the logging will be stored in a file", "section": "kivy", "key": "log_enable" }, { "type": "options", "title": "Log level", "desc": "Level of logging information", "section": "kivy", "key": "log_level", "options": ["trace", "debug", "info", "warning", "error", "critical"] }, { "type": "title", "title": "Keyboard" }, { "type": "options", "title": "Keyboard mode", "desc": "Activate the usage of Kivy Virtual Keyboard", "section": "kivy", "key": "keyboard_mode", "options": ["system", "dock", "multi", "systemanddock", "systemandmulti"] }, { "type": "options", "title": "Keyboard layout", "desc": "Select a layout for virtual keyboard", "section": "kivy", "key": "keyboard_layout", "options": ["qwerty", "azerty", "qwertz", "de_CH", "fr_CH", "en_US"] }, { "type": "title", "title": "Input post-processing" }, { "type": "numeric", "title": "Double tap distance", "desc": "Radius in pixels within a double tap is detected", "section": "postproc", "key": "double_tap_distance" }, { "type": "numeric", "title": "Double tap time", "desc": "Time in milliseconds during a double tap is allowed", "section": "postproc", "key": "double_tap_time" }, { "type": "numeric", "title": "Retain distance", "desc": "Maximum distance to retain the touch", "section": "postproc", "key": "retain_distance" }, { "type": "numeric", "title": "Retain time", "desc": "Time in milliseconds during the touch will be retain", "section": "postproc", "key": "retain_distance" }, { "type": "numeric", "title": "Jitter distance", "desc": "Radius in pixels within the touch moves will be ignored", "section": "postproc", "key": "jitter_distance" } ] ================================================ FILE: tickeys/kivy/data/style.kv ================================================ #:kivy 1.0