diff --git a/MementoMori.py b/MementoMori.py index f7fbf55..b5f3371 100644 --- a/MementoMori.py +++ b/MementoMori.py @@ -1,2 +1,14 @@ # It is not that we have a short time to live, but that we waste a lot of it. Life is long enough, and a sufficiently generous amount has been given to us for the highest achievements if it were all well invested -# SENECA \ No newline at end of file +# SENECA + +from kivy.app import App +from kivy.uix.widget import Widget + +class PongGame(Widget): + pass + +class TestApp(App): + def build(self): + return PongGame() + +TestApp().run() \ No newline at end of file diff --git a/kivy/__init__.py b/kivy/__init__.py new file mode 100644 index 0000000..8800f18 --- /dev/null +++ b/kivy/__init__.py @@ -0,0 +1,516 @@ +''' +Kivy framework +============== + +Kivy is an open source library for developing multi-touch applications. It is +cross-platform (Linux/OSX/Windows/Android/iOS) 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 +using `Cython `_. + +See http://kivy.org for more information. +''' + +__all__ = ( + 'require', 'parse_kivy_version', + '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_config_fn', 'kivy_usermodules_dir', 'kivy_examples_dir' +) + +import sys +import shutil +from getopt import getopt, GetoptError +import os +from os import environ, mkdir +from os.path import dirname, join, basename, exists, expanduser +import pkgutil +import re +from kivy.logger import Logger, LOG_LEVELS +from kivy.utils import platform +from kivy._version import __version__, RELEASE as _KIVY_RELEASE, \ + _kivy_git_hash, _kivy_build_date + +# 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) + +if sys.version_info[0] == 2: + Logger.critical( + 'Unsupported Python version detected!: Kivy 2.0.0 and higher does not ' + 'support Python 2. Please upgrade to Python 3, or downgrade Kivy to ' + '1.11.0 - the last Kivy release that still supports Python 2.') + + +def parse_kivy_version(version): + """Parses the kivy version as described in :func:`require` into a 3-tuple + of ([x, y, z], 'rc|a|b|dev|post', 'N') where N is the tag revision. The + last two elements may be None. + """ + m = re.match( + '^([0-9]+)\\.([0-9]+)\\.([0-9]+?)(rc|a|b|\\.dev|\\.post)?([0-9]+)?$', + version) + if m is None: + raise Exception('Revision format must be X.Y.Z[-tag]') + + major, minor, micro, tag, tagrev = m.groups() + if tag == '.dev': + tag = 'dev' + if tag == '.post': + tag = 'post' + return [int(major), int(minor), int(micro)], tag, tagrev + + +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', '.post', 'a', 'b', or 'rc'. + The tagrevision is the revision number 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. + + ''' + + # user version + revision, tag, tagrev = parse_kivy_version(version) + # current version + sysrevision, systag, systagrev = parse_kivy_version(__version__) + + if tag and not systag: + 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 get_includes(): + '''Retrieves the directories containing includes needed to build new Cython + modules with Kivy as a dependency. Currently returns the location of the + kivy.graphics module. + + .. versionadded:: 1.9.1 + ''' + root_dir = dirname(__file__) + return [join(root_dir, 'graphics'), join(root_dir, 'tools', 'gles_compat'), + join(root_dir, 'include')] + + +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 [KIVY OPTION...] [-- PROGRAM OPTIONS]:: + + Options placed after a '-- ' separator, will not be touched by kivy, + and instead passed to your program. + + Set KIVY_NO_ARGS=1 in your environment or before you import Kivy to + disable Kivy's argument parser. + + -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', 'null'), + 'audio': ( + 'gstplayer', 'pygame', 'ffpyplayer', 'sdl2', + 'avplayer'), + 'image': ('tex', 'imageio', 'dds', 'sdl2', 'pygame', 'pil', 'ffpy', 'gif'), + 'camera': ('opencv', 'gi', 'avfoundation', + 'android', 'picamera'), + 'spelling': ('enchant', 'osxappkit', ), + 'clipboard': ( + 'android', 'winctypes', 'xsel', 'xclip', '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') + 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 data directory +kivy_data_dir = environ.get('KIVY_DATA_DIR', + join(kivy_base_dir, 'data')) +#: Kivy binary deps directory +kivy_binary_deps_dir = environ.get('KIVY_BINARY_DEPS', + join(kivy_base_dir, 'binary_deps')) +#: 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 examples directory +kivy_examples_dir = '' +for examples_dir in ( + join(dirname(dirname(__file__)), 'examples'), + join(sys.exec_prefix, 'share', 'kivy-examples'), + join(sys.prefix, 'share', 'kivy-examples'), + '/usr/share/kivy-examples', '/usr/local/share/kivy-examples', + expanduser('~/.local/share/kivy-examples')): + if exists(examples_dir): + kivy_examples_dir = examples_dir + break + + +def _patch_mod_deps_win(dep_mod, mod_name): + import site + dep_bins = [] + + for d in [sys.prefix, site.USER_BASE]: + p = join(d, 'share', mod_name, 'bin') + if os.path.isdir(p): + os.environ["PATH"] = p + os.pathsep + os.environ["PATH"] + if hasattr(os, 'add_dll_directory'): + os.add_dll_directory(p) + dep_bins.append(p) + + dep_mod.dep_bins = dep_bins + + +# if there are deps, import them so they can do their magic. +_packages = [] +try: + from kivy import deps as old_deps + for importer, modname, ispkg in pkgutil.iter_modules(old_deps.__path__): + if not ispkg: + continue + if modname.startswith('gst'): + _packages.insert(0, (importer, modname, 'kivy.deps')) + else: + _packages.append((importer, modname, 'kivy.deps')) +except ImportError: + pass + +try: + import kivy_deps + for importer, modname, ispkg in pkgutil.iter_modules(kivy_deps.__path__): + if not ispkg: + continue + if modname.startswith('gst'): + _packages.insert(0, (importer, modname, 'kivy_deps')) + else: + _packages.append((importer, modname, 'kivy_deps')) +except ImportError: + pass + +_logging_msgs = [] +for importer, modname, package in _packages: + try: + mod = importer.find_module(modname).load_module(modname) + + version = '' + if hasattr(mod, '__version__'): + version = ' {}'.format(mod.__version__) + _logging_msgs.append( + 'deps: Successfully imported "{}.{}"{}'. + format(package, modname, version)) + + if modname.startswith('gst') and version == '0.3.3': + _patch_mod_deps_win(mod, modname) + + except ImportError as e: + Logger.warning( + 'deps: Error importing dependency "{}.{}": {}'. + format(package, modname, str(e))) + +# Don't go further if we generate documentation +if any(name in sys.argv[0] for name in ( + 'sphinx-build', 'autobuild.py', 'sphinx' +)): + environ['KIVY_DOC'] = '1' +if 'sphinx-build' in sys.argv[0]: + environ['KIVY_DOC_INCLUDE'] = '1' +if any(('nosetests' in arg or 'pytest' in arg) for arg in sys.argv): + environ['KIVY_UNITTEST'] = '1' +if any('pyinstaller' in arg.lower() 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') + 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(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 overridden in command line + if ('KIVY_UNITTEST' not in environ and + 'KIVY_PACKAGING' not in environ and + environ.get('KIVY_NO_ARGS', "false") not in ('true', '1', 'yes')): + # save sys argv, otherwise, 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', + 'multiprocessing-fork', 'display=', 'size=', 'rotate=', + 'config=', 'debug', 'dpi=']) + + except GetoptError as err: + Logger.error('Core: %s' % str(err)) + kivy_usage() + sys.exit(2) + + mp_fork = None + try: + for opt, arg in opts: + if opt == '--multiprocessing-fork': + mp_fork = True + break + except: + pass + + # set argv to the non-read args + sys.argv = sys_argv[0:1] + args + if mp_fork is not None: + # Needs to be first opt for support_freeze to work + sys.argv.insert(1, '--multiprocessing-fork') + + 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'): + ol = arg.split(':', 2) + if len(ol) == 2: + Config.set(ol[0], ol[1], '') + elif len(ol) == 3: + Config.set(ol[0], ol[1], ol[2]) + else: + raise Exception('Invalid --config value') + if ol[0] == 'kivy' and ol[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') + +for msg in _logging_msgs: + Logger.info(msg) + +if not _KIVY_RELEASE and _kivy_git_hash and _kivy_build_date: + Logger.info('Kivy: v%s, git-%s, %s' % ( + __version__, _kivy_git_hash[:7], _kivy_build_date)) +else: + Logger.info('Kivy: v%s' % __version__) +Logger.info('Kivy: Installed at "{}"'.format(__file__)) +Logger.info('Python: v{}'.format(sys.version)) +Logger.info('Python: Interpreter at "{}"'.format(sys.executable)) + +from kivy.logger import file_log_handler +if file_log_handler is not None: + file_log_handler.purge_logs() diff --git a/kivy/__pycache__/__init__.cpython-310.pyc b/kivy/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..5fe1e1c Binary files /dev/null and b/kivy/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/__pycache__/_version.cpython-310.pyc b/kivy/__pycache__/_version.cpython-310.pyc new file mode 100644 index 0000000..28097e9 Binary files /dev/null and b/kivy/__pycache__/_version.cpython-310.pyc differ diff --git a/kivy/__pycache__/animation.cpython-310.pyc b/kivy/__pycache__/animation.cpython-310.pyc new file mode 100644 index 0000000..fc28fe5 Binary files /dev/null and b/kivy/__pycache__/animation.cpython-310.pyc differ diff --git a/kivy/__pycache__/app.cpython-310.pyc b/kivy/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..687c9b4 Binary files /dev/null and b/kivy/__pycache__/app.cpython-310.pyc differ diff --git a/kivy/__pycache__/atlas.cpython-310.pyc b/kivy/__pycache__/atlas.cpython-310.pyc new file mode 100644 index 0000000..4ea5363 Binary files /dev/null and b/kivy/__pycache__/atlas.cpython-310.pyc differ diff --git a/kivy/__pycache__/base.cpython-310.pyc b/kivy/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..0d10618 Binary files /dev/null and b/kivy/__pycache__/base.cpython-310.pyc differ diff --git a/kivy/__pycache__/cache.cpython-310.pyc b/kivy/__pycache__/cache.cpython-310.pyc new file mode 100644 index 0000000..97173ad Binary files /dev/null and b/kivy/__pycache__/cache.cpython-310.pyc differ diff --git a/kivy/__pycache__/clock.cpython-310.pyc b/kivy/__pycache__/clock.cpython-310.pyc new file mode 100644 index 0000000..95f6834 Binary files /dev/null and b/kivy/__pycache__/clock.cpython-310.pyc differ diff --git a/kivy/__pycache__/compat.cpython-310.pyc b/kivy/__pycache__/compat.cpython-310.pyc new file mode 100644 index 0000000..69df9db Binary files /dev/null and b/kivy/__pycache__/compat.cpython-310.pyc differ diff --git a/kivy/__pycache__/config.cpython-310.pyc b/kivy/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..2b98289 Binary files /dev/null and b/kivy/__pycache__/config.cpython-310.pyc differ diff --git a/kivy/__pycache__/context.cpython-310.pyc b/kivy/__pycache__/context.cpython-310.pyc new file mode 100644 index 0000000..54bdaf1 Binary files /dev/null and b/kivy/__pycache__/context.cpython-310.pyc differ diff --git a/kivy/__pycache__/event.cpython-310.pyc b/kivy/__pycache__/event.cpython-310.pyc new file mode 100644 index 0000000..39b58e5 Binary files /dev/null and b/kivy/__pycache__/event.cpython-310.pyc differ diff --git a/kivy/__pycache__/factory.cpython-310.pyc b/kivy/__pycache__/factory.cpython-310.pyc new file mode 100644 index 0000000..355730a Binary files /dev/null and b/kivy/__pycache__/factory.cpython-310.pyc differ diff --git a/kivy/__pycache__/factory_registers.cpython-310.pyc b/kivy/__pycache__/factory_registers.cpython-310.pyc new file mode 100644 index 0000000..dfd687c Binary files /dev/null and b/kivy/__pycache__/factory_registers.cpython-310.pyc differ diff --git a/kivy/__pycache__/geometry.cpython-310.pyc b/kivy/__pycache__/geometry.cpython-310.pyc new file mode 100644 index 0000000..23ed951 Binary files /dev/null and b/kivy/__pycache__/geometry.cpython-310.pyc differ diff --git a/kivy/__pycache__/gesture.cpython-310.pyc b/kivy/__pycache__/gesture.cpython-310.pyc new file mode 100644 index 0000000..f089e90 Binary files /dev/null and b/kivy/__pycache__/gesture.cpython-310.pyc differ diff --git a/kivy/__pycache__/interactive.cpython-310.pyc b/kivy/__pycache__/interactive.cpython-310.pyc new file mode 100644 index 0000000..ab10bb5 Binary files /dev/null and b/kivy/__pycache__/interactive.cpython-310.pyc differ diff --git a/kivy/__pycache__/loader.cpython-310.pyc b/kivy/__pycache__/loader.cpython-310.pyc new file mode 100644 index 0000000..68191f2 Binary files /dev/null and b/kivy/__pycache__/loader.cpython-310.pyc differ diff --git a/kivy/__pycache__/logger.cpython-310.pyc b/kivy/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000..4f845a2 Binary files /dev/null and b/kivy/__pycache__/logger.cpython-310.pyc differ diff --git a/kivy/__pycache__/metrics.cpython-310.pyc b/kivy/__pycache__/metrics.cpython-310.pyc new file mode 100644 index 0000000..ee7a685 Binary files /dev/null and b/kivy/__pycache__/metrics.cpython-310.pyc differ diff --git a/kivy/__pycache__/multistroke.cpython-310.pyc b/kivy/__pycache__/multistroke.cpython-310.pyc new file mode 100644 index 0000000..06e43f7 Binary files /dev/null and b/kivy/__pycache__/multistroke.cpython-310.pyc differ diff --git a/kivy/__pycache__/parser.cpython-310.pyc b/kivy/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..b788e8b Binary files /dev/null and b/kivy/__pycache__/parser.cpython-310.pyc differ diff --git a/kivy/__pycache__/resources.cpython-310.pyc b/kivy/__pycache__/resources.cpython-310.pyc new file mode 100644 index 0000000..a51c2a3 Binary files /dev/null and b/kivy/__pycache__/resources.cpython-310.pyc differ diff --git a/kivy/__pycache__/setupconfig.cpython-310.pyc b/kivy/__pycache__/setupconfig.cpython-310.pyc new file mode 100644 index 0000000..458c5ab Binary files /dev/null and b/kivy/__pycache__/setupconfig.cpython-310.pyc differ diff --git a/kivy/__pycache__/support.cpython-310.pyc b/kivy/__pycache__/support.cpython-310.pyc new file mode 100644 index 0000000..c9dd93d Binary files /dev/null and b/kivy/__pycache__/support.cpython-310.pyc differ diff --git a/kivy/__pycache__/utils.cpython-310.pyc b/kivy/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..f97758e Binary files /dev/null and b/kivy/__pycache__/utils.cpython-310.pyc differ diff --git a/kivy/__pycache__/vector.cpython-310.pyc b/kivy/__pycache__/vector.cpython-310.pyc new file mode 100644 index 0000000..73374f4 Binary files /dev/null and b/kivy/__pycache__/vector.cpython-310.pyc differ diff --git a/kivy/__pycache__/weakmethod.cpython-310.pyc b/kivy/__pycache__/weakmethod.cpython-310.pyc new file mode 100644 index 0000000..9ade6b5 Binary files /dev/null and b/kivy/__pycache__/weakmethod.cpython-310.pyc differ diff --git a/kivy/_clock.cpython-310-x86_64-linux-gnu.so b/kivy/_clock.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..dfac127 Binary files /dev/null and b/kivy/_clock.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/_clock.pxd b/kivy/_clock.pxd new file mode 100644 index 0000000..6dd96ba --- /dev/null +++ b/kivy/_clock.pxd @@ -0,0 +1,131 @@ + +cdef class ClockEvent(object): + + cdef int _is_triggered + cdef public ClockEvent next + '''The next :class:`ClockEvent` in order they were scheduled. + ''' + cdef public ClockEvent prev + '''The previous :class:`ClockEvent` in order they were scheduled. + ''' + cdef public object cid + cdef public CyClockBase clock + '''The :class:`CyClockBase` instance associated with the event. + ''' + cdef public int loop + '''Whether this event repeats at intervals of :attr:`timeout`. + ''' + cdef public object weak_callback + cdef public object callback + cdef public double timeout + '''The duration after scheduling when the callback should be executed. + ''' + cdef public double _last_dt + cdef public double _dt + cdef public list _del_queue + + cdef public object clock_ended_callback + """A Optional callback for this event, which if provided is called by the clock + when the clock is stopped and the event was not ticked. + """ + cdef public object weak_clock_ended_callback + + cdef public int release_ref + """If True, the event should never release the reference to the callbacks. + If False, a weakref may be created instead. + """ + + cpdef get_callback(self) + cpdef get_clock_ended_callback(self) + cpdef cancel(self) + cpdef release(self) + cpdef tick(self, double curtime) + + +cdef class FreeClockEvent(ClockEvent): + + cdef public int free + '''Whether this event was scheduled as a free event. + ''' + + +cdef class CyClockBase(object): + + cdef public double _last_tick + cdef public int max_iteration + '''The maximum number of callback iterations at the end of the frame, before the next + frame. If more iterations occur, a warning is issued. + ''' + + cdef public double clock_resolution + '''If the remaining time until the event timeout is less than :attr:`clock_resolution`, + the clock will execute the callback even if it hasn't exactly timed out. + + If -1, the default, the resolution will be computed from config's ``maxfps``. + Otherwise, the provided value is used. Defaults to -1. + ''' + + cdef public double _max_fps + + cdef public ClockEvent _root_event + '''The first event in the chain. Can be None. + ''' + cdef public ClockEvent _next_event + '''During frame processing when we service the events, this points to the next + event that will be processed. After ticking an event, we continue with + :attr:`_next_event`. + + If a event that is canceled is the :attr:`_next_event`, :attr:`_next_event` + is shifted to point to the after after this, or None if it's at the end of the + chain. + ''' + cdef public ClockEvent _cap_event + '''The cap event is the last event in the chain for each frame. + For a particular frame, events may be added dynamically after this event, + and the current frame should not process them. + + Similarly to :attr:`_next_event`, + when canceling the :attr:`_cap_event`, :attr:`_cap_event` is shifted to the + one previous to it. + ''' + cdef public ClockEvent _last_event + '''The last event in the chain. New events are added after this. Can be None. + ''' + cdef public object _lock + cdef public object _lock_acquire + cdef public object _lock_release + + cdef public int has_started + cdef public int has_ended + cdef public object _del_safe_lock + cdef public int _del_safe_done + + cpdef get_resolution(self) + cpdef ClockEvent create_lifecycle_aware_trigger( + self, callback, clock_ended_callback, timeout=*, interval=*, release_ref=*) + cpdef ClockEvent create_trigger(self, callback, timeout=*, interval=*, release_ref=*) + cpdef schedule_lifecycle_aware_del_safe(self, callback, clock_ended_callback) + cpdef schedule_del_safe(self, callback) + cpdef ClockEvent schedule_once(self, callback, timeout=*) + cpdef ClockEvent schedule_interval(self, callback, timeout) + cpdef unschedule(self, callback, all=*) + cpdef _release_references(self) + cpdef _process_del_safe_events(self) + cpdef _process_events(self) + cpdef _process_events_before_frame(self) + cpdef get_min_timeout(self) + cpdef get_events(self) + cpdef get_before_frame_events(self) + cpdef _process_clock_ended_del_safe_events(self) + cpdef _process_clock_ended_callbacks(self) + + +cdef class CyClockBaseFree(CyClockBase): + + cpdef FreeClockEvent create_lifecycle_aware_trigger_free( + self, callback, clock_ended_callback, timeout=*, interval=*, release_ref=*) + cpdef FreeClockEvent create_trigger_free(self, callback, timeout=*, interval=*, release_ref=*) + cpdef FreeClockEvent schedule_once_free(self, callback, timeout=*) + cpdef FreeClockEvent schedule_interval_free(self, callback, timeout) + cpdef _process_free_events(self, double last_tick) + cpdef get_min_free_timeout(self) diff --git a/kivy/_event.cpython-310-x86_64-linux-gnu.so b/kivy/_event.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..0e48f41 Binary files /dev/null and b/kivy/_event.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/_event.pxd b/kivy/_event.pxd new file mode 100644 index 0000000..502f681 --- /dev/null +++ b/kivy/_event.pxd @@ -0,0 +1,68 @@ +from cpython.ref cimport PyObject + +cdef dict cache_properties_per_cls + +cdef class ObjectWithUid(object): + cdef readonly int uid + + +cdef class Observable(ObjectWithUid): + cdef object __fbind_mapping + cdef object bound_uid + + +cdef class EventDispatcher(ObjectWithUid): + cdef dict __event_stack + cdef dict __properties + cdef dict __storage + cdef object __weakref__ + cdef public set _kwargs_applied_init + cdef public object _proxy_ref + 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 EventObservers observers + + cdef void set_largs(self, tuple largs) + + +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 BoundCallback make_callback(self, object observer, tuple largs, dict kwargs, int is_ref, uid=*) + cdef inline void bind(self, object observer, object src_observer, int is_ref) except * + cdef inline object fbind(self, object observer, tuple largs, dict kwargs, int is_ref) + cdef inline BoundCallback fbind_callback(self, object observer, tuple largs, dict kwargs, int is_ref) + cdef inline void fbind_existing_callback(self, BoundCallback callback) + cdef inline void unbind(self, object observer, int stop_on_first) except * + cdef inline void funbind(self, object observer, tuple largs, dict kwargs) except * + cdef inline object unbind_uid(self, object uid) + cdef inline object unbind_callback(self, BoundCallback callback) + 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 diff --git a/kivy/_metrics.cpython-310-x86_64-linux-gnu.so b/kivy/_metrics.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..9bf17f8 Binary files /dev/null and b/kivy/_metrics.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/_metrics.pxd b/kivy/_metrics.pxd new file mode 100644 index 0000000..2c14eac --- /dev/null +++ b/kivy/_metrics.pxd @@ -0,0 +1,5 @@ +from kivy._event cimport EventObservers + +cdef EventObservers pixel_scale_observers + +cpdef float dpi2px(value, str ext) except * diff --git a/kivy/_version.py b/kivy/_version.py new file mode 100644 index 0000000..c713fbf --- /dev/null +++ b/kivy/_version.py @@ -0,0 +1,17 @@ +# This file is imported from __init__.py and exec'd from setup.py + +MAJOR = 2 +MINOR = 1 +MICRO = 0 +RELEASE = True + +__version__ = '%d.%d.%d' % (MAJOR, MINOR, MICRO) + +if not RELEASE: + # if it's a rcx release, it's not proceeded by a period. If it is a + # devx release, it must start with a period + __version__ += '' + + +_kivy_git_hash = '023bd79b90f9831b45bb8eb449346648aa5fe5f8' +_kivy_build_date = '20220306' diff --git a/kivy/animation.py b/kivy/animation.py new file mode 100644 index 0000000..2dbff0b --- /dev/null +++ b/kivy/animation.py @@ -0,0 +1,832 @@ +''' +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 collections import ChainMap +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 0, which means + the animation is updated for every frame. + + To update the animation less often, set the step value to a float. + For example, if you want to animate at 30 FPS, use s=1/30. + + :Events: + `on_start`: animation, widget + Fired when the animation is started on a widget. + `on_complete`: animation, widget + Fired when the animation is completed or stopped on a widget. + `on_progress`: animation, widget, progression + Fired when the progression of the animation is changing. + + .. versionchanged:: 1.4.0 + Added s/step parameter. + + .. versionchanged:: 1.10.0 + The default value of the step parameter was changed from 1/60. to 0. + ''' + + _update_ev = None + + _instances = set() + + __events__ = ('on_start', 'on_progress', 'on_complete') + + def __init__(self, **kw): + super().__init__() + # Initialize + self._clock_installed = False + self._duration = kw.pop('d', kw.pop('duration', 1.)) + self._transition = kw.pop('t', kw.pop('transition', 'linear')) + self._step = kw.pop('s', kw.pop('step', 0)) + if isinstance(self._transition, string_types): + self._transition = getattr(AnimationTransition, self._transition) + 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 + + .. versionchanged:: 2.1.0 + If the parameter ``widget`` is None, all animated widgets will be + the target and cancelled. If ``largs`` is also given, animation of + these properties will be canceled for all animated widgets. + ''' + if widget is None: + if largs: + for animation in Animation._instances.copy(): + for info in tuple(animation._widgets.values()): + widget = info['widget'] + for x in largs: + animation.cancel_property(widget, x) + else: + for animation in Animation._instances: + animation._widgets.clear() + animation._clock_uninstall() + Animation._instances.clear() + return + 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 further. 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 + self._update_ev = 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 + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = None + + 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 + self._widgets.pop(uid, None) + self._clock_uninstall() + if not self._widgets: + self._unregister() + 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 CompoundAnimation(Animation): + + 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().cancel(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`) + + This method overrides `:class:kivy.animation.Animation`'s + version, to cancel it on all animations of the Sequence. + + .. versionadded:: 1.10.0 + ''' + self.anim1.cancel_property(widget, prop) + self.anim2.cancel_property(widget, prop) + if (not self.anim1.have_properties_to_animate(widget) and + not self.anim2.have_properties_to_animate(widget)): + self.cancel(widget) + + def have_properties_to_animate(self, widget): + return (self.anim1.have_properties_to_animate(widget) or + self.anim2.have_properties_to_animate(widget)) + + @property + def animated_properties(self): + return ChainMap({}, + self.anim2.animated_properties, + self.anim1.animated_properties) + + @property + def transition(self): + # This property is impossible to implement + raise AttributeError( + "Can't lookup transition attribute of a CompoundAnimation") + + +class Sequence(CompoundAnimation): + + def __init__(self, anim1, anim2): + super().__init__() + + #: Repeat the sequence. See 'Repeating animation' in the header + #: documentation. + self.repeat = False + + self.anim1 = anim1 + self.anim2 = anim2 + + self.anim1.bind(on_complete=self.on_anim1_complete, + 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 stop(self, widget): + props = self._widgets.pop(widget.uid, None) + self.anim1.stop(widget) + self.anim2.stop(widget) + if props: + self.dispatch('on_complete', widget) + super().cancel(widget) + + def start(self, widget): + self.stop(widget) + self._widgets[widget.uid] = True + self._register() + self.dispatch('on_start', widget) + self.anim1.start(widget) + + def on_anim1_complete(self, instance, widget): + if widget.uid not in self._widgets: + return + 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 widget.uid not in self._widgets: + return + if self.repeat: + self.anim1.start(widget) + else: + self.dispatch('on_complete', widget) + self.cancel(widget) + + def on_anim2_progress(self, instance, widget, progress): + self.dispatch('on_progress', widget, .5 + progress / 2.) + + +class Parallel(CompoundAnimation): + + def __init__(self, anim1, anim2): + super().__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 stop(self, widget): + self.anim1.stop(widget) + self.anim2.stop(widget) + if self._widgets.pop(widget.uid, None): + self.dispatch('on_complete', widget) + super().cancel(widget) + + 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 on_anim_complete(self, instance, widget): + self._widgets[widget.uid]['complete'] += 1 + if self._widgets[widget.uid]['complete'] == 2: + self.stop(widget) + + +class AnimationTransition: + '''Collection of animation functions to be used with the Animation object. + Easing Functions ported to Kivy from the Clutter Project + https://developer.gnome.org/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 diff --git a/kivy/app.py b/kivy/app.py new file mode 100644 index 0000000..c571b1b --- /dev/null +++ b/kivy/app.py @@ -0,0 +1,1191 @@ +''' +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 relationship between main.py and test.kv is explained in +:meth:`App.load_kv`. + +.. _Application configuration: + +Application configuration +------------------------- + +Use the configuration file +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Your application might need its own configuration file. The +:class:`App` class handles 'ini' files automatically if you add +the section key-value pair to the :meth:`App.build_config` method using the +`config` parameter (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 to the config, a file is created on the +disk (see :attr:`~App.get_application_config` for its location) and +named based your class name. "TestApp" will give a config file named +"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 has been changed to', value) + elif token == ('section1', 'key2'): + print('Our key2 has 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 + +.. versionadded:: 1.0.7 + +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, then :meth:`App.on_stop` is called. + #. If True is returned (default case), 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. + +Asynchronous app +---------------- + +In addition to running an app normally, +Kivy can be run within an async event loop such as provided by the standard +library asyncio package or the trio package (highly recommended). + +Background +~~~~~~~~~~ + +Normally, when a Kivy app is run, it blocks the thread that runs it until the +app exits. Internally, at each clock iteration it executes all the app +callbacks, handles graphics and input, and idles by sleeping for any remaining +time. + +To be able to run asynchronously, the Kivy app may not sleep, but instead must +release control of the running context to the asynchronous event loop running +the Kivy app. We do this when idling by calling the appropriate functions of +the async package being used instead of sleeping. + +Async configuration +~~~~~~~~~~~~~~~~~~~ + +To run a Kivy app asynchronously, either the :func:`async_runTouchApp` or +:meth:`App.async_run` coroutine must be scheduled to run in the event loop of +the async library being used. + +The environmental variable ``KIVY_EVENTLOOP`` or the ``async_lib`` parameter in +:func:`async_runTouchApp` and :meth:`App.async_run` set the async +library that Kivy uses internally when the app is run with +:func:`async_runTouchApp` and :meth:`App.async_run`. It can be set to one of +`"asyncio"` when the standard library `asyncio` is used, or `"trio"` if the +trio library is used. If the environment variable is not set and ``async_lib`` +is not provided, the stdlib ``asyncio`` is used. + +:meth:`~kivy.clock.ClockBaseBehavior.init_async_lib` can also be directly +called to set the async library to use, but it may only be called before the +app has begun running with :func:`async_runTouchApp` or :meth:`App.async_run`. + +To run the app asynchronously, one schedules :func:`async_runTouchApp` +or :meth:`App.async_run` to run within the given library's async event loop as +in the examples shown below. Kivy is then treated as just another coroutine +that the given library runs in its event loop. Internally, Kivy will use the +specified async library's API, so ``KIVY_EVENTLOOP`` or ``async_lib`` must +match the async library that is running Kivy. + + +For a fuller basic and more advanced examples, see the demo apps in +``examples/async``. + +Asyncio example +~~~~~~~~~~~~~-- + +.. code-block:: python + + import asyncio + + from kivy.app import async_runTouchApp + from kivy.uix.label import Label + + + loop = asyncio.get_event_loop() + loop.run_until_complete( + async_runTouchApp(Label(text='Hello, World!'), async_lib='asyncio')) + loop.close() + +Trio example +~~~~~~~~~~-- + +.. code-block:: python + + import trio + + from kivy.app import async_runTouchApp + from kivy.uix.label import Label + + from functools import partial + + # use functools.partial() to pass keyword arguments: + async_runTouchApp_func = partial(async_runTouchApp, async_lib='trio') + + trio.run(async_runTouchApp_func, Label(text='Hello, World!')) + +Interacting with Kivy app from other coroutines +----------------------------------------------- + +It is fully safe to interact with any kivy object from other coroutines +running within the same async event loop. This is because they are all running +from the same thread and the other coroutines are only executed when Kivy +is idling. + +Similarly, the kivy callbacks may safely interact with objects from other +coroutines running in the same event loop. Normal single threaded rules apply +to both case. + +.. versionadded:: 2.0.0 + +''' + +__all__ = ('App', 'runTouchApp', 'async_runTouchApp', 'stopTouchApp') + +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, async_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 +from kivy.uix.widget import Widget +from kivy.properties import ObjectProperty, StringProperty +from kivy.setupconfig import USE_SDL2 + + +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', + 'on_config_change', ) + + # Stored so that we only need to determine this once + _user_data_dir = "" + + 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'): + ''' + 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: /..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` + + .. versionadded:: 1.0.7 + + .. versionchanged:: 1.4.0 + Customized the defaultpath for iOS and Android platforms. Added a + defaultpath parameter for desktop OS's (not applicable to iOS + and Android.) + + .. versionchanged:: 1.11.0 + Changed the Android version to make use of the + :attr:`~App.user_data_dir` and added a missing dot to the iOS + config file name. + ''' + + if platform == 'android': + return join(self.user_data_dir, '.{0}.ini'.format(self.name)) + 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 + + def _get_user_data_dir(self): + # Determine and return the user_data_dir. + data_dir = "" + if platform == 'ios': + data_dir = expanduser(join('~/Documents', self.name)) + elif platform == 'android': + from jnius import autoclass, cast + PythonActivity = autoclass('org.kivy.android.PythonActivity') + context = cast('android.content.Context', PythonActivity.mActivity) + file_p = cast('java.io.File', context.getFilesDir()) + data_dir = file_p.getAbsolutePath() + elif platform == 'win': + data_dir = os.path.join(os.environ['APPDATA'], self.name) + elif platform == 'macosx': + data_dir = '~/Library/Application Support/{}'.format(self.name) + data_dir = expanduser(data_dir) + else: # _platform == 'linux' or anything else...: + data_dir = os.environ.get('XDG_CONFIG_HOME', '~/.config') + data_dir = expanduser(join(data_dir, self.name)) + if not exists(data_dir): + os.mkdir(data_dir) + return data_dir + + @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 Windows, `%APPDATA%/` is returned. + + On OS X, `~/Library/Application Support/` is returned. + + On Linux, `$XDG_CONFIG_HOME/` is returned. + + On Android, `Context.GetFilesDir + `_ is returned. + + .. versionchanged:: 1.11.0 + + On Android, this function previously returned + `/sdcard/`. This folder became read-only by default + in Android API 26 and the user_data_dir has therefore been moved + to a writeable location. + + ''' + if self._user_data_dir == "": + self._user_data_dir = self._get_user_data_dir() + return self._user_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_prepare(self): + 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') + + def run(self): + '''Launches the app in standalone mode. + ''' + self._run_prepare() + runTouchApp() + self.stop() + + async def async_run(self, async_lib=None): + '''Identical to :meth:`run`, but is a coroutine and can be + scheduled in a running async event loop. + + See :mod:`kivy.app` for example usage. + + .. versionadded:: 2.0.0 + ''' + self._run_prepare() + await async_runTouchApp(async_lib=async_lib) + 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 + if self._app_window: + for child in self._app_window.children: + self._app_window.remove_widget(child) + App._running_app = None + + 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. + + 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 True. + + .. versionadded:: 1.1.0 + .. versionchanged:: 1.10.0 + The default return value is now True. + ''' + return True + + 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. + + .. versionchanged:: 1.10.1 + Added corresponding ``on_config_change`` event. + ''' + 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.dispatch('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' and not USE_SDL2: + 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()) diff --git a/kivy/atlas.py b/kivy/atlas.py new file mode 100644 index 0000000..c893ca3 --- /dev/null +++ b/kivy/atlas.py @@ -0,0 +1,456 @@ +''' +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 2 or more 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. If the +size you specify is not large enough to fit all of the source images, more +atlas images will be created as required e.g. ``myatlas-1.png``, +``myatlas-2.png`` etc. + +.. 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 specify the images by supplying the path:: + + 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:: + + a = Button(background_normal='atlas://images/myatlas/button', + background_down='atlas://images/myatlas/button_down') + +In other words, the path to the images is replaced by:: + + atlas://path/to/myatlas/id + # will search for the ``path/to/myatlas.atlas`` and get the image ``id`` + +.. 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, ListProperty +import os + + +# late import to prevent recursion +CoreImage = None + + +class Atlas(EventDispatcher): + '''Manage texture atlas. See module documentation for more information. + ''' + + original_textures = ListProperty([]) + '''List of original atlas textures (which contain the :attr:`textures`). + + :attr:`original_textures` is a :class:`~kivy.properties.ListProperty` and + defaults to []. + + .. versionadded:: 1.9.1 + ''' + + 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) + atlas_texture = ci.texture + self.original_textures.append(atlas_texture) + + # 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] = atlas_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. If the size is not large enough to + fit all of the source images, more atlas images will created + as required. + `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 = list(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 appropriate 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 = list(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')) diff --git a/kivy/base.py b/kivy/base.py new file mode 100644 index 0000000..cbe693d --- /dev/null +++ b/kivy/base.py @@ -0,0 +1,617 @@ +# pylint: disable=W0611 +''' +Kivy Base +========= + +This module contains the Kivy core functionality and is not intended for end +users. Feel free to look through it, but bare in mind that calling any of +these methods directly may result in an unpredictable behavior as the calls +access directly the event loop of an application. +''' + +__all__ = ( + 'EventLoop', + 'EventLoopBase', + 'ExceptionHandler', + 'ExceptionManagerBase', + 'ExceptionManager', + 'runTouchApp', + 'async_runTouchApp', + 'stopTouchApp', +) + +import sys +import os +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 caught by ExceptionHandler') + return ExceptionManager.PASS + + ExceptionManager.add_handler(E()) + + Then, all exceptions will be set to PASS, and logged to the console! + ''' + + def handle_exception(self, exception): + '''Called by :class:`ExceptionManagerBase` to handle a exception. + + Defaults to returning :attr:`ExceptionManager.RAISE` that re-raises the + exception. Return :attr:`ExceptionManager.PASS` to indicate that the + exception was handled and should be ignored. + + This may be called multiple times with the same exception, if + :attr:`ExceptionManager.RAISE` is returned as the exception bubbles + through multiple kivy exception handling levels. + ''' + return ExceptionManager.RAISE + + +class ExceptionManagerBase: + '''ExceptionManager manages exceptions handlers.''' + + RAISE = 0 + """The exception should be re-raised. + """ + PASS = 1 + """The exception should be ignored as it was handled by the handler. + """ + + def __init__(self): + self.handlers = [] + self.policy = ExceptionManagerBase.RAISE + + def add_handler(self, cls): + '''Add a new exception handler to the stack.''' + if cls not in self.handlers: + self.handlers.append(cls) + + def remove_handler(self, cls): + '''Remove the exception handler from the stack.''' + if cls in self.handlers: + self.handlers.remove(cls) + + def handle_exception(self, inst): + '''Called when an exception occurred in the :func:`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: ExceptionManagerBase = register_context( + 'ExceptionManager', ExceptionManagerBase) +"""The :class:`ExceptionManagerBase` instance that handles kivy exceptions. +""" + + +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.stopping = False + 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. + + .. versionchanged:: 2.1.0 + Provider will be also removed if it exist in auto-remove list. + ''' + if provider in self.input_providers: + self.input_providers.remove(provider) + if provider in self.input_providers_autoremove: + self.input_providers_autoremove.remove(provider) + + def add_event_listener(self, listener): + '''Add a new event listener for getting touch events. + ''' + if listener not 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 before :meth:`EventLoopBase.run()`. This starts all + configured input providers. + + .. versionchanged:: 2.1.0 + Method can be called multiple times, but event loop will start only + once. + ''' + if self.status == 'started': + return + self.status = 'started' + self.quit = False + Clock.start_clock() + 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()`. + + .. versionchanged:: 2.1.0 + Method can be called multiple times, but event loop will stop only + once. + ''' + if self.status != 'started': + return + # 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() + self.remove_input_provider(provider) + + # ensure any restart will not break anything later. + self.input_events = [] + + Clock.stop_clock() + self.stopping = False + 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 remove_android_splash(self, *args): + '''Remove android presplash in SDL2 bootstrap.''' + try: + from android import remove_presplash + remove_presplash() + except ImportError: + Logger.warning( + 'Base: Failed to import "android" module. ' + 'Could not remove android presplash.') + return + + def post_dispatch_input(self, etype, me): + '''This function is called by :meth:`EventLoopBase.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 + if not me.is_touch: + # Non-touch event must be handled by the event manager + return + me.grab_state = True + for weak_widget in me.grab_list[:]: + # weak_widget is a weak reference to widget + wid = weak_widget() + if wid is None: + # object is gone, stop. + me.grab_list.remove(weak_widget) + continue + root_window = wid.get_root_window() + if wid != root_window and root_window is not None: + me.push() + try: + root_window.transform_motion_event_2d(me, wid) + except AttributeError: + 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 + me.dispatch_done() + + 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 :meth:`EventLoopBase.idle()` to read events from input + providers, pass events to postproc, and dispatch final events. + ''' + + # first, acquire 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 mainloop(self): + while not self.quit and self.status == 'started': + try: + self.idle() + if self.window: + self.window.mainloop() + except BaseException as inst: + # use exception manager first + r = ExceptionManager.handle_exception(inst) + if r == ExceptionManager.RAISE: + stopTouchApp() + raise + else: + pass + + async def async_mainloop(self): + while not self.quit and self.status == 'started': + try: + await self.async_idle() + if self.window: + self.window.mainloop() + except BaseException as inst: + # use exception manager first + r = ExceptionManager.handle_exception(inst) + if r == ExceptionManager.RAISE: + stopTouchApp() + raise + else: + pass + + Logger.info("Window: exiting mainloop and closing.") + self.close() + + 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 + if not self.quit: + self.dispatch_input() + + # flush all the canvas operation + if not self.quit: + Builder.sync() + + # tick before draw + if not self.quit: + Clock.tick_draw() + + # flush all the canvas operation + if not self.quit: + Builder.sync() + + if not self.quit: + 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 + + async def async_idle(self): + '''Identical to :meth:`idle`, but instead used when running + within an async event loop. + ''' + + # update dt + await Clock.async_tick() + + # read and dispatch input from providers + if not self.quit: + self.dispatch_input() + + # flush all the canvas operation + if not self.quit: + Builder.sync() + + # tick before draw + if not self.quit: + Clock.tick_draw() + + # flush all the canvas operation + if not self.quit: + Builder.sync() + + if not self.quit: + 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 _runTouchApp_prepare(widget=None): + from kivy.input import MotionEventFactory, kivy_postproc_modules + + # Ok, we got one widget, and we are not in embedded 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() + + # remove presplash on the next frame + if platform == 'android': + Clock.schedule_once(EventLoop.remove_android_splash) + + # in non-embedded mode, there 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 loop + # ourself (previous behavior.) + # + + +def runTouchApp(widget=None, embedded=False): + '''Static main function that starts the application loop. + You can access some magic via the following arguments: + + See :mod:`kivy.app` for example usage. + + :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. + + `embedded` + No event dispatching is done. This will be your job. + + `widget + embedded` + 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 useful for embedding Kivy + in another toolkit. (like Qt, check kivy-designed) + + ''' + _runTouchApp_prepare(widget=widget) + + # we are in embedded mode, don't do dispatching. + if embedded: + return + + try: + EventLoop.mainloop() + finally: + stopTouchApp() + + +async def async_runTouchApp(widget=None, embedded=False, async_lib=None): + '''Identical to :func:`runTouchApp` but instead it is a coroutine + that can be run in an existing async event loop. + + ``async_lib`` is the async library to use. See :mod:`kivy.app` for details + and example usage. + + .. versionadded:: 2.0.0 + ''' + if async_lib is not None: + Clock.init_async_lib(async_lib) + _runTouchApp_prepare(widget=widget) + + # we are in embedded mode, don't do dispatching. + if embedded: + return + + try: + await EventLoop.async_mainloop() + finally: + stopTouchApp() + + +def stopTouchApp(): + '''Stop the current application by leaving the main loop. + + See :mod:`kivy.app` for example usage. + ''' + if EventLoop is None: + return + if EventLoop.status in ('stopped', 'closed'): + return + if EventLoop.status != 'started': + if not EventLoop.stopping: + EventLoop.stopping = True + Clock.schedule_once(lambda dt: stopTouchApp(), 0) + return + Logger.info('Base: Leaving application in progress...') + EventLoop.close() diff --git a/kivy/cache.py b/kivy/cache.py new file mode 100644 index 0000000..48d9cd0 --- /dev/null +++ b/kivy/cache.py @@ -0,0 +1,262 @@ +''' +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. +''' + +from os import environ +from kivy.logger import Logger +from kivy.clock import Clock + +__all__ = ('Cache', ) + + +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. + + :raises: + `ValueError`: If `None` is used as `key`. + + .. versionchanged:: 2.0.0 + Raises `ValueError` if `None` is used as `key`. + + ''' + # check whether obj should not be cached first + if getattr(obj, '_nocache', False): + return + if key is None: + # This check is added because of the case when key is None and + # one of purge methods gets called. Then loop in purge method will + # call Cache.remove with key None which then clears entire + # category from Cache making next iteration of loop to raise a + # KeyError because next key will not exist. + # See: https://github.com/kivy/kivy/pull/6950 + raise ValueError('"None" cannot be used as key in Cache') + try: + cat = Cache._categories[category] + except KeyError: + Logger.warning('Cache: category <%s> does not exist' % category) + return + + timeout = timeout or cat['timeout'] + + 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 + argument is not supplied, the entire category will be purged. + ''' + try: + if key is not None: + del Cache._objects[category][key] + Logger.trace('Cache: Removed %s:%s from cache' % + (category, key)) + else: + Cache._objects[category] = {} + Logger.trace('Cache: Flushed category %s from cache' % + category) + except Exception: + pass + + @staticmethod + def _purge_oldest(category, maxpurge=1): + Logger.trace('Cache: Remove oldest in %s' % category) + import heapq + time = Clock.get_time() + heap_list = [] + for key in Cache._objects[category]: + obj = Cache._objects[category][key] + if obj['lastaccess'] == obj['timestamp'] == time: + continue + heapq.heappush(heap_list, (obj['lastaccess'], key)) + Logger.trace('Cache: <<< %f' % obj['lastaccess']) + n = 0 + while n <= maxpurge: + try: + n += 1 + lastaccess, key = heapq.heappop(heap_list) + Logger.trace('Cache: %d => %s %f %f' % + (n, key, lastaccess, Clock.get_time())) + except Exception: + return + Cache.remove(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 automatically 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: + Logger.trace('Cache: Removed %s:%s from cache due to ' + 'timeout' % (category, key)) + Cache.remove(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) diff --git a/kivy/clock.py b/kivy/clock.py new file mode 100644 index 0000000..a52b3c9 --- /dev/null +++ b/kivy/clock.py @@ -0,0 +1,1172 @@ +''' +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 canceled and won't + repeat. + +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 class method 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 + +:meth:`CyClockBase.create_trigger` is an advanced method way to defer a +callback. It functions exactly like :meth:`CyClockBase.schedule_once` and +:meth:`CyClockBase.schedule_interval` except that it doesn't immediately +schedule the callback. Instead, one schedules the callback using the +:class:`ClockEvent` returned by it. This ensures that you can call the event +multiple times but it won't be scheduled more than once. This is not the case +with :meth:`CyClockBase.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 + event = Clock.create_trigger(my_callback) + event() + event() + + # will also run the callback only once before the next frame + event = Clock.schedule_once(my_callback) # now it's already scheduled + event() # won't be scheduled again + event() + +In addition, it is more convenient to create and bind to +the triggered event than using :meth:`CyClockBase.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. + +Unscheduling +------------- + +An event scheduled with :meth:`CyClockBase.schedule_once`, +:meth:`CyClockBase.schedule_interval`, or with +:meth:`CyClockBase.create_trigger` and then triggered can be unscheduled in +multiple ways. E.g:: + + def my_callback(dt): + pass + + # call my_callback every 0.5 seconds + event = Clock.schedule_interval(my_callback, 0.5) + + # call my_callback in 5 seconds + event2 = Clock.schedule_once(my_callback, 5) + + event_trig = Clock.create_trigger(my_callback, 5) + event_trig() + + # unschedule using cancel + event.cancel() + + # unschedule using Clock.unschedule + Clock.unschedule(event2) + + # unschedule using Clock.unschedule with the callback + # NOT RECOMMENDED + Clock.unschedule(my_callback) + +The best way to unschedule a callback is with :meth:`ClockEvent.cancel`. +:meth:`CyClockBase.unschedule` is mainly an alias for that for that function. +However, if the original callback itself is passed to +:meth:`CyClockBase.unschedule`, it'll unschedule all instances of that +callback (provided ``all`` is True, the default, otherwise only the first match +is removed). + +Calling :meth:`CyClockBase.unschedule` on the original callback is highly +discouraged because it's significantly slower than when using the event. + +Clock Lifecycle +--------------- + +Kivy's clock has a lifecycle. By default, scheduling a callback after the Clock +has ended will not raise an error, even though the callback may never be +called. That's because most callbacks are like services, e.g. responding to a +user button press - if the app is running the callbacks need to service the app +and respond to the input, but once the app has stopped or is stopping, we can +safely not process these events. + +Other events always need to be processed. E.g. another thread may request a +callback in kivy's thread and then process some result. If the event is not +processed in Kivy's thread because the app stopped, the second thread may +block forever hanging the application as it exits. + +Consequently, we provide a API +(:meth:`CyClockBase.create_lifecycle_aware_trigger`) for scheduling callbacks +that raise a :class:`ClockNotRunningError` if the clock has stopped. If the +scheduling succeeded it guarantees that one of its callbacks will be called. +I.e. the new :meth:`CyClockBase.create_lifecycle_aware_trigger` accepts an +additional ``clock_ended_callback`` parameter. Normally, ``callback`` will be +called when the event is processed. But, if the clock is stopped before it can +be processed, if the application exited normally (and the app was started) and +the event wasn't canceled, and the callbacks are not garbage collected, then +``clock_ended_callback`` will be called instead when the clock is stopped. + +That is, given these conditions, if :class:`ClockNotRunningError` was not +raised when the event was scheduled, then one of these callbacks will be +called - either ``callback`` if the event executed normally, or +``clock_ended_callback`` if the clock is stopped while the event is scheduled. + +By default, events can be scheduled before the clock is started because it is +assumed the clock will eventually be started when the app starts. I.e. +calling :meth:`CyClockBase.create_lifecycle_aware_trigger` before the clock +and application starts will succeed. But if the app never actually starts, then +neither of the callbacks may be executed. + +.. versionadded:: 2.0.0 + The lifecycle was added in 2.0.0 + +Exception Handling +------------------ + +Kivy provides a exception handling manager, +:attr:`~kivy.base.ExceptionManager`, to handle its internal exceptions +including exceptions raised by clock callbacks, without crashing the +application. By default when an exception is raised, the app will crash. +But, if a handler is registered with the exception manager and the handler +handles the exception, the app will not crash and will continue as normal.:: + + from kivy.base import ExceptionHandler, ExceptionManager + class MyHandler(ExceptionHandler): + def handle_exception(self, inst): + if isinstance(inst, ValueError): + Logger.exception('ValueError caught by MyHandler') + return ExceptionManager.PASS + return ExceptionManager.RAISE + + ExceptionManager.add_handler(MyHandler()) + +Then, all ValueError exceptions will be logged to the console and ignored. +Similarly, if a scheduled clock callback raises a ValueError, other clock +events will still be processed normally. + +If an event's callback raises an exception, before the exception handler is +executed, the callback is immediately canceled. + +It still is possible for the app to be corrupted if kivy itself is the source +of the exception. I.e. even with a handler that ignores exceptions and doesn't +crash, the app may be in a corrupted state if the error originates from within +Kivy itself. However, the exception handler can help protect the app from +crashing and can help protect against user callbacks crashing the app. + +.. versionchanged:: 2.0.0 + Prior to Kivy 2.0.0, an exception raised in a event's callback would + cause the clock to crash and subsequent events may or may not be executed. + Even if the exception was handled by an + :class:`~kivy.base.ExceptionHandler`, there was no guarantee that some + scheduled events would not be skipped. + + From 2.0.0 onward, if a event's exception is handled by an + :class:`~kivy.base.ExceptionHandler`, other events will be shielded from + the exception and will execute normally. + +Scheduling from ``__del__`` +--------------------------- + +It is not safe to schedule Clock events from a object's ``__del__`` or +``__dealloc__`` method. If you must schedule a Clock call from this method, use +:meth:`CyClockBase.schedule_del_safe` or +:meth:`CyClockBase.schedule_lifecycle_aware_del_safe` instead. + +Threading and Callback Order +----------------------------- + +Beginning with 1.10.0, all the events scheduled for the same frame, e.g. +all the events scheduled in the same frame with a ``timeout`` of ``0``, +well be executed in the order they were scheduled. + +Also, all the scheduling and canceling methods are fully thread safe and +can be safely used from external threads. + +As a a consequence, calling :meth:`CyClockBase.unschedule` with the original +callback is now significantly slower and highly discouraged. Instead, the +returned events should be used to cancel. As a tradeoff, all the other methods +are now significantly faster than before. + +Advanced Clock Details +----------------------- + +The following section goes into the internal kivy clock details as well +as the various clock options. It is meant only for advanced users. + +Fundamentally, the Kivy clock attempts to execute any scheduled callback +rhythmically as determined by the specified fps (frame per second, see +``maxfps`` in :mod:`~kivy.config`). That is, ideally, given e.g. a desired fps +of 30, the clock will execute the callbacks at intervals of 1 / 30 seconds, or +every 33.33 ms. All the callbacks in a frame are given the same timestamp, +i.e. the ``dt`` passed to the callback are all the same and it's the difference +in time between the start of this and the previous frame. + +Because of inherent indeterminism, the frames do not actually occur exactly +at intervals of the fps and ``dt`` may be under or over the desired fps. +Also, once the timeout is "close enough" to the desired timeout, as determined +internally, Kivy will execute the callback in the current frame even when the +"actual time" has not elapsed the ``timeout`` amount. + +Kivy offers now, since ``1.10.0``, multiple clocks with different behaviors. + +Default Clock +^^^^^^^^^^^^^^ + +The default clock (``default``) behaves as described above. When a callback +with a timeout of zero or non-zero is scheduled, they are executed at the frame +that is near the timeout, which is a function of the fps. So a timeout of zero +would still result in a delay of one frame or about 1 / fps, typically a bit +less but sometimes more depending on the CPU usage of the other events +scheduled for that frame. + +In a test using a fps of 30, a callback with a timeout of 0, 0.001, and 0.05, +resulted in a mean callback delay of 0.02487, 0.02488, and 0.05011 seconds, +respectively. When tested with a fps of 600 the delay for 0.05 was similar, +except the standard deviation was reduced resulting in overall better accuracy. + +Interruptible Clock +^^^^^^^^^^^^^^^^^^^^ + +The default clock suffers from the quantization problem, as frames occur only +on intervals and any scheduled timeouts will not be able to occur during an +interval. For example, with the timeout of 0.05, while the mean was 0.05011, +its values ranged between 0.02548 - 0.07348 and a standard deviation of 0.002. +Also, there's the minimum timeout of about 0.02487. + +The interruptible clock (``interrupt``) will execute timeouts even during a +frame. So a timeout of zero will execute as quickly as possible and similarly +a non-zero timeout will be executed even during the interval. + +This clock, and all the clocks described after this have an option, +:attr:`ClockBaseInterruptBehavior.interupt_next_only`. When True, any of the +behavior new behavior will only apply to the callbacks with a timeout of +zero. Non-zero timeouts will behave like in the default clock. E.g. for this +clock when True, only zero timeouts will execute during the the interval. + +In a test using a fps of 30, a callback with a timeout of 0, 0.001, and 0.05, +resulted in a mean callback delay of 0.00013, 0.00013, and 0.04120 seconds, +respectively when :attr:`ClockBaseInterruptBehavior.interupt_next_only` was +False. Also, compared to the default clock the standard deviation was reduced. +When :attr:`ClockBaseInterruptBehavior.interupt_next_only` was True, the values +were 0.00010, 0.02414, and 0.05034, respectively. + +Free Clock +^^^^^^^^^^^ + +The interruptible clock may not be ideal for all cases because all the events +are executed during the intervals and events are not executed anymore +rhythmically as multiples of the fps. For example, there may not be any benefit +for the graphics to update in a sub-interval, so the additional accuracy +wastes CPU. + +The Free clock (``free_all``) solves this by having ``Clock.xxx_free`` versions +of all the Clock scheduling methods. By free, we mean the event is free from +the fps because it's not fps limited. E.g. +:meth:`CyClockBaseFree.create_trigger_free` corresponds to +:meth:`CyClockBase.create_trigger`. Only when an event scheduled using the +``Clock.xxx_free`` methods is present will the clock interrupt and execute +the events during the interval. So, if no ``free`` event is present the clock +behaves like the ``default`` clock, otherwise it behaves like the ``interrupt`` +clock. + +In a test using a fps of 30, a callback with a timeout of 0s, 0.001s, and +0.05s, resulted in a mean callback delay of 0.00012s, 0.00017s, and 0.04121s +seconds, respectively when it was a free event and 0.02403s, 0.02405s, and +0.04829s, respectively when it wasn't. + +Free Only Clock +^^^^^^^^^^^^^^^^^ + +The Free clock executes all events when a free event was scheduled. This +results in normal events also being execute in the middle of the interval +when a free event is scheduled. For example, above, when a free event was +absent, a normal event with a 0.001s timeout was delayed for 0.02405s. However, +if a free event happened to be also scheduled, the normal event was only +delayed 0.00014s, which may be undesirable. + +The Free only clock (``free_only``) solves it by only executing free events +during the interval and normal events are always executed like with the +default clock. For example, in the presence of a free event, a normal event +with a timeout of 0.001s still had a delay of 0.02406. So this clock, +treats free and normal events independently, with normal events always being +fps limited, but never the free events. + +Summary +^^^^^^^^ + +The kivy clock type to use can be set with the ``kivy_clock`` option the +:mod:`~kivy.config`. If ``KIVY_CLOCK`` is present in the environment it +overwrites the config selection. Its possible values are as follows: + +* When ``kivy_clock`` is ``default``, the normal clock, :class:`ClockBase`, + which limits callbacks to the maxfps quantization - is used. +* When ``kivy_clock`` is ``interrupt``, a interruptible clock, + :class:`ClockBaseInterrupt`, which doesn't limit any callbacks to the + maxfps - is used. Callbacks will be executed at any time. +* When ``kivy_clock`` is ``free_all``, a interruptible clock, + :class:`ClockBaseFreeInterruptAll`, which doesn't limit any callbacks to the + maxfps in the presence of free events, but in their absence it limits events + to the fps quantization interval - is used. +* When ``kivy_clock`` is ``free_only``, a interruptible clock, + :class:`ClockBaseFreeInterruptAll`, which treats free and normal events + independently; normal events are fps limited while free events are not - is + used. + +Async clock support +------------------- + +.. versionadded:: 2.0.0 + +Experimental async support has been added in 2.0.0. The Clock now has a +:meth:`ClockBaseBehavior.async_tick` and :meth:`ClockBaseBehavior.async_idle` +coroutine method which is used by the kivy EventLoop when the kivy EventLoop is +executed in a asynchronous manner. When used, the kivy clock does not +block while idling. + +The async library to use is selected with the `KIVY_EVENTLOOP` environmental +variable or by calling :meth:`~kivy.clock.ClockBaseBehavior.init_async_lib` +directly. The library can be one of `"asyncio"` when the standard library +`asyncio` should be used, or `"trio"` if the trio library +should be used. If not set it defaults to `"asyncio"`. + +See :mod:`~kivy.app` for example usage. +''' + +__all__ = ( + 'Clock', 'ClockNotRunningError', 'ClockEvent', 'FreeClockEvent', + 'CyClockBase', 'CyClockBaseFree', 'triggered', + 'ClockBaseBehavior', 'ClockBaseInterruptBehavior', + 'ClockBaseInterruptFreeBehavior', 'ClockBase', 'ClockBaseInterrupt', + 'ClockBaseFreeInterruptAll', 'ClockBaseFreeInterruptOnly', 'mainthread') + +from sys import platform +from os import environ +from functools import wraps, partial +from kivy.context import register_context +from kivy.config import Config +from kivy.logger import Logger +from kivy.compat import clock as _default_time +import time +try: + from kivy._clock import CyClockBase, ClockEvent, FreeClockEvent, \ + CyClockBaseFree, ClockNotRunningError +except ImportError: + Logger.error( + 'Clock: Unable to import kivy._clock. Have you perhaps forgotten to ' + 'compile kivy? Kivy contains Cython code which needs to be compiled. ' + 'A missing kivy._clock often indicates the Cython code has not been ' + 'compiled. Please follow the installation instructions and make sure ' + 'to compile Kivy') + raise + +from threading import Event as ThreadingEvent + +# some reading: http://gameprogrammingpatterns.com/game-loop.html + + +def _get_sleep_obj(): + pass + + +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 + + def _get_sleep_obj(): # noqa: F811 + return _kernel32.CreateWaitableTimerA(None, True, None) + + def _usleep(microseconds, obj=None): + delay = ctypes.c_longlong(int(-microseconds * 10)) + _kernel32.SetWaitableTimer( + obj, ctypes.byref(delay), 0, + ctypes.c_void_p(), ctypes.c_void_p(), False) + _kernel32.WaitForSingleObject(obj, 0xffffffff) + else: + if platform == 'darwin': + _libc = ctypes.CDLL('libc.dylib') + else: + from ctypes.util import find_library + _libc = ctypes.CDLL(find_library('c'), use_errno=True) + + def _libc_clock_gettime_wrapper(): + from os import strerror + + class struct_tv(ctypes.Structure): + _fields_ = [('tv_sec', ctypes.c_long), + ('tv_usec', ctypes.c_long)] + + _clock_gettime = _libc.clock_gettime + _clock_gettime.argtypes = [ctypes.c_long, + ctypes.POINTER(struct_tv)] + + if 'linux' in platform: + _clockid = 4 # CLOCK_MONOTONIC_RAW (Linux specific) + elif 'freebsd' in platform: + # clockid constants from sys/time.h + # _clockid = 4 # CLOCK_MONOTONIC (FreeBSD specific) + # 11: CLOCK_MONOTONIC_PRECISE (FreeBSD known OK for 10.2) + _clockid = 11 + # _clockid = 12 + # 12: CLOCK_MONOTONIC_FAST (FreeBSD specific) + Logger.debug('clock.py: {{{:s}}} clock ID {:d}'.format( + platform, _clockid)) + elif 'openbsd' in platform: + _clockid = 3 # CLOCK_MONOTONIC + else: + _clockid = 1 # CLOCK_MONOTONIC + + tv = struct_tv() + + def _time(): + if _clock_gettime(ctypes.c_long(_clockid), + ctypes.pointer(tv)) != 0: + _ernno = ctypes.get_errno() + raise OSError(_ernno, strerror(_ernno)) + return tv.tv_sec + (tv.tv_usec * 0.000000001) + + return _time + + _default_time = _libc_clock_gettime_wrapper() # noqa: F811 + + _libc.usleep.argtypes = [ctypes.c_ulong] + _libc_usleep = _libc.usleep + + def _usleep(microseconds, obj=None): + _libc_usleep(int(microseconds)) + +except (OSError, ImportError, AttributeError): + # ImportError: ctypes is not available on python-for-android. + # AttributeError: ctypes is now available on python-for-android, but + # "undefined symbol: clock_gettime". CF #3797 + # OSError: if the libc cannot be read (like with buildbot: invalid ELF + # header) + + def _usleep(microseconds, obj=None): + time.sleep(microseconds / 1000000.) + + +class ClockBaseBehavior(object): + '''The base of the kivy clock. + + :parameters: + + `async_lib`: string + The async library to use when the clock is run asynchronously. + Can be one of, `"asyncio"` when the standard library asyncio + should be used, or `"trio"` if the trio library should be used. + + It defaults to `'asyncio'` or the value in the environmental + variable `KIVY_EVENTLOOP` if set. :meth:`init_async_lib` can also + be called directly to set the library. + ''' + + _dt = 0.0001 + _last_fps_tick = None + _start_tick = 0 + _fps = 0 + _rfps = 0 + _fps_counter = 0 + _rfps_counter = 0 + _frames = 0 + _frames_displayed = 0 + _events_duration = 0 + '''The measured time that it takes to process all the events etc, excepting + any sleep or waiting time. It is the average and is updated every 5 + seconds. + ''' + + _duration_count = 0 + _sleep_time = 0 + _duration_ts0 = 0 + + MIN_SLEEP = 0.005 + '''The minimum time to sleep. If the remaining time is less than this, + the event loop will continue. + ''' + SLEEP_UNDERSHOOT = MIN_SLEEP - 0.001 + + _async_lib = None + + _async_wait_for = None + + def __init__(self, async_lib='asyncio', **kwargs): + self.init_async_lib(async_lib) + super(ClockBaseBehavior, self).__init__(**kwargs) + self._duration_ts0 = self._start_tick = self._last_tick = self.time() + self._max_fps = float(Config.getint('graphics', 'maxfps')) + + def init_async_lib(self, lib): + """Manually sets the async library to use internally, when running in + a asynchronous manner. + + This can be called anytime before the kivy event loop has started, + but not once the kivy App is running. + + :parameters: + + `lib`: string + The async library to use when the clock is run asynchronously. + Can be one of, `"asyncio"` when the standard library asyncio + should be used, or `"trio"` if the trio library should be used. + """ + if lib == 'trio': + import trio + self._async_lib = trio + + async def wait_for(coro, t): + with trio.move_on_after(t): + await coro + self._async_wait_for = wait_for + elif lib == 'asyncio': + import asyncio + self._async_lib = asyncio + self._async_wait_for = asyncio.wait_for + else: + raise ValueError('async library {} not recognized'.format(lib)) + + @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 necessarily drawn) 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 usleep(self, microseconds): + '''Sleeps for the number of microseconds. + ''' + pass + + def idle(self): + '''(internal) waits here until the next frame. + ''' + fps = self._max_fps + if fps > 0: + min_sleep = self.get_resolution() + undershoot = 4 / 5. * min_sleep + usleep = self.usleep + ready = self._check_ready + + done, sleeptime = ready(fps, min_sleep, undershoot) + while not done: + usleep(1000000 * sleeptime) + done, sleeptime = ready(fps, min_sleep, undershoot) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + return current + + async def async_idle(self): + '''(internal) async version of :meth:`idle`. + ''' + fps = self._max_fps + if fps > 0: + min_sleep = self.get_resolution() + undershoot = 4 / 5. * min_sleep + ready = self._check_ready + + slept = False + done, sleeptime = ready(fps, min_sleep, undershoot) + while not done: + slept = True + await self._async_lib.sleep(sleeptime) + done, sleeptime = ready(fps, min_sleep, undershoot) + + if not slept: + await self._async_lib.sleep(0) + else: + await self._async_lib.sleep(0) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + return current + + def _check_ready(self, fps, min_sleep, undershoot): + sleeptime = 1 / fps - (self.time() - self._last_tick) + return sleeptime - undershoot <= min_sleep, sleeptime - undershoot + + 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.pre_idle() + ts = self.time() + self.post_idle(ts, self.idle()) + + async def async_tick(self): + '''async version of :meth:`tick`. ''' + self.pre_idle() + ts = self.time() + current = await self.async_idle() + self.post_idle(ts, current) + + def pre_idle(self): + '''Called before :meth:`idle` by :meth:`tick`. + ''' + self._release_references() + + def post_idle(self, ts, current): + '''Called after :meth:`idle` by :meth:`tick`. + ''' + # tick the current time + self._frames += 1 + self._fps_counter += 1 + + # compute how long the event processing takes + self._duration_count += 1 + self._sleep_time += current - ts + t_tot = current - self._duration_ts0 + if t_tot >= 1.: + self._events_duration = \ + (t_tot - self._sleep_time) / float(self._duration_count) + self._duration_ts0 = current + self._sleep_time = self._duration_count = 0 + + # 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 + + time = staticmethod(partial(_default_time)) + + def handle_exception(self, e): + from kivy.base import ExceptionManager + + if ExceptionManager.handle_exception(e) == ExceptionManager.RAISE: + raise + + +ClockBaseBehavior.time.__doc__ = \ + '''Proxy method for :func:`~kivy.compat.clock`. ''' + + +class ClockBaseInterruptBehavior(ClockBaseBehavior): + '''A kivy clock which can be interrupted during a frame to execute events. + ''' + + interupt_next_only = False + _event = None + _async_event = None + _get_min_timeout_func = None + + def __init__(self, interupt_next_only=False, **kwargs): + super(ClockBaseInterruptBehavior, self).__init__(**kwargs) + self._event = ThreadingEvent() + self.interupt_next_only = interupt_next_only + self._get_min_timeout_func = self.get_min_timeout + + def init_async_lib(self, lib): + super(ClockBaseInterruptBehavior, self).init_async_lib(lib) + if lib == 'trio': + import trio + self._async_event = trio.Event() + # we don't know if this is called after things have already been + # scheduled, so don't delay for a full frame before processing + # events + self._async_event.set() + elif lib == 'asyncio': + import asyncio + self._async_event = asyncio.Event() + self._async_event.set() + + def usleep(self, microseconds): + self._event.clear() + self._event.wait(microseconds / 1000000.) + + async def async_usleep(self, microseconds): + self._async_event.clear() + await self._async_wait_for( + self._async_event.wait(), microseconds / 1000000.) + + def on_schedule(self, event): + fps = self._max_fps + if not fps: + return + + if not event.timeout or ( + not self.interupt_next_only and event.timeout <= + 1 / fps - # remaining time + (self.time() - self._last_tick) + # elapsed time + 4 / 5. * self.get_resolution()): # resolution fudge factor + self._event.set() + if self._async_event: + self._async_event.set() + + def idle(self): + fps = self._max_fps + event = self._event + resolution = self.get_resolution() + if fps > 0: + done, sleeptime = self._check_ready( + fps, resolution, 4 / 5. * resolution, event) + if not done: + event.wait(sleeptime) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + event.clear() + # anything scheduled from now on, if scheduled for the upcoming frame + # will cause a timeout of the event on the next idle due to on_schedule + # `self._last_tick = current` must happen before clear, otherwise the + # on_schedule computation is wrong when exec between the clear and + # the `self._last_tick = current` bytecode. + return current + + async def async_idle(self): + fps = self._max_fps + event = self._async_event + resolution = self.get_resolution() + if fps > 0: + done, sleeptime = self._check_ready( + fps, resolution, 4 / 5. * resolution, event) + if not done: + await self._async_wait_for(event.wait(), sleeptime) + else: + await self._async_lib.sleep(0) + else: + await self._async_lib.sleep(0) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + event.clear() + # anything scheduled from now on, if scheduled for the upcoming frame + # will cause a timeout of the event on the next idle due to on_schedule + # `self._last_tick = current` must happen before clear, otherwise the + # on_schedule computation is wrong when exec between the clear and + # the `self._last_tick = current` bytecode. + return current + + def _check_ready(self, fps, min_sleep, undershoot, event): + if event.is_set(): + return True, 0 + + t = self._get_min_timeout_func() + if not t: + return True, 0 + + if not self.interupt_next_only: + curr_t = self.time() + sleeptime = min(1 / fps - (curr_t - self._last_tick), t - curr_t) + else: + sleeptime = 1 / fps - (self.time() - self._last_tick) + return sleeptime - undershoot <= min_sleep, sleeptime - undershoot + + +class ClockBaseInterruptFreeBehavior(ClockBaseInterruptBehavior): + '''A base class for the clock that interrupts the sleep interval for + free events. + ''' + + def __init__(self, **kwargs): + super(ClockBaseInterruptFreeBehavior, self).__init__(**kwargs) + self._get_min_timeout_func = self.get_min_free_timeout + + def on_schedule(self, event): + if not event.free: # only wake up for free events + return + # free events should use real time not frame time + event._last_dt = self.time() + return super(ClockBaseInterruptFreeBehavior, + self).on_schedule(event) + + +class ClockBase(ClockBaseBehavior, CyClockBase): + '''The ``default`` kivy clock. See module for details. + ''' + + _sleep_obj = None + + def __init__(self, **kwargs): + super(ClockBase, self).__init__(**kwargs) + self._sleep_obj = _get_sleep_obj() + + def usleep(self, microseconds): + _usleep(microseconds, self._sleep_obj) + + +class ClockBaseInterrupt(ClockBaseInterruptBehavior, CyClockBase): + '''The ``interrupt`` kivy clock. See module for details. + ''' + + pass + + +class ClockBaseFreeInterruptAll( + ClockBaseInterruptFreeBehavior, CyClockBaseFree): + '''The ``free_all`` kivy clock. See module for details. + ''' + + pass + + +class ClockBaseFreeInterruptOnly( + ClockBaseInterruptFreeBehavior, CyClockBaseFree): + '''The ``free_only`` kivy clock. See module for details. + ''' + + def idle(self): + fps = self._max_fps + current = self.time() + event = self._event + if fps > 0: + min_sleep = self.get_resolution() + usleep = self.usleep + undershoot = 4 / 5. * min_sleep + min_t = self.get_min_free_timeout + interupt_next_only = self.interupt_next_only + + sleeptime = 1 / fps - (current - self._last_tick) + while sleeptime - undershoot > min_sleep: + if event.is_set(): + do_free = True + else: + t = min_t() + if not t: + do_free = True + elif interupt_next_only: + do_free = False + else: + sleeptime = min(sleeptime, t - current) + do_free = sleeptime - undershoot <= min_sleep + + if do_free: + event.clear() + self._process_free_events(current) + else: + event.wait(sleeptime - undershoot) + current = self.time() + sleeptime = 1 / fps - (current - self._last_tick) + + self._dt = current - self._last_tick + self._last_tick = current + event.clear() # this needs to stay after _last_tick + return current + + async def async_idle(self): + fps = self._max_fps + current = self.time() + event = self._async_event + if fps > 0: + min_sleep = self.get_resolution() + usleep = self.usleep + undershoot = 4 / 5. * min_sleep + min_t = self.get_min_free_timeout + interupt_next_only = self.interupt_next_only + + sleeptime = 1 / fps - (current - self._last_tick) + slept = False + while sleeptime - undershoot > min_sleep: + if event.is_set(): + do_free = True + else: + t = min_t() + if not t: + do_free = True + elif interupt_next_only: + do_free = False + else: + sleeptime = min(sleeptime, t - current) + do_free = sleeptime - undershoot <= min_sleep + + if do_free: + event.clear() + self._process_free_events(current) + else: + slept = True + await self._async_wait_for( + event.wait(), sleeptime - undershoot) + current = self.time() + sleeptime = 1 / fps - (current - self._last_tick) + + if not slept: + await self._async_lib.sleep(0) + else: + await self._async_lib.sleep(0) + + self._dt = current - self._last_tick + self._last_tick = current + event.clear() # this needs to stay after _last_tick + return current + + +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 succeeded!', + '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 + + +def triggered(timeout=0, interval=False): + '''Decorator that will trigger the call of the function at the specified + timeout, through the method :meth:`CyClockBase.create_trigger`. Subsequent + calls to the decorated function (while the timeout is active) are ignored. + + It can be helpful when an expensive function (i.e. call to a server) can be + triggered by different methods. Setting a proper timeout will delay the + calling and only one of them will be triggered. + + @triggered(timeout, interval=False) + def callback(id): + print('The callback has been called with id=%d' % id) + + >> callback(id=1) + >> callback(id=2) + The callback has been called with id=2 + + The decorated callback can also be unscheduled using: + + >> callback.cancel() + + .. versionadded:: 1.10.1 + ''' + + def wrapper_triggered(func): + + _args = [] + _kwargs = {} + + def cb_function(dt): + func(*tuple(_args), **_kwargs) + + cb_trigger = Clock.create_trigger( + cb_function, + timeout=timeout, + interval=interval) + + @wraps(func) + def trigger_function(*args, **kwargs): + _args[:] = [] + _args.extend(list(args)) + _kwargs.clear() + _kwargs.update(kwargs) + cb_trigger() + + def trigger_cancel(): + cb_trigger.cancel() + + setattr(trigger_function, 'cancel', trigger_cancel) + + return trigger_function + + return wrapper_triggered + + +if 'KIVY_DOC_INCLUDE' in environ: + #: Instance of :class:`ClockBaseBehavior`. + Clock: ClockBase = None +else: + _classes = {'default': ClockBase, 'interrupt': ClockBaseInterrupt, + 'free_all': ClockBaseFreeInterruptAll, + 'free_only': ClockBaseFreeInterruptOnly} + _clk = environ.get('KIVY_CLOCK', Config.get('kivy', 'kivy_clock')) + if _clk not in _classes: + raise Exception( + '{} is not a valid kivy clock. Valid clocks are {}'.format( + _clk, sorted(_classes.keys()))) + + Clock: ClockBase = register_context( + 'Clock', _classes[_clk], + async_lib=environ.get('KIVY_EVENTLOOP', 'asyncio')) + '''The kivy Clock instance. See module documentation for details. + ''' diff --git a/kivy/compat.py b/kivy/compat.py new file mode 100644 index 0000000..bc02c52 --- /dev/null +++ b/kivy/compat.py @@ -0,0 +1,82 @@ +''' +Compatibility module for Python 2.7 and >= 3.4 +============================================== + +This module provides a set of utility types and functions for optimization and +to aid in writing Python 2/3 compatible code. +''' + +__all__ = ('PY2', 'clock', 'string_types', 'queue', 'iterkeys', + 'itervalues', 'iteritems', 'isclose') + +import sys +import time +from math import isinf, fabs +try: + import queue +except ImportError: + import Queue as queue +try: + from math import isclose +except ImportError: + isclose = None + +PY2 = False +'''False, because we don't support Python 2 anymore.''' + +clock = None +'''A clock with the highest available resolution on your current Operating +System.''' + +string_types = str +'''A utility type for detecting string in a Python 2/3 friendly way. For +example: + +.. code-block:: python + + if isinstance(s, string_types): + print("It's a string or unicode type") + else: + print("It's something else.") +''' + +text_type = str + +#: unichr is just chr in py3, since all strings are unicode +unichr = chr + +iterkeys = lambda d: iter(d.keys()) +itervalues = lambda d: iter(d.values()) +iteritems = lambda d: iter(d.items()) + + +clock = time.perf_counter + + +def _isclose(a, b, rel_tol=1e-9, abs_tol=0.0): + '''Measures whether two floats are "close" to each other. Identical to + https://docs.python.org/3.6/library/math.html#math.isclose, for older + versions of python. + ''' + + if a == b: # short-circuit exact equality + return True + + if rel_tol < 0.0 or abs_tol < 0.0: + raise ValueError('error tolerances must be non-negative') + + # use cmath so it will work with complex or float + if isinf(abs(a)) or isinf(abs(b)): + # This includes the case of two infinities of opposite sign, or + # one infinity and one finite number. Two infinities of opposite sign + # would otherwise have an infinite relative tolerance. + return False + diff = fabs(b - a) + + return (((diff <= fabs(rel_tol * b)) or + (diff <= fabs(rel_tol * a))) or + (diff <= abs_tol)) + + +if isclose is None: + isclose = _isclose diff --git a/kivy/config.py b/kivy/config.py new file mode 100644 index 0000000..163301f --- /dev/null +++ b/kivy/config.py @@ -0,0 +1,969 @@ +''' +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. + +Applying configurations +----------------------- + +Configuration options control the initialization of the :class:`~kivy.app.App`. +In order to avoid situations where the config settings do not work or are not +applied before window creation (like setting an initial window size), +:meth:`Config.set ` should be used before +importing any other Kivy modules. Ideally, this means setting them right at +the start of your main.py script. + +Alternatively, you can save these settings permanently using +:meth:`Config.set ` then +:meth:`Config.write `. In this case, you will need to +restart the app for the changes to take effect. Note that this approach will +effect all Kivy apps system wide. + +Please note that no underscores (`_`) are allowed in the section name. + +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() + +For information on configuring your :class:`~kivy.app.App`, please see the +:ref:`Application configuration` section. + +.. 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. + +Changing configuration with environment variables +------------------------------------------------- + +Since 1.11.0, it is now possible to change the configuration using +environment variables. They take precedence on the loaded config.ini. +The format is:: + + KCFG_
_ = + +For example: + + KCFG_GRAPHICS_FULLSCREEN=auto ... + KCFG_KIVY_LOG_LEVEL=warning ... + +Or in your file before any kivy import: + + import os + os.environ["KCFG_KIVY_LOG_LEVEL"] = "warning" + +If you don't want to map any environment variables, you can disable +the behavior:: + + os.environ["KIVY_NO_ENV_CONFIG"] = "1" + + +.. _configuration-tokens: + +Available configuration tokens +------------------------------ + +.. |log_levels| replace:: + 'trace', 'debug', 'info', 'warning', 'error' or 'critical' + +:kivy: + + `default_font`: list + Default fonts used for widgets displaying any text. It defaults to + ['Roboto', 'data/fonts/Roboto-Regular.ttf', + 'data/fonts/Roboto-Italic.ttf', 'data/fonts/Roboto-Bold.ttf', + 'data/fonts/Roboto-BoldItalic.ttf']. + `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. + `kivy_clock`: one of `default`, `interrupt`, `free_all`, `free_only` + The clock type to use with kivy. See :mod:`kivy.clock`. + `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. + + `log_maxfiles`: int + Keep log_maxfiles recent logfiles while purging the log directory. Set + 'log_maxfiles' to -1 to disable logfile purging (eg keep all logfiles). + + .. note:: + You end up with 'log_maxfiles + 1' logfiles because the logger + adds a new one after purging. + + `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 resizing + must also be disabled to hide the resizing border. + `custom_titlebar`: int, one of 0 or 1 + If set to `1`, removes the window border and allows user to set a Widget + as a titlebar + see :meth:`~kivy.core.window.WindowBase.set_custom_titlebar` + for detailed usage + `custom_titlebar_border`: int, defaults to 5 + sets the how many pixles off the border should be used as the + rezising frame + `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. + + .. warning:: + Setting maxfps to 0 will lead to max CPU usage. + + '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 + Set whether or not the cursor is shown on the window. + `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`. + `minimum_width`: int + Minimum width to restrict the window to. (sdl2 only) + `minimum_height`: int + Minimum height to restrict the window to. (sdl2 only) + `min_state_time`: float, defaults to .035 + Minimum time for widgets to display a given visual state. + This attrib is currently used by widgets like + :class:`~kivy.uix.dropdown.DropDown` & + :class:`~kivy.uix.behaviors.buttonbehavior.ButtonBehavior` to + make sure they display their current visual state for the given + time. + `allow_screensaver`: int, one of 0 or 1, defaults to 1 + Allow the device to show a screen saver, or to go to sleep + on mobile devices. Only works for the sdl2 window provider. + `vsync`: `none`, empty value, or integers + Whether vsync is enabled, currently only used with sdl2 window. + Possible values are `none` or empty value -- leaves it unchanged, + ``0`` -- disables vsync, ``1`` or larger -- sets vsync interval, + ``-1`` sets adaptive vsync. It falls back to 1 if setting to ``2+`` + or ``-1`` failed. See ``SDL_GL_SetSwapInterval``. + `verify_gl_main_thread`: int, 1 or 0, defaults to 1 + Whether to check if code that changes any gl instructions is + running outside the main thread and then raise an error. + +: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 :mod:`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. + + .. deprecated:: 1.7.0 + Please use + :class:`~kivy.uix.scrollview.ScrollView.effect_cls` instead. + + `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. + +.. versionchanged:: 2.1.0 + `vsync` has been added to the graphics section. + `verify_gl_main_thread` has been added to the graphics section. + +.. versionchanged:: 1.10.0 + `min_state_time` and `allow_screensaver` have been added + to the `graphics` section. + `kivy_clock` has been added to the kivy section. + `default_font` has beed added to the kivy section. + +.. 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 = 24 + +Config = None +'''The default Kivy configuration object. This is a :class:`ConfigParser` +instance with the :attr:`~kivy.config.ConfigParser.name` set to 'kivy'. + +.. code-block:: python + + Config = ConfigParser(name='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 named `'kivy'` and the ConfigParser instance used by the + :meth:`App.build_settings <~kivy.app.App.build_settings>` method is named + `'app'`. + + :Parameters: + `name`: string + The name of the instance. See :attr:`name`. Defaults to `''`. + + .. versionchanged:: 1.9.0 + Each ConfigParser can now be :attr:`named `. 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='', **kwargs): + PythonConfigParser.__init__(self, **kwargs) + 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 or key has + changed. If you don't specify a section or key, it will call the + callback for all section/key 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) + ret = PythonConfigParser.set(self, section, option, e_value) + self._do_callbacks(section, option, value) + return ret + + def setall(self, section, keyvalues): + '''Sets multiple key-value pairs in a section. keyvalues should be a + dictionary containing the key-value pairs to be set. + ''' + 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 multiple key-value defaults in a section. keyvalues should be + a dictionary containing the new key-value defaults. + ''' + self.adddefaultsection(section) + for key, value in keyvalues.items(): + self.setdefault(section, key, value) + + def setdefault(self, section, option, value): + '''Set the default value for an option in the specified section. + ''' + if self.has_option(section, option): + return + self.set(section, option, value) + + def getdefault(self, section, option, defaultvalue): + '''Get the value of an option in the specified section. 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 the value of an option in the specified section. If not found, + it will return the default value. The value will always be + returned 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. + ''' + assert("_" not in section) + 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, False otherwise. + ''' + 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] + if config is not None: + config = config() + if config is not None: + return config + del ConfigParser._named_configs[name] + 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 changed dynamically 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 and 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') + Config.adddefaultsection('network') + + # 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', '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 repetition + 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.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.setdefault('graphics', 'window_state', 'visible') + + elif version == 13: + Config.setdefault('graphics', 'minimum_width', '0') + Config.setdefault('graphics', 'minimum_height', '0') + + elif version == 14: + Config.setdefault('graphics', 'min_state_time', '.035') + + elif version == 15: + Config.setdefault('kivy', 'kivy_clock', 'default') + + elif version == 16: + Config.setdefault('kivy', 'default_font', [ + 'Roboto', + 'data/fonts/Roboto-Regular.ttf', + 'data/fonts/Roboto-Italic.ttf', + 'data/fonts/Roboto-Bold.ttf', + 'data/fonts/Roboto-BoldItalic.ttf']) + + elif version == 17: + Config.setdefault('graphics', 'allow_screensaver', '1') + + elif version == 18: + Config.setdefault('kivy', 'log_maxfiles', '100') + + elif version == 19: + Config.setdefault('graphics', 'shaped', '0') + Config.setdefault( + 'kivy', 'window_shape', + 'data/images/defaultshape.png' + ) + + elif version == 20: + Config.setdefault('network', 'useragent', 'curl') + + elif version == 21: + Config.setdefault('graphics', 'vsync', '') + + elif version == 22: + Config.setdefault('graphics', 'verify_gl_main_thread', '1') + + elif version == 23: + Config.setdefault('graphics', 'custom_titlebar', '0') + Config.setdefault('graphics', 'custom_titlebar_border', '5') + + 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') + + # Load configuration from env + if environ.get('KIVY_NO_ENV_CONFIG', '0') != '1': + for key, value in environ.items(): + if not key.startswith("KCFG_"): + continue + try: + _, section, name = key.split("_", 2) + except ValueError: + Logger.warning(( + "Config: Environ `{}` invalid format, " + "must be KCFG_section_name").format(key)) + continue + + # extract and check section + section = section.lower() + if not Config.has_section(section): + Logger.warning( + "Config: Environ `{}`: unknown section `{}`".format( + key, section)) + continue + + # extract and check the option name + name = name.lower() + sections_to_check = { + "kivy", "graphics", "widgets", "postproc", "network"} + if (section in sections_to_check and + not Config.has_option(section, name)): + Logger.warning(( + "Config: Environ `{}` unknown `{}` " + "option in `{}` section.").format( + key, name, section)) + # we don't avoid to set an unknown option, because maybe + # an external modules or widgets (in garden?) may want to + # save its own configuration here. + + Config.set(section, name, value) diff --git a/kivy/context.py b/kivy/context.py new file mode 100644 index 0000000..7d6e88e --- /dev/null +++ b/kivy/context.py @@ -0,0 +1,102 @@ +''' +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 popping 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) diff --git a/kivy/core/__init__.py b/kivy/core/__init__.py new file mode 100644 index 0000000..6cfc364 --- /dev/null +++ b/kivy/core/__init__.py @@ -0,0 +1,247 @@ +''' +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 sysconfig +import sys +import traceback +import tempfile +import subprocess +import importlib +import kivy +from kivy.logger import Logger + + +class CoreCriticalException(Exception): + pass + + +def core_select_lib(category, llist, create_instance=False, + base='kivy.core', basemodule=None): + if 'KIVY_DOC' in os.environ: + return + category = category.lower() + basemodule = basemodule or category + 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 = importlib.__import__(name='{2}.{0}.{1}'.format( + basemodule, 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)) + 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. Please enable ' + 'debug logging (e.g. add -d if running from the command line, or ' + 'change the log level in the config) and re-run your app to ' + 'identify potential causes\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 + importlib.__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 + + +def handle_win_lib_import_error(category, provider, mod_name): + if sys.platform != 'win32': + return + + assert mod_name.startswith('kivy.') + kivy_root = os.path.dirname(kivy.__file__) + dirs = mod_name[5:].split('.') + mod_path = os.path.join(kivy_root, *dirs) + + # get the full expected path to the compiled pyd file + # filename is .cp-.pyd + # https://github.com/python/cpython/blob/master/Doc/whatsnew/3.5.rst + if hasattr(sys, 'gettotalrefcount'): # debug + mod_path += '._d' + mod_path += '.cp{}{}-{}.pyd'.format( + sys.version_info.major, sys.version_info.minor, + sysconfig.get_platform().replace('-', '_')) + + # does the compiled pyd exist at all? + if not os.path.exists(mod_path): + Logger.debug( + '{}: Failed trying to import "{}" for provider {}. Compiled file ' + 'does not exist. Have you perhaps forgotten to compile Kivy, or ' + 'did not install all required dependencies?'.format( + category, provider, mod_path)) + return + + # tell user to provide dependency walker + env_var = 'KIVY_{}_DEPENDENCY_WALKER'.format(provider.upper()) + if env_var not in os.environ: + Logger.debug( + '{0}: Failed trying to import the "{1}" provider from "{2}". ' + 'This error is often encountered when a dependency is missing,' + ' or if there are multiple copies of the same dependency dll on ' + 'the Windows PATH and they are incompatible with each other. ' + 'This can occur if you are mixing installations (such as different' + ' python installations, like anaconda python and a system python) ' + 'or if another unrelated program added its directory to the PATH. ' + 'Please examine your PATH and python installation for potential ' + 'issues. To further troubleshoot a "DLL load failed" error, ' + 'please download ' + '"Dependency Walker" (64 or 32 bit version - matching your python ' + 'bitness) from dependencywalker.com and set the environment ' + 'variable {3} to the full path of the downloaded depends.exe file ' + 'and rerun your application to generate an error report'. + format(category, provider, mod_path, env_var)) + return + + depends_bin = os.environ[env_var] + if not os.path.exists(depends_bin): + raise ValueError('"{}" provided in {} does not exist'.format( + depends_bin, env_var)) + + # make file for the resultant log + fd, temp_file = tempfile.mkstemp( + suffix='.dwi', prefix='kivy_depends_{}_log_'.format(provider), + dir=os.path.expanduser('~/')) + os.close(fd) + + Logger.info( + '{}: Running dependency walker "{}" on "{}" to generate ' + 'troubleshooting log. Please wait for it to complete'.format( + category, depends_bin, mod_path)) + Logger.debug( + '{}: Dependency walker command is "{}"'.format( + category, + [depends_bin, '/c', '/od:{}'.format(temp_file), mod_path])) + + try: + subprocess.check_output([ + depends_bin, '/c', '/od:{}'.format(temp_file), mod_path]) + except subprocess.CalledProcessError as exc: + if exc.returncode >= 0x00010000: + Logger.error( + '{}: Dependency walker failed with error code "{}". No ' + 'error report was generated'. + format(category, exc.returncode)) + return + + Logger.info( + '{}: dependency walker generated "{}" containing troubleshooting ' + 'information about provider {} and its failing file "{} ({})". You ' + 'can open the file in dependency walker to view any potential issues ' + 'and troubleshoot it yourself. ' + 'To share the file with the Kivy developers and request support, ' + 'please contact us at our support channels ' + 'https://kivy.org/doc/master/contact.html (not on github, unless ' + 'it\'s truly a bug). Make sure to provide the generated file as well ' + 'as the *complete* Kivy log being printed here. Keep in mind the ' + 'generated dependency walker log file contains paths to dlls on your ' + 'system used by kivy or its dependencies to help troubleshoot them, ' + 'and these paths may include your name in them. Please view the ' + 'log file in dependency walker before sharing to ensure you are not ' + 'sharing sensitive paths'.format( + category, temp_file, provider, mod_name, mod_path)) diff --git a/kivy/core/__pycache__/__init__.cpython-310.pyc b/kivy/core/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..e9e3611 Binary files /dev/null and b/kivy/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/audio/__init__.py b/kivy/core/audio/__init__.py new file mode 100644 index 0000000..9e2f380 --- /dev/null +++ b/kivy/core/audio/__init__.py @@ -0,0 +1,244 @@ +''' +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 +:func:`SoundLoader.load` will be the best sound provider for that particular +file type, so it might return different Sound classes depending the file type. + +Event dispatching and state changes +----------------------------------- + +Audio is often processed in parallel to your code. This means you often need to +enter the Kivy :func:`eventloop ` in order to allow +events and state changes to be dispatched correctly. + +You seldom need to worry about this as Kivy apps typically always +require this event loop for the GUI to remain responsive, but it is good to +keep this in mind when debugging or running in a +`REPL `_ +(Read-eval-print loop). + +.. versionchanged:: 1.10.0 + The pygst and gi providers have been removed. + +.. 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. + +.. 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.resources import resource_find +from kivy.properties import StringProperty, NumericProperty, OptionProperty, \ + AliasProperty, BooleanProperty, BoundedNumericProperty +from kivy.utils import platform +from kivy.setupconfig import USE_SDL2 + +from sys import float_info + + +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. + ''' + + pitch = BoundedNumericProperty(1., min=float_info.epsilon) + '''Pitch of a sound. 2 is an octave higher, .5 one below. This is only + implemented for SDL2 audio provider yet. + + .. versionadded:: 1.10.0 + + :attr:`pitch` 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=True) + ''' + .. deprecated:: 1.3.0 + Use :attr:`state` instead. + ''' + + def _get_filename(self): + return self.source + filename = AliasProperty( + _get_filename, None, bind=('source', ), deprecated=True) + ''' + .. 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). + + .. note:: + Most sound providers cannot seek when the audio is stopped. + Play then seek. + ''' + 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 = [] +if platform == 'android': + audio_libs += [('android', 'audio_android')] +elif platform in ('macosx', 'ios'): + audio_libs += [('avplayer', 'audio_avplayer')] +try: + from kivy.lib.gstplayer import GstPlayer # NOQA + audio_libs += [('gstplayer', 'audio_gstplayer')] +except ImportError: + pass +audio_libs += [('ffpyplayer', 'audio_ffpyplayer')] +if USE_SDL2: + audio_libs += [('sdl2', 'audio_sdl2')] +else: + audio_libs += [('pygame', 'audio_pygame')] + +libs_loaded = core_register_libs('audio', audio_libs) diff --git a/kivy/core/audio/__pycache__/__init__.cpython-310.pyc b/kivy/core/audio/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1933bb1 Binary files /dev/null and b/kivy/core/audio/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/audio/__pycache__/audio_android.cpython-310.pyc b/kivy/core/audio/__pycache__/audio_android.cpython-310.pyc new file mode 100644 index 0000000..a3cd8da Binary files /dev/null and b/kivy/core/audio/__pycache__/audio_android.cpython-310.pyc differ diff --git a/kivy/core/audio/__pycache__/audio_avplayer.cpython-310.pyc b/kivy/core/audio/__pycache__/audio_avplayer.cpython-310.pyc new file mode 100644 index 0000000..817c619 Binary files /dev/null and b/kivy/core/audio/__pycache__/audio_avplayer.cpython-310.pyc differ diff --git a/kivy/core/audio/__pycache__/audio_ffpyplayer.cpython-310.pyc b/kivy/core/audio/__pycache__/audio_ffpyplayer.cpython-310.pyc new file mode 100644 index 0000000..4f250fd Binary files /dev/null and b/kivy/core/audio/__pycache__/audio_ffpyplayer.cpython-310.pyc differ diff --git a/kivy/core/audio/__pycache__/audio_gstplayer.cpython-310.pyc b/kivy/core/audio/__pycache__/audio_gstplayer.cpython-310.pyc new file mode 100644 index 0000000..6c8b096 Binary files /dev/null and b/kivy/core/audio/__pycache__/audio_gstplayer.cpython-310.pyc differ diff --git a/kivy/core/audio/__pycache__/audio_pygame.cpython-310.pyc b/kivy/core/audio/__pycache__/audio_pygame.cpython-310.pyc new file mode 100644 index 0000000..0a9ca15 Binary files /dev/null and b/kivy/core/audio/__pycache__/audio_pygame.cpython-310.pyc differ diff --git a/kivy/core/audio/audio_android.py b/kivy/core/audio/audio_android.py new file mode 100644 index 0000000..1558aeb --- /dev/null +++ b/kivy/core/audio/audio_android.py @@ -0,0 +1,104 @@ +""" +AudioAndroid: Kivy audio implementation for Android using native API +""" + +__all__ = ("SoundAndroidPlayer", ) + +from jnius import autoclass, java_method, PythonJavaClass +from android import api_version +from kivy.core.audio import Sound, SoundLoader + + +MediaPlayer = autoclass("android.media.MediaPlayer") +AudioManager = autoclass("android.media.AudioManager") +if api_version >= 21: + AudioAttributesBuilder = autoclass("android.media.AudioAttributes$Builder") + + +class OnCompletionListener(PythonJavaClass): + __javainterfaces__ = ["android/media/MediaPlayer$OnCompletionListener"] + __javacontext__ = "app" + + def __init__(self, callback, **kwargs): + super(OnCompletionListener, self).__init__(**kwargs) + self.callback = callback + + @java_method("(Landroid/media/MediaPlayer;)V") + def onCompletion(self, mp): + self.callback() + + +class SoundAndroidPlayer(Sound): + @staticmethod + def extensions(): + return ("mp3", "mp4", "aac", "3gp", "flac", "mkv", "wav", "ogg", "m4a", + "gsm", "mid", "xmf", "mxmf", "rtttl", "rtx", "ota", "imy") + + def __init__(self, **kwargs): + self._mediaplayer = None + self._completion_listener = None + super(SoundAndroidPlayer, self).__init__(**kwargs) + + def load(self): + self.unload() + self._mediaplayer = MediaPlayer() + if api_version >= 21: + self._mediaplayer.setAudioAttributes( + AudioAttributesBuilder() + .setLegacyStreamType(AudioManager.STREAM_MUSIC) + .build()) + else: + self._mediaplayer.setAudioStreamType(AudioManager.STREAM_MUSIC) + self._mediaplayer.setDataSource(self.source) + self._completion_listener = OnCompletionListener( + self._completion_callback + ) + self._mediaplayer.setOnCompletionListener(self._completion_listener) + self._mediaplayer.prepare() + + def unload(self): + if self._mediaplayer: + self._mediaplayer.release() + self._mediaplayer = None + + def play(self): + if not self._mediaplayer: + return + self._mediaplayer.start() + super(SoundAndroidPlayer, self).play() + + def stop(self): + if not self._mediaplayer: + return + self._mediaplayer.stop() + self._mediaplayer.prepare() + + def seek(self, position): + if not self._mediaplayer: + return + self._mediaplayer.seekTo(float(position) * 1000) + + def get_pos(self): + if self._mediaplayer: + return self._mediaplayer.getCurrentPosition() / 1000. + return super(SoundAndroidPlayer, self).get_pos() + + def on_volume(self, instance, volume): + if self._mediaplayer: + volume = float(volume) + self._mediaplayer.setVolume(volume, volume) + + def _completion_callback(self): + super(SoundAndroidPlayer, self).stop() + + def _get_length(self): + if self._mediaplayer: + return self._mediaplayer.getDuration() / 1000. + return super(SoundAndroidPlayer, self)._get_length() + + def on_loop(self, instance, loop): + if self._mediaplayer: + self._mediaplayer.setLooping(loop) + + +SoundLoader.register(SoundAndroidPlayer) diff --git a/kivy/core/audio/audio_avplayer.py b/kivy/core/audio/audio_avplayer.py new file mode 100644 index 0000000..af3ff0c --- /dev/null +++ b/kivy/core/audio/audio_avplayer.py @@ -0,0 +1,72 @@ +''' +AudioAvplayer: implementation of Sound using pyobjus / AVFoundation. +Works on iOS / OSX. +''' + +__all__ = ('SoundAvplayer', ) + +from kivy.core.audio import Sound, SoundLoader +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework, INCLUDE + +load_framework(INCLUDE.AVFoundation) +AVAudioPlayer = autoclass("AVAudioPlayer") +NSURL = autoclass("NSURL") +NSString = autoclass("NSString") + + +class SoundAvplayer(Sound): + @staticmethod + def extensions(): + # taken from https://goo.gl/015kvU + return ("aac", "adts", "aif", "aiff", "aifc", "caf", "mp3", "mp4", + "m4a", "snd", "au", "sd2", "wav") + + def __init__(self, **kwargs): + self._avplayer = None + super(SoundAvplayer, self).__init__(**kwargs) + + def load(self): + self.unload() + fn = NSString.alloc().initWithUTF8String_(self.source) + url = NSURL.alloc().initFileURLWithPath_(fn) + self._avplayer = AVAudioPlayer.alloc().initWithContentsOfURL_error_( + url, None) + + def unload(self): + self.stop() + self._avplayer = None + + def play(self): + if not self._avplayer: + return + self._avplayer.play() + super(SoundAvplayer, self).play() + + def stop(self): + if not self._avplayer: + return + self._avplayer.stop() + super(SoundAvplayer, self).stop() + + def seek(self, position): + if not self._avplayer: + return + self._avplayer.playAtTime_(float(position)) + + def get_pos(self): + if self._avplayer: + return self._avplayer.currentTime + return super(SoundAvplayer, self).get_pos() + + def on_volume(self, instance, volume): + if self._avplayer: + self._avplayer.volume = float(volume) + + def _get_length(self): + if self._avplayer: + return self._avplayer.duration + return super(SoundAvplayer, self)._get_length() + + +SoundLoader.register(SoundAvplayer) diff --git a/kivy/core/audio/audio_ffpyplayer.py b/kivy/core/audio/audio_ffpyplayer.py new file mode 100644 index 0000000..6283aae --- /dev/null +++ b/kivy/core/audio/audio_ffpyplayer.py @@ -0,0 +1,185 @@ +''' +FFmpeg based audio player +========================= + +To use, you need to install ffpyplayer 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, 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 + +try: + Logger.info( + 'SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.__version__)) +except: + 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' + + 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._player_callback, + 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.perf_counter() + while (player.get_metadata()['duration'] is None and + not self.quitted and time.perf_counter() - 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() + self.seek(0) + + 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) diff --git a/kivy/core/audio/audio_gstplayer.py b/kivy/core/audio/audio_gstplayer.py new file mode 100644 index 0000000..3fd3a4d --- /dev/null +++ b/kivy/core/audio/audio_gstplayer.py @@ -0,0 +1,101 @@ +''' +Audio Gstplayer +=============== + +.. versionadded:: 1.8.0 + +Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer` +This player is the preferred 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', 'flac', 'mp4') + + 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.source + if not uri: + return + if '://' not in uri: + uri = 'file:' + pathname2url(realpath(uri)) + return uri + + +SoundLoader.register(SoundGstplayer) diff --git a/kivy/core/audio/audio_pygame.py b/kivy/core/audio/audio_pygame.py new file mode 100644 index 0000000..fdf9c61 --- /dev/null +++ b/kivy/core/audio/audio_pygame.py @@ -0,0 +1,127 @@ +''' +AudioPygame: implementation of Sound with Pygame + +.. warning:: + + Pygame has been deprecated and will be removed in the release after Kivy + 1.11.0. +''' + +__all__ = ('SoundPygame', ) + +from kivy.clock import Clock +from kivy.utils import platform, deprecated +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 automatically 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') + _check_play_ev = None + + @staticmethod + def extensions(): + if _platform == 'android': + return ('wav', 'ogg', 'mp3', 'm4a') + return ('wav', 'ogg') + + @deprecated( + msg='Pygame has been deprecated and will be removed after 1.11.0') + 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 + self._check_play_ev = 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 + if self._check_play_ev is not None: + self._check_play_ev.cancel() + self._check_play_ev = None + self._channel = None + super(SoundPygame, self).stop() + + def load(self): + self.unload() + if self.source is None: + return + self._data = mixer.Sound(self.source) + + 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) diff --git a/kivy/core/audio/audio_sdl2.cpython-310-x86_64-linux-gnu.so b/kivy/core/audio/audio_sdl2.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..28097d9 Binary files /dev/null and b/kivy/core/audio/audio_sdl2.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/camera/__init__.py b/kivy/core/camera/__init__.py new file mode 100644 index 0000000..b621b34 --- /dev/null +++ b/kivy/core/camera/__init__.py @@ -0,0 +1,151 @@ +''' +Camera +====== + +Core class for acquiring the camera and converting its input into a +:class:`~kivy.graphics.texture.Texture`. + +.. versionchanged:: 1.10.0 + The pygst and videocapture providers have been removed. + +.. 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. +''' + +__all__ = ('CameraBase', 'Camera') + + +from kivy.utils import platform +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 doesn't 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_texture` + 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 platform in ['macosx', 'ios']: + providers += (('avfoundation', 'camera_avfoundation', + 'CameraAVFoundation'), ) +elif platform == 'android': + providers += (('android', 'camera_android', 'CameraAndroid'), ) +else: + providers += (('picamera', 'camera_picamera', 'CameraPiCamera'), ) + providers += (('gi', 'camera_gi', 'CameraGi'), ) + +providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), ) + + +Camera = core_select_lib('camera', (providers)) diff --git a/kivy/core/camera/__pycache__/__init__.cpython-310.pyc b/kivy/core/camera/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7f82c2e Binary files /dev/null and b/kivy/core/camera/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/camera/__pycache__/camera_android.cpython-310.pyc b/kivy/core/camera/__pycache__/camera_android.cpython-310.pyc new file mode 100644 index 0000000..00b9016 Binary files /dev/null and b/kivy/core/camera/__pycache__/camera_android.cpython-310.pyc differ diff --git a/kivy/core/camera/__pycache__/camera_gi.cpython-310.pyc b/kivy/core/camera/__pycache__/camera_gi.cpython-310.pyc new file mode 100644 index 0000000..5056187 Binary files /dev/null and b/kivy/core/camera/__pycache__/camera_gi.cpython-310.pyc differ diff --git a/kivy/core/camera/__pycache__/camera_opencv.cpython-310.pyc b/kivy/core/camera/__pycache__/camera_opencv.cpython-310.pyc new file mode 100644 index 0000000..d3b989b Binary files /dev/null and b/kivy/core/camera/__pycache__/camera_opencv.cpython-310.pyc differ diff --git a/kivy/core/camera/__pycache__/camera_picamera.cpython-310.pyc b/kivy/core/camera/__pycache__/camera_picamera.cpython-310.pyc new file mode 100644 index 0000000..5f169c3 Binary files /dev/null and b/kivy/core/camera/__pycache__/camera_picamera.cpython-310.pyc differ diff --git a/kivy/core/camera/camera_android.py b/kivy/core/camera/camera_android.py new file mode 100644 index 0000000..66f5b8d --- /dev/null +++ b/kivy/core/camera/camera_android.py @@ -0,0 +1,206 @@ +from jnius import autoclass, PythonJavaClass, java_method +from kivy.clock import Clock +from kivy.graphics.texture import Texture +from kivy.graphics import Fbo, Callback, Rectangle +from kivy.core.camera import CameraBase +import threading + + +Camera = autoclass('android.hardware.Camera') +SurfaceTexture = autoclass('android.graphics.SurfaceTexture') +GL_TEXTURE_EXTERNAL_OES = autoclass( + 'android.opengl.GLES11Ext').GL_TEXTURE_EXTERNAL_OES +ImageFormat = autoclass('android.graphics.ImageFormat') + + +class PreviewCallback(PythonJavaClass): + """ + Interface used to get back the preview frame of the Android Camera + """ + __javainterfaces__ = ('android.hardware.Camera$PreviewCallback', ) + + def __init__(self, callback): + super(PreviewCallback, self).__init__() + self._callback = callback + + @java_method('([BLandroid/hardware/Camera;)V') + def onPreviewFrame(self, data, camera): + self._callback(data, camera) + + +class CameraAndroid(CameraBase): + """ + Implementation of CameraBase using Android API + """ + + _update_ev = None + + def __init__(self, **kwargs): + self._android_camera = None + self._preview_cb = PreviewCallback(self._on_preview_frame) + self._buflock = threading.Lock() + super(CameraAndroid, self).__init__(**kwargs) + + def __del__(self): + self._release_camera() + + def init_camera(self): + self._release_camera() + self._android_camera = Camera.open(self._index) + params = self._android_camera.getParameters() + width, height = self._resolution + params.setPreviewSize(width, height) + supported_focus_modes = self._android_camera.getParameters() \ + .getSupportedFocusModes() + if supported_focus_modes.contains('continuous-picture'): + params.setFocusMode('continuous-picture') + self._android_camera.setParameters(params) + # self._android_camera.setDisplayOrientation() + self.fps = 30. + + pf = params.getPreviewFormat() + assert(pf == ImageFormat.NV21) # default format is NV21 + self._bufsize = int(ImageFormat.getBitsPerPixel(pf) / 8. * + width * height) + + self._camera_texture = Texture(width=width, height=height, + target=GL_TEXTURE_EXTERNAL_OES, + colorfmt='rgba') + self._surface_texture = SurfaceTexture(int(self._camera_texture.id)) + self._android_camera.setPreviewTexture(self._surface_texture) + + self._fbo = Fbo(size=self._resolution) + self._fbo['resolution'] = (float(width), float(height)) + self._fbo.shader.fs = ''' + #extension GL_OES_EGL_image_external : require + #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; + uniform samplerExternalOES texture1; + uniform vec2 resolution; + + void main() + { + vec2 coord = vec2(tex_coord0.y * ( + resolution.y / resolution.x), 1. -tex_coord0.x); + gl_FragColor = texture2D(texture1, tex_coord0); + } + ''' + with self._fbo: + self._texture_cb = Callback(lambda instr: + self._camera_texture.bind) + Rectangle(size=self._resolution) + + def _release_camera(self): + if self._android_camera is None: + return + + self.stop() + self._android_camera.release() + self._android_camera = None + + # clear texture and it'll be reset in `_update` pointing to new FBO + self._texture = None + del self._fbo, self._surface_texture, self._camera_texture + + def _on_preview_frame(self, data, camera): + with self._buflock: + if self._buffer is not None: + # add buffer back for reuse + self._android_camera.addCallbackBuffer(self._buffer) + self._buffer = data + # check if frame grabbing works + # print self._buffer, len(self.frame_data) + + def _refresh_fbo(self): + self._texture_cb.ask_update() + self._fbo.draw() + + def start(self): + super(CameraAndroid, self).start() + + with self._buflock: + self._buffer = None + for k in range(2): # double buffer + buf = b'\x00' * self._bufsize + self._android_camera.addCallbackBuffer(buf) + self._android_camera.setPreviewCallbackWithBuffer(self._preview_cb) + + self._android_camera.startPreview() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = Clock.schedule_interval(self._update, 1 / self.fps) + + def stop(self): + super(CameraAndroid, self).stop() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = None + self._android_camera.stopPreview() + + self._android_camera.setPreviewCallbackWithBuffer(None) + # buffer queue cleared as well, to be recreated on next start + with self._buflock: + self._buffer = None + + def _update(self, dt): + self._surface_texture.updateTexImage() + self._refresh_fbo() + if self._texture is None: + self._texture = self._fbo.texture + self.dispatch('on_load') + self._copy_to_gpu() + + def _copy_to_gpu(self): + """ + A dummy placeholder (the image is already in GPU) to be consistent + with other providers. + """ + self.dispatch('on_texture') + + def grab_frame(self): + """ + Grab current frame (thread-safe, minimal overhead) + """ + with self._buflock: + if self._buffer is None: + return None + buf = self._buffer.tostring() + return buf + + def decode_frame(self, buf): + """ + Decode image data from grabbed frame. + + This method depends on OpenCV and NumPy - however it is only used for + fetching the current frame as a NumPy array, and not required when + this :class:`CameraAndroid` provider is simply used by a + :class:`~kivy.uix.camera.Camera` widget. + """ + import numpy as np + from cv2 import cvtColor + + w, h = self._resolution + arr = np.fromstring(buf, 'uint8').reshape((h + h / 2, w)) + arr = cvtColor(arr, 93) # NV21 -> BGR + return arr + + def read_frame(self): + """ + Grab and decode frame in one call + """ + return self.decode_frame(self.grab_frame()) + + @staticmethod + def get_camera_count(): + """ + Get the number of available cameras. + """ + return Camera.getNumberOfCameras() diff --git a/kivy/core/camera/camera_gi.py b/kivy/core/camera/camera_gi.py new file mode 100644 index 0000000..f9f4fe6 --- /dev/null +++ b/kivy/core/camera/camera_gi.py @@ -0,0 +1,170 @@ +''' +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() diff --git a/kivy/core/camera/camera_opencv.py b/kivy/core/camera/camera_opencv.py new file mode 100644 index 0000000..f24a520 --- /dev/null +++ b/kivy/core/camera/camera_opencv.py @@ -0,0 +1,163 @@ +''' +OpenCV Camera: Implement CameraBase with OpenCV +''' + +# +# TODO: make usage of thread or multiprocess +# + +from __future__ import division + +__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: + # opencv 1 case + import opencv as cv + + try: + import opencv.highgui as hg + except ImportError: + 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() + +except ImportError: + # opencv 2 case (and also opencv 3, because it still uses cv2 module name) + try: + import cv2 + # here missing this OSX specific highgui thing. + # I'm not on OSX so don't know if it is still valid in opencv >= 2 + except ImportError: + raise + + +class CameraOpenCV(CameraBase): + ''' + Implementation of CameraBase using OpenCV + ''' + _update_ev = None + + def __init__(self, **kwargs): + # we will need it, because constants have + # different access paths between ver. 2 and 3 + try: + self.opencvMajorVersion = int(cv.__version__[0]) + except NameError: + self.opencvMajorVersion = int(cv2.__version__[0]) + + self._device = None + super(CameraOpenCV, self).__init__(**kwargs) + + def init_camera(self): + # consts have changed locations between versions 2 and 3 + if self.opencvMajorVersion in (3, 4): + PROPERTY_WIDTH = cv2.CAP_PROP_FRAME_WIDTH + PROPERTY_HEIGHT = cv2.CAP_PROP_FRAME_HEIGHT + PROPERTY_FPS = cv2.CAP_PROP_FPS + elif self.opencvMajorVersion == 2: + PROPERTY_WIDTH = cv2.cv.CV_CAP_PROP_FRAME_WIDTH + PROPERTY_HEIGHT = cv2.cv.CV_CAP_PROP_FRAME_HEIGHT + PROPERTY_FPS = cv2.cv.CV_CAP_PROP_FPS + elif self.opencvMajorVersion == 1: + PROPERTY_WIDTH = cv.CV_CAP_PROP_FRAME_WIDTH + PROPERTY_HEIGHT = cv.CV_CAP_PROP_FRAME_HEIGHT + PROPERTY_FPS = cv.CV_CAP_PROP_FPS + + Logger.debug('Using opencv ver.' + str(self.opencvMajorVersion)) + + if self.opencvMajorVersion == 1: + # 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) + + elif self.opencvMajorVersion in (2, 3, 4): + # create the device + self._device = cv2.VideoCapture(self._index) + # Set preferred resolution + self._device.set(PROPERTY_WIDTH, + self.resolution[0]) + self._device.set(PROPERTY_HEIGHT, + self.resolution[1]) + # and get frame to check if it's ok + ret, frame = self._device.read() + + # source: + # http://stackoverflow.com/questions/32468371/video-capture-propid-parameters-in-opencv # noqa + self._resolution = (int(frame.shape[1]), int(frame.shape[0])) + # get fps + self.fps = self._device.get(PROPERTY_FPS) + + if self.fps == 0 or self.fps == 1: + self.fps = 1.0 / 30 + elif self.fps > 1: + self.fps = 1.0 / self.fps + + 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: + ret, frame = self._device.read() + self._format = 'bgr' + try: + self._buffer = frame.imageData + except AttributeError: + # frame is already of type ndarray + # which can be reshaped to 1-d. + self._buffer = frame.reshape(-1) + self._copy_to_gpu() + except: + Logger.exception('OpenCV: Couldn\'t get image from Camera') + + def start(self): + super(CameraOpenCV, self).start() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = Clock.schedule_interval(self._update, self.fps) + + def stop(self): + super(CameraOpenCV, self).stop() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = None diff --git a/kivy/core/camera/camera_picamera.py b/kivy/core/camera/camera_picamera.py new file mode 100644 index 0000000..98c5e95 --- /dev/null +++ b/kivy/core/camera/camera_picamera.py @@ -0,0 +1,96 @@ +''' +PiCamera Camera: Implement CameraBase with PiCamera +''' + +# +# TODO: make usage of thread or multiprocess +# + +__all__ = ('CameraPiCamera', ) + +from math import ceil + +from kivy.logger import Logger +from kivy.clock import Clock +from kivy.graphics.texture import Texture +from kivy.core.camera import CameraBase + +from picamera import PiCamera +import numpy + + +class CameraPiCamera(CameraBase): + '''Implementation of CameraBase using PiCamera + ''' + _update_ev = None + + def __init__(self, **kwargs): + self._camera = None + self._format = 'bgr' + self._framerate = kwargs.get('framerate', 30) + super(CameraPiCamera, self).__init__(**kwargs) + + def init_camera(self): + if self._camera is not None: + self._camera.close() + + self._camera = PiCamera() + self._camera.resolution = self.resolution + self._camera.framerate = self._framerate + self._camera.iso = 800 + + self.fps = 1. / self._framerate + + if not self.stopped: + self.start() + + def raw_buffer_size(self): + '''Round buffer size up to 32x16 blocks. + + See https://picamera.readthedocs.io/en/release-1.13/recipes2.html#capturing-to-a-numpy-array + ''' # noqa + return ( + ceil(self.resolution[0] / 32.) * 32, + ceil(self.resolution[1] / 16.) * 16 + ) + + 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: + bufsize = self.raw_buffer_size() + output = numpy.empty( + (bufsize[0] * bufsize[1] * 3,), dtype=numpy.uint8) + self._camera.capture(output, self._format, use_video_port=True) + + # Trim the buffer to fit the actual requested resolution. + # TODO: Is there a simpler way to do all this reshuffling? + output = output.reshape((bufsize[0], bufsize[1], 3)) + output = output[:self.resolution[0], :self.resolution[1], :] + self._buffer = output.reshape( + (self.resolution[0] * self.resolution[1] * 3,)) + + self._copy_to_gpu() + except KeyboardInterrupt: + raise + except Exception: + Logger.exception('PiCamera: Couldn\'t get image from Camera') + + def start(self): + super(CameraPiCamera, self).start() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = Clock.schedule_interval(self._update, self.fps) + + def stop(self): + super(CameraPiCamera, self).stop() + if self._update_ev is not None: + self._update_ev.cancel() + self._update_ev = None diff --git a/kivy/core/clipboard/__init__.py b/kivy/core/clipboard/__init__.py new file mode 100644 index 0000000..2fe4a9e --- /dev/null +++ b/kivy/core/clipboard/__init__.py @@ -0,0 +1,157 @@ +''' +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: + +.. code-block:: kv + + #:import Clipboard kivy.core.clipboard.Clipboard + + Button: + on_release: + self.text = Clipboard.paste() + Clipboard.copy('Data') +''' + +__all__ = ('ClipboardBase', 'Clipboard') + +from kivy import Logger +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. + ''' + pass + + 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): + self._ensure_clipboard() + if not isinstance(data, bytes): + data = data.encode(self._encoding) + 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( + ('xclip', 'clipboard_xclip', 'ClipboardXclip')) + _clipboards.append( + ('xsel', 'clipboard_xsel', 'ClipboardXsel')) + _clipboards.append( + ('dbusklipper', 'clipboard_dbusklipper', 'ClipboardDbusKlipper')) + _clipboards.append( + ('gtk3', 'clipboard_gtk3', 'ClipboardGtk3')) + +if USE_SDL2: + _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) +CutBuffer = None + +if platform == 'linux': + _cutbuffers = [ + ('xclip', 'clipboard_xclip', 'ClipboardXclip'), + ('xsel', 'clipboard_xsel', 'ClipboardXsel'), + ] + + if Clipboard.__class__.__name__ in (c[2] for c in _cutbuffers): + CutBuffer = Clipboard + else: + CutBuffer = core_select_lib('cutbuffer', _cutbuffers, True, + basemodule='clipboard') + + if CutBuffer: + Logger.info('CutBuffer: cut buffer support enabled') diff --git a/kivy/core/clipboard/__pycache__/__init__.cpython-310.pyc b/kivy/core/clipboard/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..def6fec Binary files /dev/null and b/kivy/core/clipboard/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/_clipboard_ext.cpython-310.pyc b/kivy/core/clipboard/__pycache__/_clipboard_ext.cpython-310.pyc new file mode 100644 index 0000000..4da6e6c Binary files /dev/null and b/kivy/core/clipboard/__pycache__/_clipboard_ext.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_android.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_android.cpython-310.pyc new file mode 100644 index 0000000..98ac9f3 Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_android.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_dbusklipper.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_dbusklipper.cpython-310.pyc new file mode 100644 index 0000000..84502e3 Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_dbusklipper.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_dummy.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_dummy.cpython-310.pyc new file mode 100644 index 0000000..555abad Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_dummy.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_gtk3.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_gtk3.cpython-310.pyc new file mode 100644 index 0000000..a9472f4 Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_gtk3.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_nspaste.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_nspaste.cpython-310.pyc new file mode 100644 index 0000000..c66f718 Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_nspaste.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_pygame.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_pygame.cpython-310.pyc new file mode 100644 index 0000000..a56949c Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_pygame.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_sdl2.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_sdl2.cpython-310.pyc new file mode 100644 index 0000000..bc381fb Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_sdl2.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_winctypes.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_winctypes.cpython-310.pyc new file mode 100644 index 0000000..f0bebfd Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_winctypes.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_xclip.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_xclip.cpython-310.pyc new file mode 100644 index 0000000..a25a453 Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_xclip.cpython-310.pyc differ diff --git a/kivy/core/clipboard/__pycache__/clipboard_xsel.cpython-310.pyc b/kivy/core/clipboard/__pycache__/clipboard_xsel.cpython-310.pyc new file mode 100644 index 0000000..f6786db Binary files /dev/null and b/kivy/core/clipboard/__pycache__/clipboard_xsel.cpython-310.pyc differ diff --git a/kivy/core/clipboard/_clipboard_ext.py b/kivy/core/clipboard/_clipboard_ext.py new file mode 100644 index 0000000..a27991d --- /dev/null +++ b/kivy/core/clipboard/_clipboard_ext.py @@ -0,0 +1,36 @@ +''' +Clipboard ext: base class for external command clipboards +''' + +__all__ = ('ClipboardExternalBase', ) + +from kivy.core.clipboard import ClipboardBase + + +class ClipboardExternalBase(ClipboardBase): + @staticmethod + def _clip(inout, selection): + raise NotImplementedError('clip method not implemented') + + def get(self, mimetype='text/plain'): + p = self._clip('out', 'clipboard') + data, _ = p.communicate() + return data + + def put(self, data, mimetype='text/plain'): + p = self._clip('in', 'clipboard') + p.communicate(data) + + def get_cutbuffer(self): + p = self._clip('out', 'primary') + data, _ = p.communicate() + return data.decode('utf8') + + def set_cutbuffer(self, data): + if not isinstance(data, bytes): + data = data.encode('utf8') + p = self._clip('in', 'primary') + p.communicate(data) + + def get_types(self): + return [u'text/plain'] diff --git a/kivy/core/clipboard/_clipboard_sdl2.cpython-310-x86_64-linux-gnu.so b/kivy/core/clipboard/_clipboard_sdl2.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..7e82242 Binary files /dev/null and b/kivy/core/clipboard/_clipboard_sdl2.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/clipboard/clipboard_android.py b/kivy/core/clipboard/clipboard_android.py new file mode 100644 index 0000000..426a4ca --- /dev/null +++ b/kivy/core/clipboard/clipboard_android.py @@ -0,0 +1,91 @@ +''' +Clipboard Android +================= + +Android implementation of Clipboard provider, using Pyjnius. +''' + +__all__ = ('ClipboardAndroid', ) + +from kivy import Logger +from kivy.core.clipboard import ClipboardBase +from jnius import autoclass, cast +from android.runnable import run_on_ui_thread +from android import python_act + +AndroidString = autoclass('java.lang.String') +PythonActivity = python_act +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).encode('utf-8') + + 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 = cast( + 'android.app.Activity', + PythonActivity.mActivity).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 + data = '' + if sdk < 11: + data = clippy.getText() + else: + ClipDescription = autoclass('android.content.ClipDescription') + primary_clip = clippy.getPrimaryClip() + if primary_clip: + try: + data = primary_clip.getItemAt(0) + if data: + data = data.coerceToText( + PythonActivity.mActivity.getApplicationContext()) + except Exception: + Logger.exception('Clipboard: failed to paste') + 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) diff --git a/kivy/core/clipboard/clipboard_dbusklipper.py b/kivy/core/clipboard/clipboard_dbusklipper.py new file mode 100644 index 0000000..84c5099 --- /dev/null +++ b/kivy/core/clipboard/clipboard_dbusklipper.py @@ -0,0 +1,41 @@ +''' +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'] diff --git a/kivy/core/clipboard/clipboard_dummy.py b/kivy/core/clipboard/clipboard_dummy.py new file mode 100644 index 0000000..b05be30 --- /dev/null +++ b/kivy/core/clipboard/clipboard_dummy.py @@ -0,0 +1,26 @@ +''' +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()) diff --git a/kivy/core/clipboard/clipboard_gtk3.py b/kivy/core/clipboard/clipboard_gtk3.py new file mode 100644 index 0000000..23f69aa --- /dev/null +++ b/kivy/core/clipboard/clipboard_gtk3.py @@ -0,0 +1,47 @@ +''' +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') + +import gi +gi.require_version('Gtk', '3.0') +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) + clipboard.store() + + def get_types(self): + self.init() + return ['text/plain;charset=utf-8'] diff --git a/kivy/core/clipboard/clipboard_nspaste.py b/kivy/core/clipboard/clipboard_nspaste.py new file mode 100644 index 0000000..66ef4c6 --- /dev/null +++ b/kivy/core/clipboard/clipboard_nspaste.py @@ -0,0 +1,44 @@ +''' +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() + utf8 = NSString.alloc().initWithUTF8String_(data) + pb.setString_forType_(utf8, 'public.utf8-plain-text') + + def get_types(self): + return list('text/plain',) diff --git a/kivy/core/clipboard/clipboard_pygame.py b/kivy/core/clipboard/clipboard_pygame.py new file mode 100644 index 0000000..8f1671d --- /dev/null +++ b/kivy/core/clipboard/clipboard_pygame.py @@ -0,0 +1,67 @@ +''' +Clipboard Pygame: an implementation of the Clipboard using pygame.scrap. + +.. warning:: + + Pygame has been deprecated and will be removed in the release after Kivy + 1.11.0. +''' + +__all__ = ('ClipboardPygame', ) + +from kivy.utils import platform +from kivy.core.clipboard import ClipboardBase +from kivy.utils import deprecated + +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' + } + + @deprecated( + msg='Pygame has been deprecated and will be removed after 1.11.0') + def __init__(self, *largs, **kwargs): + super(ClipboardPygame, self).__init__(*largs, **kwargs) + + 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 list(self._aliases.items())[:]: + if mime in types: + del self._aliases[mime] + if pygtype in types: + types.append(mime) + self._types = types + return self._types diff --git a/kivy/core/clipboard/clipboard_sdl2.py b/kivy/core/clipboard/clipboard_sdl2.py new file mode 100644 index 0000000..52e3f6d --- /dev/null +++ b/kivy/core/clipboard/clipboard_sdl2.py @@ -0,0 +1,36 @@ +''' +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: + from kivy.core import handle_win_lib_import_error + handle_win_lib_import_error( + 'Clipboard', 'sdl2', 'kivy.core.clipboard._clipboard_sdl2') + raise + + +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'] diff --git a/kivy/core/clipboard/clipboard_winctypes.py b/kivy/core/clipboard/clipboard_winctypes.py new file mode 100644 index 0000000..59b2a92 --- /dev/null +++ b/kivy/core/clipboard/clipboard_winctypes.py @@ -0,0 +1,66 @@ +''' +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 +from ctypes import wintypes +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'): + GetClipboardData = user32.GetClipboardData + GetClipboardData.argtypes = [wintypes.UINT] + GetClipboardData.restype = wintypes.HANDLE + + user32.OpenClipboard(user32.GetActiveWindow()) + # Standard Clipboard Format "1" is "CF_TEXT" + pcontents = GetClipboardData(13) + + # if someone pastes a FILE, the content is None for SCF 13 + # and the clipboard is locked if not closed properly + if not pcontents: + user32.CloseClipboard() + return '' + data = c_wchar_p(pcontents).value.encode(self._encoding) + user32.CloseClipboard() + return data + + def put(self, text, mimetype='text/plain'): + text = text.decode(self._encoding) # auto converted later + text += u'\x00' + + SetClipboardData = user32.SetClipboardData + SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE] + SetClipboardData.restype = wintypes.HANDLE + + GlobalAlloc = kernel32.GlobalAlloc + GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t] + GlobalAlloc.restype = wintypes.HGLOBAL + + CF_UNICODETEXT = 13 + + user32.OpenClipboard(user32.GetActiveWindow()) + user32.EmptyClipboard() + hCd = GlobalAlloc(0, len(text) * ctypes.sizeof(ctypes.c_wchar)) + + # ignore null character for strSource pointer + msvcrt.wcscpy_s(c_wchar_p(hCd), len(text), c_wchar_p(text[:-1])) + SetClipboardData(CF_UNICODETEXT, hCd) + user32.CloseClipboard() + + def get_types(self): + return ['text/plain'] diff --git a/kivy/core/clipboard/clipboard_xclip.py b/kivy/core/clipboard/clipboard_xclip.py new file mode 100644 index 0000000..29279dd --- /dev/null +++ b/kivy/core/clipboard/clipboard_xclip.py @@ -0,0 +1,29 @@ +''' +Clipboard xclip: an implementation of the Clipboard using xclip +command line tool. +''' + +__all__ = ('ClipboardXclip', ) + +from kivy.utils import platform +from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase + +if platform != 'linux': + raise SystemError('unsupported platform for xclip clipboard') + +try: + import subprocess + + p = subprocess.Popen(['xclip', '-version'], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + p.communicate() +except: + raise + + +class ClipboardXclip(ClipboardExternalBase): + @staticmethod + def _clip(inout, selection): + pipe = {'std' + inout: subprocess.PIPE} + return subprocess.Popen( + ['xclip', '-' + inout, '-selection', selection], **pipe) diff --git a/kivy/core/clipboard/clipboard_xsel.py b/kivy/core/clipboard/clipboard_xsel.py new file mode 100644 index 0000000..9b0fee2 --- /dev/null +++ b/kivy/core/clipboard/clipboard_xsel.py @@ -0,0 +1,29 @@ +''' +Clipboard xsel: an implementation of the Clipboard using xsel command line + tool. +''' + +__all__ = ('ClipboardXsel', ) + +from kivy.utils import platform +from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase + +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(ClipboardExternalBase): + @staticmethod + def _clip(inout, selection): + pipe = {'std' + inout: subprocess.PIPE} + sel = 'b' if selection == 'clipboard' else selection[0] + io = inout[0] + return subprocess.Popen( + ['xsel', '-' + sel + io], **pipe) diff --git a/kivy/core/gl/__init__.py b/kivy/core/gl/__init__.py new file mode 100644 index 0000000..5a647d6 --- /dev/null +++ b/kivy/core/gl/__init__.py @@ -0,0 +1,83 @@ +# 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. +''' + +import sys +from os import environ + +MIN_REQUIRED_GL_VERSION = (2, 0) + + +def msgbox(message): + if sys.platform == 'win32': + import ctypes + from ctypes.wintypes import LPCWSTR + ctypes.windll.user32.MessageBoxW(None, LPCWSTR(message), + u"Kivy Fatal Error", 0) + sys.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.graphics.cgl import cgl_get_initialized_backend_name + from kivy.utils import platform + + def init_gl(allowed=[], ignored=[]): + gl_init_symbols(allowed, ignored) + print_gl_version() + gl_init_resources() + + def print_gl_version(): + backend = cgl_get_initialized_backend_name() + Logger.info('GL: Backend used <{}>'.format(backend)) + 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 and backend != "mock"): + if hasattr(sys, "_kivy_opengl_required_func"): + sys._kivy_opengl_required_func(major, minor, version, vendor, + renderer) + else: + 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 + # Automatically import window auto to ensure the default window creation + import kivy.core.window # NOQA diff --git a/kivy/core/gl/__pycache__/__init__.cpython-310.pyc b/kivy/core/gl/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..68186ff Binary files /dev/null and b/kivy/core/gl/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/image/__init__.py b/kivy/core/image/__init__.py new file mode 100644 index 0000000..d01cde6 --- /dev/null +++ b/kivy/core/image/__init__.py @@ -0,0 +1,1002 @@ +''' +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. + +.. versionchanged:: 1.11.0 + + Add support for argb and abgr image data + +In-memory image loading +----------------------- + +.. versionadded:: 1.9.0 + + Official support for in-memory loading. Not all the providers support it, + but currently SDL2, pygame, pil and imageio work. + +To load an image with a filename, you would usually do:: + + from kivy.core.image import Image as CoreImage + im = CoreImage("image.png") + +You can also load the image data directly from a memory block. Instead of +passing the filename, you'll need to pass the data as a BytesIO object +together with an "ext" parameter. 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 requires a +filename. If you want caching, add a filename that represents 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") + +Saving an image +--------------- + +A CoreImage can be saved to a file:: + + from kivy.core.image import Image as CoreImage + image = CoreImage(...) + image.save("/tmp/test.png") + +Or you can get the bytes (new in `1.11.0`): + + import io + from kivy.core.image import Image as CoreImage + data = io.BytesIO() + image = CoreImage(...) + image.save(data, fmt="png") + png_bytes = data.read() + +''' +import re +from base64 import b64decode +import imghdr + +__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', 'bgr', 'rgba', 'bgra', 'argb', 'abgr', + '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(fmt, is_bytesio=False): + '''Indicate if the loader can save the Image object + + .. versionchanged:: 1.11.0 + Parameter `fmt` and `is_bytesio` added + ''' + return False + + @staticmethod + def can_load_memory(): + '''Indicate if the loader can load an image by passing data + ''' + return False + + @staticmethod + def save(*largs, **kwargs): + 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 memory 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 its + # 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 find %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 + # Get actual image format instead of extension if possible + ext = imghdr.what(filename) or ext + 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, BytesIO or Image object + A string path to the image file or data URI to be loaded; or a + Texture object, which will be wrapped in an Image object; or a + BytesIO object containing raw image data; or an already existing + image object, in which 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. + `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. + `ext`: str, only with BytesIO `arg` + File extension to use in determining how to load raw image data. + `filename`: str, only with BytesIO `arg` + Filename to use in the image cache for raw image data. + ''' + + copy_attributes = ('_size', '_filename', '_texture', '_image', + '_mipmap', '_nocache') + + data_uri_re = re.compile(r'^data:image/([^;,]*)(;[^,]*)?,(.*)$') + + _anim_ev = None + + 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): + groups = self.data_uri_re.findall(arg) + if groups: + self._nocache = True + imtype, optstr, data = groups[0] + options = [o for o in optstr.split(';') if o] + ext = imtype + isb64 = 'base64' in options + if data: + if isb64: + data = b64decode(data) + self.load_memory(BytesIO(data), ext) + else: + 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 + if self._anim_ev is not None: + self._anim_ev.cancel() + self._anim_ev = None + + if allow_anim and self._anim_available and self._anim_delay >= 0: + self._anim_ev = 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: + if self._anim_ev is not None: + self._anim_ev.cancel() + self._anim_ev = None + + if self._anim_delay >= 0: + self._anim_ev = 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 + 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, fmt=None): + '''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 heavily 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 False. + + 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. + + .. versionchanged:: 1.11.0 + Parameter `fmt` added to force the output format of the file + Filename can now be a BytesIO object. + + ''' + is_bytesio = False + if isinstance(filename, BytesIO): + is_bytesio = True + if not fmt: + raise Exception( + "You must specify a format to save into a BytesIO object") + elif fmt is None: + fmt = self._find_format_from_filename(filename) + + pixels = None + size = None + loaders = [ + x for x in ImageLoader.loaders + if x.can_save(fmt, is_bytesio=is_bytesio) + ] + 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 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: + pixelfmt = 'rgb' + elif l_pixels == size[0] * size[1] * 4: + pixelfmt = 'rgba' + else: + raise Exception('Unable to determine the format of the pixels') + return loader.save( + filename, size[0], size[1], pixelfmt, pixels, flipped, fmt) + + def _find_format_from_filename(self, filename): + ext = filename.rsplit(".", 1)[-1].lower() + if ext in { + 'bmp', 'jpe', 'lbm', 'pcx', 'png', 'pnm', + 'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv'}: + return ext + elif ext in ('jpg', 'jpeg'): + return 'jpg' + elif ext in ('b64', 'base64'): + return 'base64' + return None + + 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 function 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] + + bgr_flag = False + if data.fmt == 'argb': + color.reverse() # bgra + bgr_flag = True + elif data.fmt == 'abgr': + color.reverse() # rgba + + # conversion for BGR->RGB, BGRA->RGBA format + if bgr_flag or 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')] + +libs_loaded = core_register_libs('image', image_libs) + +from os import environ +if 'KIVY_DOC' not 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 diff --git a/kivy/core/image/__pycache__/__init__.cpython-310.pyc b/kivy/core/image/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..001c2be Binary files /dev/null and b/kivy/core/image/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_dds.cpython-310.pyc b/kivy/core/image/__pycache__/img_dds.cpython-310.pyc new file mode 100644 index 0000000..3d608c7 Binary files /dev/null and b/kivy/core/image/__pycache__/img_dds.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_ffpyplayer.cpython-310.pyc b/kivy/core/image/__pycache__/img_ffpyplayer.cpython-310.pyc new file mode 100644 index 0000000..8805b85 Binary files /dev/null and b/kivy/core/image/__pycache__/img_ffpyplayer.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_pil.cpython-310.pyc b/kivy/core/image/__pycache__/img_pil.cpython-310.pyc new file mode 100644 index 0000000..44f4882 Binary files /dev/null and b/kivy/core/image/__pycache__/img_pil.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_pygame.cpython-310.pyc b/kivy/core/image/__pycache__/img_pygame.cpython-310.pyc new file mode 100644 index 0000000..7b5d5ab Binary files /dev/null and b/kivy/core/image/__pycache__/img_pygame.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_sdl2.cpython-310.pyc b/kivy/core/image/__pycache__/img_sdl2.cpython-310.pyc new file mode 100644 index 0000000..53b0c03 Binary files /dev/null and b/kivy/core/image/__pycache__/img_sdl2.cpython-310.pyc differ diff --git a/kivy/core/image/__pycache__/img_tex.cpython-310.pyc b/kivy/core/image/__pycache__/img_tex.cpython-310.pyc new file mode 100644 index 0000000..ca4a965 Binary files /dev/null and b/kivy/core/image/__pycache__/img_tex.cpython-310.pyc differ diff --git a/kivy/core/image/_img_sdl2.cpython-310-x86_64-linux-gnu.so b/kivy/core/image/_img_sdl2.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..4c28cc1 Binary files /dev/null and b/kivy/core/image/_img_sdl2.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/image/img_dds.py b/kivy/core/image/img_dds.py new file mode 100644 index 0000000..adb6f27 --- /dev/null +++ b/kivy/core/image/img_dds.py @@ -0,0 +1,40 @@ +''' +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) diff --git a/kivy/core/image/img_ffpyplayer.py b/kivy/core/image/img_ffpyplayer.py new file mode 100644 index 0000000..1533839 --- /dev/null +++ b/kivy/core/image/img_ffpyplayer.py @@ -0,0 +1,87 @@ +''' +FFPyPlayer: FFmpeg based image loader +''' + +__all__ = ('ImageLoaderFFPy', ) + +import ffpyplayer +from ffpyplayer.pic import ImageLoader as ffImageLoader, SWScale +from ffpyplayer.tools import set_log_callback, 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) diff --git a/kivy/core/image/img_pil.py b/kivy/core/image/img_pil.py new file mode 100644 index 0000000..dfc062d --- /dev/null +++ b/kivy/core/image/img_pil.py @@ -0,0 +1,123 @@ +''' +PIL: PIL image loader +''' + +__all__ = ('ImageLoaderPIL', ) + +try: + import Image as PILImage +except ImportError: + # for python3 + from PIL import Image as PILImage + +from kivy.logger import Logger +from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader + +try: + # Pillow + PILImage.frombytes + PILImage.Image.tobytes +except AttributeError: + # PIL + # monkey patch frombytes and tobytes methods, refs: + # https://github.com/kivy/kivy/issues/5460 + PILImage.frombytes = PILImage.frombuffer + PILImage.Image.tobytes = PILImage.Image.tostring + + +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 implemented 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(fmt, is_bytesio): + if is_bytesio: + return False + return fmt in ImageLoaderPIL.extensions() + + @staticmethod + def can_load_memory(): + return True + + @staticmethod + def extensions(): + '''Return accepted extensions for this loader''' + PILImage.init() + return tuple((ext_with_dot[1:] for ext_with_dot in PILImage.EXTENSION)) + + 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.tobytes()) + 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, pixelfmt, pixels, flipped=False, + imagefmt=None): + image = PILImage.frombytes(pixelfmt.upper(), (width, height), pixels) + if flipped: + image = image.transpose(PILImage.FLIP_TOP_BOTTOM) + image.save(filename) + return True + + +# register +ImageLoader.register(ImageLoaderPIL) diff --git a/kivy/core/image/img_pygame.py b/kivy/core/image/img_pygame.py new file mode 100644 index 0000000..73a5132 --- /dev/null +++ b/kivy/core/image/img_pygame.py @@ -0,0 +1,119 @@ +''' +Pygame: Pygame image loader + +.. warning:: + + Pygame has been deprecated and will be removed in the release after Kivy + 1.11.0. +''' + +__all__ = ('ImageLoaderPygame', ) + +from kivy.logger import Logger +from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader +from os.path import isfile +from kivy.utils import deprecated + +try: + import pygame +except: + raise + + +class ImageLoaderPygame(ImageLoaderBase): + '''Image loader based on the PIL library''' + + @deprecated( + msg='Pygame has been deprecated and will be removed after 1.11.0') + def __init__(self, *largs, **kwargs): + super(ImageLoaderPygame, self).__init__(*largs, **kwargs) + + @staticmethod + def extensions(): + '''Return accepted extensions for this loader''' + # under OS X, 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(fmt, is_bytesio): + if is_bytesio: + return False + return fmt in ('png', 'jpg') + + @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 and not im.get_colorkey(): + 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, pixelfmt, pixels, flipped, + imagefmt=None): + surface = pygame.image.fromstring( + pixels, (width, height), pixelfmt.upper(), flipped) + pygame.image.save(surface, filename) + return True + + +# register +ImageLoader.register(ImageLoaderPygame) diff --git a/kivy/core/image/img_sdl2.py b/kivy/core/image/img_sdl2.py new file mode 100644 index 0000000..da684be --- /dev/null +++ b/kivy/core/image/img_sdl2.py @@ -0,0 +1,66 @@ +''' +SDL2 image loader +================= +''' + +__all__ = ('ImageLoaderSDL2', ) + +from kivy.logger import Logger +from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader +try: + from kivy.core.image import _img_sdl2 +except ImportError: + from kivy.core import handle_win_lib_import_error + handle_win_lib_import_error( + 'image', 'sdl2', 'kivy.core.image._img_sdl2') + raise + + +class ImageLoaderSDL2(ImageLoaderBase): + '''Image loader based on SDL2_image''' + + def _ensure_ext(self): + _img_sdl2.init() + + @staticmethod + def extensions(): + '''Return accepted extensions for this loader''' + return ('bmp', 'jpg', 'jpeg', 'jpe', 'lbm', 'pcx', 'png', 'pnm', + 'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv') + + @staticmethod + def can_save(fmt, is_bytesio): + return fmt in ('jpg', 'png') + + @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, pixelfmt, pixels, flipped, imagefmt): + _img_sdl2.save(filename, width, height, pixelfmt, pixels, flipped, + imagefmt) + return True + + +# register +ImageLoader.register(ImageLoaderSDL2) diff --git a/kivy/core/image/img_tex.py b/kivy/core/image/img_tex.py new file mode 100644 index 0000000..187e011 --- /dev/null +++ b/kivy/core/image/img_tex.py @@ -0,0 +1,58 @@ +''' +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) diff --git a/kivy/core/spelling/__init__.py b/kivy/core/spelling/__init__.py new file mode 100644 index 0000000..83949e5 --- /dev/null +++ b/kivy/core/spelling/__init__.py @@ -0,0 +1,135 @@ +''' +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) diff --git a/kivy/core/spelling/__pycache__/__init__.cpython-310.pyc b/kivy/core/spelling/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4fb3acc Binary files /dev/null and b/kivy/core/spelling/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/spelling/__pycache__/spelling_enchant.cpython-310.pyc b/kivy/core/spelling/__pycache__/spelling_enchant.cpython-310.pyc new file mode 100644 index 0000000..f2c939d Binary files /dev/null and b/kivy/core/spelling/__pycache__/spelling_enchant.cpython-310.pyc differ diff --git a/kivy/core/spelling/__pycache__/spelling_osxappkit.cpython-310.pyc b/kivy/core/spelling/__pycache__/spelling_osxappkit.cpython-310.pyc new file mode 100644 index 0000000..7ef6679 Binary files /dev/null and b/kivy/core/spelling/__pycache__/spelling_osxappkit.cpython-310.pyc differ diff --git a/kivy/core/spelling/spelling_enchant.py b/kivy/core/spelling/spelling_enchant.py new file mode 100644 index 0000000..55ed99a --- /dev/null +++ b/kivy/core/spelling/spelling_enchant.py @@ -0,0 +1,50 @@ +''' +Enchant Spelling +================ + +Implementation spelling backend based on enchant. + +.. warning:: pyenchant doesn't have dedicated build anymore for Windows/x64. + See https://github.com/kivy/kivy/issues/5816 for more information +''' + + +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 diff --git a/kivy/core/spelling/spelling_osxappkit.py b/kivy/core/spelling/spelling_osxappkit.py new file mode 100644 index 0000000..2afda71 --- /dev/null +++ b/kivy/core/spelling/spelling_osxappkit.py @@ -0,0 +1,64 @@ +''' +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) diff --git a/kivy/core/text/__init__.py b/kivy/core/text/__init__.py new file mode 100644 index 0000000..fb1e2ee --- /dev/null +++ b/kivy/core/text/__init__.py @@ -0,0 +1,1016 @@ +''' +Text +==== + +An abstraction of text creation. Depending of the selected backend, the +accuracy of text rendering may vary. + +.. versionchanged:: 1.10.1 + :meth:`LabelBase.find_base_direction` added. + +.. 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 rendering text with 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.text 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 + + +Font Context Manager +==================== + +A font context is a namespace where multiple fonts are loaded; if a font is +missing a glyph needed to render text, it can fall back to a different font in +the same context. The font context manager can be used to query and manipulate +the state of font contexts when using the Pango text provider (no other +provider currently implements it). + +.. versionadded:: 1.11.0 + +.. warning:: This feature requires the Pango text provider. + +Font contexts can be created automatically by :class:`kivy.uix.label.Label` or +:class:`kivy.uix.textinput.TextInput`; if a non-existent context is used in +one of these classes, it will be created automatically, or if a font file is +specified without a context (this creates an isolated context, without +support for fallback). + +Usage example:: + + from kivy.uix.label import Label + from kivy.core.text import FontContextManager as FCM + + # Create a font context containing system fonts + one custom TTF + FCM.create('system://myapp') + family = FCM.add_font('/path/to/file.ttf') + + # These are now interchangeable ways to refer to the custom font: + lbl1 = Label(font_context='system://myapp', family_name=family) + lbl2 = Label(font_context='system://myapp', font_name='/path/to/file.ttf') + + # You could also refer to a system font by family, since this is a + # system:// font context + lbl3 = Label(font_context='system://myapp', family_name='Arial') +''' + +__all__ = ('LabelBase', 'Label', + 'FontContextManagerBase', 'FontContextManager') + +import re +import os +from ast import literal_eval +from functools import partial +from copy import copy +from kivy import kivy_data_dir +from kivy.config import Config +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, USE_PANGOFT2 + + +if 'KIVY_DOC' not in os.environ: + _default_font_paths = literal_eval(Config.get('kivy', 'default_font')) + DEFAULT_FONT = _default_font_paths.pop(0) +else: + DEFAULT_FONT = None + +FONT_REGULAR = 0 +FONT_ITALIC = 1 +FONT_BOLD = 2 +FONT_BOLDITALIC = 3 + +whitespace_pat = re.compile('( +)') + + +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_context`: str, defaults to None + Context for the specified font (see :class:`kivy.uix.label.Label` + for details). `None` will autocreate an isolated context named + after the resolved font file. + `font_name`: str, defaults to DEFAULT_FONT + Font name of the text + `font_family`: str, defaults to None + Font family name to request for drawing, this can only be used + with `font_context`. + `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 stripped, 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'`. + `outline_width`: int, defaults to None + Width in pixels for the outline. + `outline_color`: tuple, defaults to (0, 0, 0) + Color of the outline. + `font_features`: str, defaults to None + OpenType font features in CSS format (Pango only) + `base_direction`: str, defaults to None (auto) + Text direction, one of `None`, `'ltr'`, `'rtl'`, `'weak_ltr'`, + or `'weak_rtl'` (Pango only) + `text_language`: str, defaults to None (user locale) + RFC-3066 format language tag as a string (Pango only) + + .. versionchanged:: 1.10.1 + `font_context`, `font_family`, `font_features`, `base_direction` + and `text_language` were added. + + .. versionchanged:: 1.10.0 + `outline_width` and `outline_color` were added. + + .. 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 = [] + + _font_dirs_files = [] + + _texture_1px = None + + _font_family_support = False + + def __init__( + self, text='', font_size=12, font_name=DEFAULT_FONT, bold=False, + italic=False, underline=False, strikethrough=False, font_family=None, + 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', + font_hinting='normal', font_kerning=True, font_blended=True, + outline_width=None, outline_color=None, font_context=None, + font_features=None, base_direction=None, text_language=None, + **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, + 'underline': underline, 'strikethrough': strikethrough, + 'font_family': font_family, + '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, + 'font_hinting': font_hinting, + 'font_kerning': font_kerning, + 'font_blended': font_blended, + 'outline_width': outline_width, + 'font_context': font_context, + 'font_features': font_features, + 'base_direction': base_direction, + 'text_language': text_language} + + kwargs_get = kwargs.get + options['color'] = color or (1, 1, 1, 1) + options['outline_color'] = outline_color or (0, 0, 0, 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.is_shortened = False + 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. + ''' + + if fn_regular is None: + raise ValueError("font_regular cannot be None") + + 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} not found'.format(font_type)) + else: + fonts.append(font) + else: + fonts.append(fonts[0]) # 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 + + if self._font_family_support and options['font_family']: + options['font_name_r'] = None + return + + # is the font registered? + if fontname in fonts: + # return the preferred font for the current bold/italic combination + 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 and not fontname.endswith('.ttf'): + fontname = '{}.ttf'.format(fontname) + filename = resource_find(fontname) + + if filename is None: + # XXX for compatibility, check directly in the data dir + filename = pep8_fn = os.path.join(kivy_data_dir, fontname) + if not os.path.exists(pep8_fn) or not os.path.isfile(pep8_fn): + raise IOError('Label: File %r not found' % fontname) + fontscache[fontname] = filename + options['font_name_r'] = filename + + @staticmethod + def get_system_fonts_dir(): + '''Return the directories used by the system for fonts. + ''' + if LabelBase._fonts_dirs: + return LabelBase._fonts_dirs + + fdirs = [] + if platform == 'linux': + fdirs = [ + '/usr/share/fonts', '/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.path.join(os.environ['SYSTEMROOT'], 'Fonts')] + elif platform == 'ios': + fdirs = ['/System/Library/Fonts'] + elif platform == 'android': + fdirs = ['/system/fonts'] + else: + raise Exception("Unknown platform: {}".format(platform)) + + fdirs.append(os.path.join(kivy_data_dir, 'fonts')) + # register the font dirs + rdirs = [] + _font_dir_files = [] + for fdir in fdirs: + for _dir, dirs, files in os.walk(fdir): + _font_dir_files.extend(files) + resource_add_path(_dir) + rdirs.append(_dir) + LabelBase._fonts_dirs = rdirs + LabelBase._font_dirs_files = _font_dir_files + + return rdirs + + 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`. + + :returns: + 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: + self.is_shortened = True + 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: + self.is_shortened = True + 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 + self.is_shortened = True + 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: + self.is_shortened = True + 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 + self.is_shortened = True + 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) + + self.is_shortened = True + return chr('{0}...{1}').format(text[:e1], text[s2 + 1:]) + + def _default_line_options(self, lines): + for line in lines: + if len(line.words): # get opts from first line, first word + return line.words[0].options + return None + + def clear_texture(self): + self._render_begin() + data = self._render_end() + assert(data) + if data is not None and data.width > 1: + self.texture.blit_data(data) + return + + # FIXME: This should possibly use a Config value, and possibly we should + # expose pango_unichar_direction() / pango_bidi_type_for_unichar() + @staticmethod + def find_base_direction(text): + '''Searches a string the first character that has a strong direction, + according to the Unicode bidirectional algorithm. Returns `None` if + the base direction cannot be determined, or one of `'ltr'` or `'rtl'`. + + .. versionadded: 1.10.1 + + .. note:: This feature requires the Pango text provider. + ''' + return 'ltr' + + def render_lines(self, lines, options, render_text, y, size): + get_extents = self.get_cached_extents() + uw, uh = options['text_size'] + xpad = options['padding_x'] + if uw is not None: + uww = uw - 2 * xpad # real width of just text + w = size[0] + sw = options['space_width'] + halign = options['halign'] + split = re.split + find_base_dir = self.find_base_direction + cur_base_dir = options['base_direction'] + + 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 + if not cur_base_dir: + cur_base_dir = find_base_dir(line) + x = xpad + if halign == 'auto': + if cur_base_dir and 'rtl' in cur_base_dir: + x = max(0, int(w - lw - xpad)) # right-align RTL text + elif halign == 'center': + x = int((w - lw) / 2.) + elif halign == '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 == 'justify' 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(whitespace_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 + return y + + def _render_real(self): + lines = self._cached_lines + options = self._default_line_options(lines) + if options is None: # there was no text to render + return self.clear_texture() + + old_opts = self.options + ih = self._internal_size[1] # the real size of text, not texture + size = self.size + valign = options['valign'] + + y = ypad = options['padding_y'] # pos in the texture + if valign == 'bottom': + y = size[1] - ih + ypad + elif valign == 'middle' or valign == 'center': + y = int((size[1] - ih) / 2 + ypad) + + self._render_begin() + self.render_lines(lines, options, self._render_text, y, size) + + # get data from provider + data = self._render_end() + assert(data) + self.options = old_opts + + # 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'] == 'justify') + uw, uh = options['text_size'] = self._text_size + text = self.text + if strip: + text = text.strip() + + self.is_shortened = False + 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'] == 'middle' or + options['valign'] == 'center'): + 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'] == 'top', 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): + 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', 'underline', 'strikethrough')]) + + 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.''') + + +class FontContextManagerBase(object): + @staticmethod + def create(font_context): + '''Create a font context, you must specify a unique name (string). + Returns `True` on success and `False` on failure. + + If `font_context` starts with one of the reserved words `'system://'`, + `'directory://'`, `'fontconfig://'` or `'systemconfig://'`, the context + is setup accordingly (exact results of this depends on your platform, + environment and configuration). + + * `'system://'` loads the default system's FontConfig configuration + and all fonts (usually including user fonts). + * `directory://` contexts preload a directory of font files (specified + in the context name), `systemconfig://` loads the system's FontConfig + configuration (but no fonts), and `fontconfig://` loads FontConfig + configuration file (specified in the context name!). These are for + advanced users only, check the source code and FontConfig + documentation for details. + * Fonts automatically loaded to an isolated context (ie when no + font context was specified) start with `'isolated://'`. This has + no special effect, and only serves to help you identify them in + the results returned from :meth:`list`. + * Any other string is a context that will only draw with the font + file(s) you explicitly add to it. + + .. versionadded:: 1.11.0 + + .. note:: + Font contexts are created automatically by specifying a name in the + `font_context` property of :class:`kivy.uix.label.Label` or + :class:`kivy.uix.textinput.TextInput`. They are also auto-created + by :meth:`add_font` by default, so you normally don't need to + call this directly. + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def exists(font_context): + '''Returns True if a font context with the given name exists. + + .. versionadded:: 1.11.0 + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def destroy(font_context): + '''Destroy a named font context (if it exists) + + .. versionadded:: 1.11.0 + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def list(): + '''Returns a list of `bytes` objects, each representing a cached font + context name. Note that entries that start with `isolated://` were + autocreated by loading a font file with no font_context specified. + + .. versionadded:: 1.11.0 + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def list_families(font_context): + '''Returns a list of `bytes` objects, each representing a font family + name that is available in the given `font_context`. + + .. versionadded:: 1.11.0 + + .. note:: + Pango adds static "Serif", "Sans" and "Monospace" to the list in + current versions, even if only a single custom font file is added + to the context. + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def list_custom(font_context): + '''Returns a dictionary representing all the custom-loaded fonts in + the context. The key is a `bytes` object representing the full path + to the font file, the value is a `bytes` object representing the font + family name used to request drawing with the font. + + .. versionadded:: 1.11.0 + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + @staticmethod + def add_font(font_context, filename, autocreate=True, family=None): + '''Add a font file to a named font context. If `autocreate` is true, + the context will be created if it does not exist (this is the + default). You can specify the `family` argument (string) to skip + auto-detecting the font family name. + + .. warning:: + + The `family` argument is slated for removal if the underlying + implementation can be fixed, It is offered as a way to optimize + startup time for deployed applications (it avoids opening the + file with FreeType2 to determine its family name). To use this, + first load the font file without specifying `family`, and + hardcode the returned (autodetected) `family` value in your font + context initialization. + + .. versionadded:: 1.11.0 + + .. note:: This feature requires the Pango text provider. + ''' + raise NotImplementedError("No font_context support in text provider") + + +# Load the appropriate provider +label_libs = [] +if USE_PANGOFT2: + label_libs += [('pango', 'text_pango', 'LabelPango')] + +if USE_SDL2: + label_libs += [('sdl2', 'text_sdl2', 'LabelSDL2')] +else: + label_libs += [('pygame', 'text_pygame', 'LabelPygame')] +label_libs += [ + ('pil', 'text_pil', 'LabelPIL')] +Text = 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) + + # FIXME: Better way to do this + if Label.__name__ == 'LabelPango': + from kivy.core.text.text_pango import PangoFontContextManager + FontContextManager = PangoFontContextManager() + else: + FontContextManager = FontContextManagerBase() + + +# For the first initialization, register the default font + Label.register(DEFAULT_FONT, *_default_font_paths) diff --git a/kivy/core/text/__pycache__/__init__.cpython-310.pyc b/kivy/core/text/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1a5d71b Binary files /dev/null and b/kivy/core/text/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/text/__pycache__/markup.cpython-310.pyc b/kivy/core/text/__pycache__/markup.cpython-310.pyc new file mode 100644 index 0000000..b43f22a Binary files /dev/null and b/kivy/core/text/__pycache__/markup.cpython-310.pyc differ diff --git a/kivy/core/text/__pycache__/text_pango.cpython-310.pyc b/kivy/core/text/__pycache__/text_pango.cpython-310.pyc new file mode 100644 index 0000000..7f063ac Binary files /dev/null and b/kivy/core/text/__pycache__/text_pango.cpython-310.pyc differ diff --git a/kivy/core/text/__pycache__/text_pil.cpython-310.pyc b/kivy/core/text/__pycache__/text_pil.cpython-310.pyc new file mode 100644 index 0000000..fa3b149 Binary files /dev/null and b/kivy/core/text/__pycache__/text_pil.cpython-310.pyc differ diff --git a/kivy/core/text/__pycache__/text_pygame.cpython-310.pyc b/kivy/core/text/__pycache__/text_pygame.cpython-310.pyc new file mode 100644 index 0000000..926d6a5 Binary files /dev/null and b/kivy/core/text/__pycache__/text_pygame.cpython-310.pyc differ diff --git a/kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc b/kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc new file mode 100644 index 0000000..cedc15f Binary files /dev/null and b/kivy/core/text/__pycache__/text_sdl2.cpython-310.pyc differ diff --git a/kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so b/kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..4425e4f Binary files /dev/null and b/kivy/core/text/_text_sdl2.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/text/markup.py b/kivy/core/text/markup.py new file mode 100644 index 0000000..3be79ac --- /dev/null +++ b/kivy/core/text/markup.py @@ -0,0 +1,879 @@ +''' +Text Markup +=========== + +.. versionadded:: 1.1.0 + +.. versionchanged:: 1.10.1 + + Added `font_context`, `font_features` and `text_language` (Pango only) + +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 should have a corresponding +``[/tag]`` closing tag. For example:: + + [b]Hello [color=ff0000]world[/color][/b] + +The following tags are available: + +``[b][/b]`` + Activate bold text +``[i][/i]`` + Activate italic text +``[u][/u]`` + Underlined text +``[s][/s]`` + Strikethrough text +``[font=][/font]`` + Change the font (note: this refers to a TTF file or registered alias) +``[font_context=][/font_context]`` + Change context for the font, use string value "none" for isolated context. +``[font_family=][/font_family]`` + Font family to request for drawing. This is only valid when using a + font context, and takes precedence over `[font]`. See + :class:`kivy.uix.label.Label` for details. +``[font_features=][/font_features]`` + OpenType font features, in CSS format, this is passed straight + through to Pango. The effects of requesting a feature depends on loaded + fonts, library versions, etc. Pango only, requires v1.38 or later. +``[size=][/size]`` + Change the font size. should be an integer, optionally with a + unit (i.e. ``16sp``) +``[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. +``[text_language=][/text_language]`` + Language of the text, this is an RFC-3066 format language tag (as string), + for example "en_US", "zh_CN", "fr" or "ja". This can impact font selection, + metrics and rendering. For example, the same bytes of text can look + different for `ur` and `ar` languages, though both use Arabic script. + Use the string `'none'` to revert to locale detection. Pango only. + +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 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 information. + ''' + + 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 split:: + + >>> MarkupLabel('[b]Hello world[/b]').markup + >>> ('[b]', 'Hello world', '[/b]') + + ''' + s = re.split(r'(\[.*?\])', self.label) + s = [x for x in s if x != ''] + return s + + def _push_style(self, k): + if k not 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._render_real() + 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 + 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'] != 'top' or + options['shorten'] else uh) + options['strip'] = options['strip'] or options['halign'] == 'justify' + find_base_dir = Label.find_base_direction + base_dir = options['base_direction'] + self._resolved_base_dir = None + 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 == '[u]': + spush('underline') + options['underline'] = True + self.resolve_font_name() + elif item == '[/u]': + spop('underline') + self.resolve_font_name() + elif item == '[s]': + spush('strikethrough') + options['strikethrough'] = True + self.resolve_font_name() + elif item == '[/s]': + spop('strikethrough') + 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[:13] == '[font_family=': + spush('font_family') + options['font_family'] = item[13:-1] + elif item == '[/font_family]': + spop('font_family') + elif item[:14] == '[font_context=': + fctx = item[14:-1] + if not fctx or fctx.lower() == 'none': + fctx = None + spush('font_context') + options['font_context'] = fctx + elif item == '[/font_context]': + spop('font_context') + elif item[:15] == '[font_features=': + spush('font_features') + options['font_features'] = item[15:-1] + elif item == '[/font_features]': + spop('font_features') + elif item[:15] == '[text_language=': + lang = item[15:-1] + if not lang or lang.lower() == 'none': + lang = None + spush('text_language') + options['text_language'] = lang + elif item == '[/text_language]': + spop('text_language') + 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('&', '&') + if not base_dir: + base_dir = self._resolved_base_dir = find_base_dir(item) + 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, + append_down=True, + complete=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(), + append_down=True, + complete=True + ) + self.options = old_opts + + self.is_shortened = False + 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'] == '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'] == 'justify' and uw is not None: + # XXX: update refs to justified pos + # when justify, each line should've 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 render_lines(self, lines, options, render_text, y, size): + xpad = options['padding_x'] + w = size[0] + halign = options['halign'] + refs = self._refs + anchors = self._anchors + base_dir = options['base_direction'] or self._resolved_base_dir + auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir + + 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 == 'center': + x = int((w - lw) / 2.) + elif halign == 'right' or auto_halign_r: + 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 ref not 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 anchor not in anchors: + anchors[anchor] = (x, y) + x += word.lw + y += lh + return y + + 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 + self.is_shortened = False + return ( + lw + 2 * xpad, + lh + 2 * ypad, + [LayoutLine(0, 0, lw, lh, 1, 0, line)] + ) + + elps_opts = copy(old_opts) + if 'ellipsis_options' in old_opts: + elps_opts.update(old_opts['ellipsis_options']) + + # Set new opts for ellipsis + self.options = elps_opts + # find the size of ellipsis that'll fit + elps_s = textwidth('...') + if elps_s[0] > uw: # even ellipsis didn't fit... + self.is_shortened = True + 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(elps_opts, elps_s[0], elps_s[1], '...') + uw -= elps_s[0] + # Restore old opts + self.options = old_opts + + # 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 + self.is_shortened = True + 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 + self.is_shortened = True + 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 + self.is_shortened = True + 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 + self.is_shortened = True + 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 + if uw < lw: + self.is_shortened = True + return ( + lw + 2 * xpad, + lh + 2 * ypad, + [LayoutLine(0, 0, lw, lh, 1, 0, line1)] + ) diff --git a/kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so b/kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..24dafab Binary files /dev/null and b/kivy/core/text/text_layout.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/text/text_layout.pxd b/kivy/core/text/text_layout.pxd new file mode 100644 index 0000000..bb6e397 --- /dev/null +++ b/kivy/core/text/text_layout.pxd @@ -0,0 +1,13 @@ + + +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 diff --git a/kivy/core/text/text_pango.py b/kivy/core/text/text_pango.py new file mode 100644 index 0000000..615053c --- /dev/null +++ b/kivy/core/text/text_pango.py @@ -0,0 +1,145 @@ +''' +Pango text provider +=================== + +.. versionadded:: 1.11.0 + +.. warning:: + The low-level Pango API is experimental, and subject to change without + notice for as long as this warning is present. + + +Installation +------------ + +1. Install pangoft2 (`apt install libfreetype6-dev libpango1.0-dev + libpangoft2-1.0-0`) or ensure it is available in pkg-config +2. Recompile kivy. Check that pangoft2 is found `use_pangoft2 = 1` +3. Test it! Enforce the text core renderer to pango using environment variable: + `export KIVY_TEXT=pango` + +This has been tested on OSX and Linux, Python 3.6. + + +Font context types for FontConfig+FreeType2 backend +--------------------------------------------------- + +* `system://` - `FcInitLoadConfigAndFonts()` +* `systemconfig://` - `FcInitLoadConfig()` +* `directory://` - `FcInitLoadConfig()` + `FcAppFontAddDir()` +* `fontconfig://` - `FcConfigCreate()` + `FcConfigParseAndLoad()` +* Any other context name - `FcConfigCreate()` + + +Low-level Pango access +---------------------- + +Since Kivy currently does its own text layout, the Label and TextInput widgets +do not take full advantage of Pango. For example, line breaks do not take +language/script into account, and switching alignment per paragraph (for bi- +directional text) is not supported. For advanced i18n requirements, we provide +a simple wrapper around PangoLayout that you can use to render text. + +* https://developer.gnome.org/pango/1.40/pango-Layout-Objects.html +* https://developer.gnome.org/pango/1.40/PangoMarkupFormat.html +* See the `kivy/core/text/_text_pango.pyx` file @ `cdef class KivyPangoLayout` + for more information. Not all features of PangoLayout are implemented. + +.. python:: + from kivy.core.window import Window # OpenGL must be initialized + from kivy.core.text._text_pango import KivyPangoLayout + layout = KivyPangoLayout('system://') + layout.set_markup('Hello World!') + tex = layout.render_as_Texture() + + +Known limitations +----------------- + +* Pango versions older than v1.38 has not been tested. It may work on + some systems with older pango and newer FontConfig/FreeType2 versions. +* Kivy's text layout is used, not Pango. This means we do not use Pango's + line-breaking feature (which is superior to Kivy's), and we can't use + Pango's bidirectional cursor helpers in TextInput. +* Font family collissions can happen. For example, if you use a `system://` + context and add a custom `Arial.ttf`, using `arial` as the `font_family` + may or may not draw with your custom font (depending on whether or not + there is already a system-wide "arial" font installed) +* Rendering is inefficient; the normal way to integrate Pango would be + using a dedicated PangoLayout per widget. This is not currently practical + due to missing abstractions in Kivy core (in the current implementation, + we have a dedicated PangoLayout *per font context,* which is rendered + once for each LayoutWord) +''' + +__all__ = ('LabelPango', ) + +from types import MethodType +from os.path import isfile +from kivy.resources import resource_find +from kivy.core.text import LabelBase, FontContextManagerBase +from kivy.core.text._text_pango import ( + KivyPangoRenderer, + kpango_get_extents, + kpango_get_ascent, + kpango_get_descent, + kpango_find_base_dir, + kpango_font_context_exists, + kpango_font_context_create, + kpango_font_context_destroy, + kpango_font_context_add_font, + kpango_font_context_list, + kpango_font_context_list_custom, + kpango_font_context_list_families) + + +class LabelPango(LabelBase): + + _font_family_support = True + + def __init__(self, *largs, **kwargs): + self.get_extents = MethodType(kpango_get_extents, self) + self.get_ascent = MethodType(kpango_get_ascent, self) + self.get_descent = MethodType(kpango_get_descent, self) + super(LabelPango, self).__init__(*largs, **kwargs) + + find_base_direction = staticmethod(kpango_find_base_dir) + + def _render_begin(self): + self._rdr = KivyPangoRenderer(*self._size) + + def _render_text(self, text, x, y): + self._rdr.render(self, text, x, y) + + def _render_end(self): + imgdata = self._rdr.get_ImageData() + del self._rdr + return imgdata + + +class PangoFontContextManager(FontContextManagerBase): + create = staticmethod(kpango_font_context_create) + exists = staticmethod(kpango_font_context_exists) + destroy = staticmethod(kpango_font_context_destroy) + list = staticmethod(kpango_font_context_list) + list_families = staticmethod(kpango_font_context_list_families) + list_custom = staticmethod(kpango_font_context_list_custom) + + @staticmethod + def add_font(font_context, filename, autocreate=True, family=None): + if not autocreate and not PangoFontContextManager.exists(font_context): + raise Exception("FontContextManager: Attempt to add font file " + "'{}' to non-existing context '{}' without " + "autocreate.".format(filename, font_context)) + if not filename: + raise Exception("FontContextManager: Cannot add empty font file") + if not isfile(filename): + filename = resource_find(filename) + if not isfile(filename): + if not filename.endswith('.ttf'): + filename = resource_find('{}.ttf'.format(filename)) + if filename and isfile(filename): + return kpango_font_context_add_font(font_context, filename) + raise Exception("FontContextManager: Attempt to add non-existent " + "font file: '{}' to context '{}'" + .format(filename, font_context)) diff --git a/kivy/core/text/text_pil.py b/kivy/core/text/text_pil.py new file mode 100644 index 0000000..6549166 --- /dev/null +++ b/kivy/core/text/text_pil.py @@ -0,0 +1,60 @@ +''' +Text PIL: Draw text with PIL +''' + +__all__ = ('LabelPIL', ) + +from PIL import Image, ImageFont, ImageDraw + + +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 id not 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, color=(255, 255, 255, 0)) + 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.tobytes()) + + del self._pil_im + del self._pil_draw + + return data diff --git a/kivy/core/text/text_pygame.py b/kivy/core/text/text_pygame.py new file mode 100644 index 0000000..21cac7a --- /dev/null +++ b/kivy/core/text/text_pygame.py @@ -0,0 +1,117 @@ +''' +Text Pygame: Draw text with pygame + +.. warning:: + + Pygame has been deprecated and will be removed in the release after Kivy + 1.11.0. +''' + +__all__ = ('LabelPygame', ) + +from kivy.compat import PY2 +from kivy.core.text import LabelBase +from kivy.core.image import ImageData +from kivy.utils import deprecated + +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): + + @deprecated( + msg='Pygame has been deprecated and will be removed after 1.11.0') + def __init__(self, *largs, **kwargs): + super(LabelPygame, self).__init__(*largs, **kwargs) + + def _get_font_id(self): + 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) + text.set_colorkey(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 diff --git a/kivy/core/text/text_sdl2.py b/kivy/core/text/text_sdl2.py new file mode 100644 index 0000000..f72defc --- /dev/null +++ b/kivy/core/text/text_sdl2.py @@ -0,0 +1,50 @@ +''' +SDL2 text provider +================== + +Based on SDL2 + SDL2_ttf +''' + +__all__ = ('LabelSDL2', ) + +from kivy.compat import PY2 +from kivy.core.text import LabelBase +try: + from kivy.core.text._text_sdl2 import (_SurfaceContainer, _get_extents, + _get_fontdescent, _get_fontascent) +except ImportError: + from kivy.core import handle_win_lib_import_error + handle_win_lib_import_error( + 'text', 'sdl2', 'kivy.core.text._text_sdl2') + raise + + +class LabelSDL2(LabelBase): + + def _get_font_id(self): + return '|'.join([str(self.options[x]) for x + in ('font_size', 'font_name_r', 'bold', + 'italic', 'underline', 'strikethrough')]) + + 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() diff --git a/kivy/core/video/__init__.py b/kivy/core/video/__init__.py new file mode 100644 index 0000000..55392ff --- /dev/null +++ b/kivy/core/video/__init__.py @@ -0,0 +1,219 @@ +''' +Video +===== + +Core class for reading video files and managing the video +:class:`~kivy.graphics.texture.Texture`. + +.. versionchanged:: 1.10.0 + The pyglet, pygst and gi providers have been removed. + +.. 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. + +.. 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:: 1.4.0 + 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, precise=True): + '''Move to position as percentage (strictly, a proportion from + 0 - 1) of the duration''' + 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: + pass +video_providers += [ + ('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'), + ('ffpyplayer', 'video_ffpyplayer', 'VideoFFPy'), + ('null', 'video_null', 'VideoNull')] + + +Video = core_select_lib('video', video_providers) diff --git a/kivy/core/video/__pycache__/__init__.cpython-310.pyc b/kivy/core/video/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..c80d68f Binary files /dev/null and b/kivy/core/video/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/video/__pycache__/video_ffmpeg.cpython-310.pyc b/kivy/core/video/__pycache__/video_ffmpeg.cpython-310.pyc new file mode 100644 index 0000000..3ca562e Binary files /dev/null and b/kivy/core/video/__pycache__/video_ffmpeg.cpython-310.pyc differ diff --git a/kivy/core/video/__pycache__/video_ffpyplayer.cpython-310.pyc b/kivy/core/video/__pycache__/video_ffpyplayer.cpython-310.pyc new file mode 100644 index 0000000..982c04b Binary files /dev/null and b/kivy/core/video/__pycache__/video_ffpyplayer.cpython-310.pyc differ diff --git a/kivy/core/video/__pycache__/video_gstplayer.cpython-310.pyc b/kivy/core/video/__pycache__/video_gstplayer.cpython-310.pyc new file mode 100644 index 0000000..66733e8 Binary files /dev/null and b/kivy/core/video/__pycache__/video_gstplayer.cpython-310.pyc differ diff --git a/kivy/core/video/__pycache__/video_null.cpython-310.pyc b/kivy/core/video/__pycache__/video_null.cpython-310.pyc new file mode 100644 index 0000000..f71ba1b Binary files /dev/null and b/kivy/core/video/__pycache__/video_null.cpython-310.pyc differ diff --git a/kivy/core/video/video_ffmpeg.py b/kivy/core/video/video_ffmpeg.py new file mode 100644 index 0000000..59b19b5 --- /dev/null +++ b/kivy/core/video/video_ffmpeg.py @@ -0,0 +1,106 @@ +''' +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._player.set_volume(self._volume) + self._do_load = True + + def stop(self): + self.unload() + + def seek(self, percent, precise=True): + 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 not player.is_open: + 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 read 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 _set_volume(self, value): + self._volume = value + if self._player: + self._player.set_volume(self._volume) diff --git a/kivy/core/video/video_ffpyplayer.py b/kivy/core/video/video_ffpyplayer.py new file mode 100644 index 0000000..a14f8aa --- /dev/null +++ b/kivy/core/video/video_ffpyplayer.py @@ -0,0 +1,446 @@ +''' +FFmpeg based video abstraction +============================== + +To use, you need to install ffpyplayer 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 are 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 SDL2. + +Now, you should have ffmpeg and sdl directories. In each, you should have an +'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 in 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 an 'SDL2' directory, +which then holds the headers). + +Once defined, download the ffpyplayer git repo 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, get_log_callback +except: + raise + + +from threading import Thread +from queue import Queue, Empty, Full +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); + } + """ + + _trigger = None + + def __init__(self, **kwargs): + self._ffplayer = None + self._thread = None + self._next_frame = None + self._seek_queue = [] + self._ffplayer_need_quit = False + self._wakeup_queue = Queue(maxsize=1) + self._trigger = Clock.create_trigger(self._redraw) + + super(VideoFFPy, self).__init__(**kwargs) + + def __del__(self): + self.unload() + + def _wakeup_thread(self): + try: + self._wakeup_queue.put(None, False) + except Full: + pass + + def _wait_for_wakeup(self, timeout): + try: + self._wakeup_queue.get(True, timeout) + except Empty: + pass + + 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 _set_volume(self, volume): + self._volume = volume + if self._ffplayer is not None: + self._ffplayer.set_volume(self._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': + # this causes a seek to zero + self.position = 0 + + self.dispatch('on_eos') + + @mainthread + def _finish_setup(self): + # once setup is done, we make sure player state matches what user + # could have requested while player was being setup and it was in limbo + # also, thread starts player in internal paused mode, so this unpauses + # it if user didn't request pause meanwhile + if self._ffplayer is not None: + self._ffplayer.set_volume(self._volume) + self._ffplayer.set_pause(self._state == 'paused') + self._wakeup_thread() + + 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() + if dy and du and dv: + self._tex_y.blit_buffer(dy, colorfmt='luminance') + self._tex_u.blit_buffer(du, colorfmt='luminance') + self._tex_v.blit_buffer(dv, colorfmt='luminance') + self._fbo.ask_update() + self._fbo.draw() + else: + self._texture.blit_buffer( + img.to_memoryview()[0], colorfmt='rgba') + + self.dispatch('on_frame') + + def _next_frame_run(self, ffplayer): + sleep = time.sleep + trigger = self._trigger + did_dispatch_eof = False + wait_for_wakeup = self._wait_for_wakeup + seek_queue = self._seek_queue + # video starts in internal paused state + + # fast path, if the source video is yuv420p, we'll use a glsl shader + # for buffer conversion to rgba + # wait until we get frame metadata + while not self._ffplayer_need_quit: + src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt') + if not src_pix_fmt: + wait_for_wakeup(0.005) + continue + + if src_pix_fmt == 'yuv420p': + self._out_fmt = 'yuv420p' + ffplayer.set_output_pix_fmt(self._out_fmt) + break + + if self._ffplayer_need_quit: + ffplayer.close_player() + return + + # wait until loaded or failed, shouldn't take long, but just to make + # sure metadata is available. + while not self._ffplayer_need_quit: + if ffplayer.get_metadata()['src_vid_size'] != (0, 0): + break + wait_for_wakeup(0.005) + + if self._ffplayer_need_quit: + ffplayer.close_player() + return + + self._ffplayer = ffplayer + self._finish_setup() + # now, we'll be in internal paused state and loop will wait until + # mainthread unpauses us when finishing setup + + while not self._ffplayer_need_quit: + seek_happened = False + if seek_queue: + vals = seek_queue[:] + del seek_queue[:len(vals)] + percent, precise = vals[-1] + ffplayer.seek( + percent * ffplayer.get_metadata()['duration'], + relative=False, + accurate=precise + ) + seek_happened = True + did_dispatch_eof = False + self._next_frame = None + + # Get next frame if paused: + if seek_happened and ffplayer.get_pause(): + ffplayer.set_volume(0.0) # Try to do it silently. + ffplayer.set_pause(False) + try: + # We don't know concrete number of frames to skip, + # this number worked fine on couple of tested videos: + to_skip = 6 + while True: + frame, val = ffplayer.get_frame(show=False) + # Exit loop on invalid val: + if val in ('paused', 'eof'): + break + # Exit loop on seek_queue updated: + if seek_queue: + break + # Wait for next frame: + if frame is None: + sleep(0.005) + continue + # Wait until we skipped enough frames: + to_skip -= 1 + if to_skip == 0: + break + # Assuming last frame is actual, just get it: + frame, val = ffplayer.get_frame(force_refresh=True) + finally: + ffplayer.set_pause(bool(self._state == 'paused')) + # todo: this is not safe because user could have updated + # volume between us reading it and setting it + ffplayer.set_volume(self._volume) + # Get next frame regular: + else: + frame, val = ffplayer.get_frame() + + if val == 'eof': + if not did_dispatch_eof: + self._do_eos() + did_dispatch_eof = True + wait_for_wakeup(None) + elif val == 'paused': + did_dispatch_eof = False + wait_for_wakeup(None) + else: + did_dispatch_eof = False + if frame: + self._next_frame = frame + trigger() + else: + val = val if val else (1 / 30.) + wait_for_wakeup(val) + + ffplayer.close_player() + + def seek(self, percent, precise=True): + # still save seek while thread is setting up + self._seek_queue.append((percent, precise,)) + self._wakeup_thread() + + def stop(self): + self.unload() + + def pause(self): + # if state hasn't been set (empty), there's no player. If it's + # paused, nothing to do so just handle playing + if self._state == 'playing': + # we could be in limbo while player is setting up so check. Player + # will pause when finishing setting up + if self._ffplayer is not None: + self._ffplayer.set_pause(True) + # even in limbo, indicate to start in paused state + self._state = 'paused' + self._wakeup_thread() + + def play(self): + # _state starts empty and is empty again after unloading + if self._ffplayer: + # player is already setup, just handle unpausing + assert self._state in ('paused', 'playing') + if self._state == 'paused': + self._ffplayer.set_pause(False) + self._state = 'playing' + self._wakeup_thread() + return + + # we're now either in limbo state waiting for thread to setup, + # or no thread has been started + if self._state == 'playing': + # in limbo, just wait for thread to setup player + return + elif self._state == 'paused': + # in limbo, still unpause for when player becomes ready + self._state = 'playing' + self._wakeup_thread() + return + + # load first unloads + self.load() + self._out_fmt = 'rgba' + # it starts internally paused, but unpauses itself + ff_opts = { + 'paused': True, + 'out_fmt': self._out_fmt, + 'sn': True, + 'volume': self._volume, + } + ffplayer = MediaPlayer( + self._filename, callback=self._player_callback, + thread_lib='SDL', + loglevel='info', ff_opts=ff_opts + ) + + # Disabled as an attempt to fix kivy issue #6210 + # self._ffplayer.set_volume(self._volume) + + self._thread = Thread( + target=self._next_frame_run, + name='Next frame', + args=(ffplayer, ) + ) + # todo: remove + self._thread.daemon = True + + # start in playing mode, but _ffplayer isn't set until ready. We're + # now in a limbo state + self._state = 'playing' + self._thread.start() + + def load(self): + self.unload() + + def unload(self): + # no need to call self._trigger.cancel() because _ffplayer is set + # to None below, and it's not safe to call clock stuff from __del__ + + # if thread is still alive, set it to exit and wake it + self._wakeup_thread() + self._ffplayer_need_quit = True + # wait until it exits + if self._thread: + # TODO: use callback, don't block here + self._thread.join() + self._thread = None + + if self._ffplayer: + self._ffplayer = None + self._next_frame = None + self._size = (0, 0) + self._state = '' + self._seek_queue = [] + + # reset for next load since thread is dead for sure + self._ffplayer_need_quit = False + self._wakeup_queue = Queue(maxsize=1) diff --git a/kivy/core/video/video_gstplayer.py b/kivy/core/video/video_gstplayer.py new file mode 100644 index 0000000..9bbed46 --- /dev/null +++ b/kivy/core/video/video_gstplayer.py @@ -0,0 +1,140 @@ +''' +Video Gstplayer +=============== + +.. versionadded:: 1.8.0 + +Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer` +This player is the preferred player, using Gstreamer 1.0, working on both +Python 2 and 3. +''' + +try: + from kivy.lib.gstplayer import GstPlayer, get_gst_version +except ImportError: + from kivy.core import handle_win_lib_import_error + handle_win_lib_import_error( + 'VideoGstplayer', 'gst', 'kivy.lib.gstplayer._gstplayer') + raise +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, precise=True): + self.player.seek(percent) + + def _get_position(self): + return self.player.get_position() + + def _get_duration(self): + return self.player.get_duration() + + 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 diff --git a/kivy/core/video/video_null.py b/kivy/core/video/video_null.py new file mode 100644 index 0000000..7260a42 --- /dev/null +++ b/kivy/core/video/video_null.py @@ -0,0 +1,12 @@ + +''' +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 diff --git a/kivy/core/window/__init__.py b/kivy/core/window/__init__.py new file mode 100644 index 0000000..d03776a --- /dev/null +++ b/kivy/core/window/__init__.py @@ -0,0 +1,2460 @@ +# 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 collections import defaultdict + +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, \ + ColorProperty +from kivy.utils import platform, reify, deprecated, pi_version +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 +from kivy.graphics.cgl import cgl_get_backend_name + +# late import +VKeyboard = None +android = None +Animation = 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. These keycodes 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, + ':': 58, ';': 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) + self.target = None + + 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. + `custom_titlebar`: str, one of ('0', '1') + Set to `'1'` to uses a custom titlebar + `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. + `minimum_width`: int + Minimum width of the window (only works for sdl2 window provider). + `minimum_height`: int + Minimum height of the window (only works for sdl2 window provider). + `allow_screensaver`: bool + Allow the device to show a screen saver, or to go to sleep + on mobile devices. Defaults to True. Only works for sdl2 window + provider. + + :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_cursor_enter`: + Fired when the cursor enters the window. + + .. versionadded:: 1.9.1 + + `on_cursor_leave`: + Fired when the cursor leaves the window. + + .. versionadded:: 1.9.1 + + `on_minimize`: + Fired when the window is minimized. + + .. versionadded:: 1.10.0 + + `on_maximize`: + Fired when the window is maximized. + + .. versionadded:: 1.10.0 + + `on_restore`: + Fired when the window is restored. + + .. versionadded:: 1.10.0 + + `on_hide`: + Fired when the window is hidden. + + .. versionadded:: 1.10.0 + + `on_show`: + Fired when when the window is shown. + + .. versionadded:: 1.10.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, modifier + 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_drop_begin`: x, y, *args + Fired when text(s) or file(s) drop on the application is about to + begin. + + .. versionadded:: 2.1.0 + + `on_drop_file`: filename (bytes), x, y, *args + Fired when a file is dropped on the application. + + .. versionadded:: 1.2.0 + + .. versionchanged:: 2.1.0 + Renamed from `on_dropfile` to `on_drop_file`. + + `on_drop_text`: text (bytes), x, y, *args + Fired when a text is dropped on the application. + + .. versionadded:: 2.1.0 + + `on_drop_end`: x, y, *args + Fired when text(s) or file(s) drop on the application has ended. + + .. versionadded:: 2.1.0 + + `on_memorywarning`: + Fired when the platform have memory issue (iOS / Android mostly) + You can listen to this one, and clean whatever you can. + + .. versionadded:: 1.9.0 + + `on_textedit(self, text)`: + Fired when inputting with IME. + The string inputting with IME is set as the parameter of + this event. + + .. versionadded:: 1.10.1 + ''' + + __instance = None + __initialized = False + _fake_fullscreen = False + + # private properties + _density = NumericProperty(1.) + _size = ListProperty([0, 0]) + _modifiers = ListProperty([]) + _rotation = NumericProperty(0) + _focus = BooleanProperty(True) + + gl_backends_allowed = [] + """ + A list of Kivy gl backend names, which if not empty, will be the + exclusive list of gl backends that can be used with this window. + """ + + gl_backends_ignored = [] + """ + A list of Kivy gl backend names that may not be used with this window. + """ + + managed_textinput = False + """ + True if this Window class uses `on_textinput` to insert text, internal. + """ + + 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() + '''A path to the window icon. + + .. versionadded:: 1.1.2 + + :attr:`icon` is a :class:`~kivy.properties.StringProperty`. + ''' + + def _get_modifiers(self): + return self._modifiers + + modifiers = AliasProperty(_get_modifiers, None, bind=('_modifiers',)) + '''List of keyboard modifiers currently active. + + .. versionadded:: 1.0.9 + + :attr:`modifiers` is an :class:`~kivy.properties.AliasProperty`. + ''' + + 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_pre_resize', *size) + + minimum_width = NumericProperty(0) + '''The minimum width to restrict the window to. + + .. versionadded:: 1.9.1 + + :attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + ''' + + minimum_height = NumericProperty(0) + '''The minimum height to restrict the window to. + + .. versionadded:: 1.9.1 + + :attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + ''' + + allow_screensaver = BooleanProperty(True) + '''Whether the screen saver is enabled, or on mobile devices whether the + device is allowed to go to sleep while the app is open. + + .. versionadded:: 1.10.0 + + :attr:`allow_screensaver` is a :class:`~kivy.properties.BooleanProperty` + and defaults to True. + ''' + + size = AliasProperty(_get_size, _set_size, bind=('_size', '_rotation')) + '''Get the rotated size of the window. If :attr:`rotation` is set, then the + size will change to reflect the rotation. + + .. versionadded:: 1.0.9 + + :attr:`size` is an :class:`~kivy.properties.AliasProperty`. + ''' + + clearcolor = ColorProperty((0, 0, 0, 1)) + '''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). + + .. versionadded:: 1.0.9 + + :attr:`clearcolor` is an :class:`~kivy.properties.ColorProperty` and + defaults to (0, 0, 0, 1). + + .. versionchanged:: 2.1.0 + Changed from :class:`~kivy.properties.AliasProperty` to + :class:`~kivy.properties.ColorProperty`. + ''' + + # 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, bind=('_rotation', '_size', '_density')) + '''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, + bind=('_rotation', '_size', '_density')) + '''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, bind=('width', 'height')) + '''Center of the rotated window. + + .. versionadded:: 1.0.9 + + :attr:`center` is an :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 not self.initialized: + return + self.dispatch('on_pre_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. + + .. versionadded:: 1.0.9 + + :attr:`rotation` is an :class:`~kivy.properties.AliasProperty`. + ''' + + softinput_mode = OptionProperty( + '', options=('', 'below_target', 'pan', 'scale', 'resize') + ) + '''This specifies the behavior of window contents on display of the soft + keyboard on mobile platforms. It can be one of '', 'pan', 'scale', + 'resize' or 'below_target'. Their effects are listed below. + + +----------------+-------------------------------------------------------+ + | Value | Effect | + +================+=======================================================+ + | '' | The main window is left as is, allowing you to use | + | | the :attr:`keyboard_height` to manage the window | + | | contents manually. | + +----------------+-------------------------------------------------------+ + | 'pan' | The main window pans, moving the bottom part of the | + | | window to be always on top of the keyboard. | + +----------------+-------------------------------------------------------+ + | 'resize' | The window is resized and the contents scaled to fit | + | | the remaining space. | + +----------------+-------------------------------------------------------+ + | 'below_target' | The window pans so that the current target TextInput | + | | widget requesting the keyboard is presented just above| + | | the soft keyboard. | + +----------------+-------------------------------------------------------+ + + :attr:`softinput_mode` is an :class:`~kivy.properties.OptionProperty` and + defaults to ''. + + .. note:: The `resize` option does not currently work with SDL2 on Android. + + .. versionadded:: 1.9.0 + + .. versionchanged:: 1.9.1 + The 'below_target' option was added. + ''' + + _keyboard_changed = BooleanProperty(False) + _kheight = NumericProperty(0) + _kanimation = None + + def _free_kanimation(self, *largs): + WindowBase._kanimation = None + + def _animate_content(self): + '''Animate content to IME height. + ''' + kargs = self.keyboard_anim_args + global Animation + if not Animation: + from kivy.animation import Animation + if WindowBase._kanimation: + WindowBase._kanimation.cancel(self) + WindowBase._kanimation = kanim = Animation( + _kheight=self.keyboard_height + self.keyboard_padding, + d=kargs['d'], t=kargs['t']) + kanim.bind(on_complete=self._free_kanimation) + kanim.start(self) + + def _upd_kbd_height(self, *kargs): + self._keyboard_changed = not self._keyboard_changed + self._animate_content() + + def _get_ios_kheight(self): + import ios + return ios.get_kheight() + + def _get_android_kheight(self): + if USE_SDL2: # Placeholder until the SDL2 bootstrap supports this + return 0 + global android + if not android: + import android + return android.get_keyboard_height() + + def _get_kivy_vkheight(self): + mode = Config.get('kivy', 'keyboard_mode') + if ( + mode in ['dock', 'systemanddock'] + and self._vkeyboard_cls is not None + ): + for w in self.children: + if isinstance(w, VKeyboard): + vkeyboard_height = w.height * w.scale + if self.softinput_mode == 'pan': + return vkeyboard_height + elif ( + self.softinput_mode == 'below_target' + and w.target.y < vkeyboard_height + ): + return vkeyboard_height - w.target.y + return 0 + + def _get_kheight(self): + if platform == 'android': + return self._get_android_kheight() + elif platform == 'ios': + return self._get_ios_kheight() + return self._get_kivy_vkheight() + + keyboard_height = AliasProperty(_get_kheight, bind=('_keyboard_changed',)) + '''Returns the height of the softkeyboard/IME on mobile platforms. + Will return 0 if not on mobile platform or if IME is not active. + + .. note:: This property returns 0 with SDL2 on Android, but setting + Window.softinput_mode does work. + + .. versionadded:: 1.9.0 + + :attr:`keyboard_height` is a read-only + :class:`~kivy.properties.AliasProperty` and defaults to 0. + ''' + + keyboard_anim_args = {'t': 'in_out_quart', 'd': .5} + '''The attributes for animating softkeyboard/IME. + `t` = `transition`, `d` = `duration`. This value will have no effect on + desktops. + + .. versionadded:: 1.10.0 + + :attr:`keyboard_anim_args` is a dict and defaults to + {'t': 'in_out_quart', 'd': `.5`}. + ''' + + keyboard_padding = NumericProperty(0) + '''The padding to have between the softkeyboard/IME & target + or bottom of window. Will have no effect on desktops. + + .. versionadded:: 1.10.0 + + :attr:`keyboard_padding` is a + :class:`~kivy.properties.NumericProperty` and 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. If the density is + not 1, the :attr:`system_size` is the :attr:`size` divided by + density. + + .. versionadded:: 1.0.9 + + :attr:`system_size` is an :class:`~kivy.properties.AliasProperty`. + ''' + + def _get_effective_size(self): + '''On density=1 and non-ios displays, return :attr:`system_size`, + else return scaled / rotated :attr:`size`. + + Used by MouseMotionEvent.update_graphics() and WindowBase.on_motion(). + ''' + w, h = self.system_size + if platform == 'ios' or self._density != 1: + w, h = self.size + + return w, h + + borderless = BooleanProperty(False) + '''When set to True, this property removes the window border/decoration. + Check the :mod:`~kivy.config` documentation for a more detailed + explanation on the values. + + .. versionadded:: 1.9.0 + + :attr:`borderless` is a :class:`~kivy.properties.BooleanProperty` and + defaults to False. + ''' + + custom_titlebar = BooleanProperty(False) + '''When set to True, allows the user to set a widget as a titlebar. + Check the :mod:`~kivy.config` documentation for a more detailed + explanation on the values. + + .. versionadded:: 2.1.0 + + see :meth:`~kivy.core.window.WindowBase.set_custom_titlebar` + for detailed usage + :attr:`custom_titlebar` is a :class:`~kivy.properties.BooleanProperty` and + 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' and 'fake'. Check the :mod:`~kivy.config` + documentation for more detailed explanations on these values. + + fullscreen is an :class:`~kivy.properties.OptionProperty` and defaults to + `False`. + + .. 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 cursor within the window. + + Position is relative to the left/bottom point of the window. + + .. note:: + Cursor position will be scaled by the pixel density if the high density + mode is supported by the window provider. + + .. versionadded:: 1.2.0 + + :attr:`mouse_pos` is an :class:`~kivy.properties.ObjectProperty` and + defaults to (0, 0). + ''' + + show_cursor = BooleanProperty(True) + '''Set whether or not the cursor is shown on the window. + + .. versionadded:: 1.9.1 + + :attr:`show_cursor` is a :class:`~kivy.properties.BooleanProperty` and + defaults to True. + ''' + + def _get_focus(self): + return self._focus + + focus = AliasProperty(_get_focus, None, bind=('_focus',)) + '''Check whether or not the window currently has focus. + + .. versionadded:: 1.9.1 + + :attr:`focus` is a read-only :class:`~kivy.properties.AliasProperty` and + defaults to True. + ''' + + def _set_cursor_state(self, value): + pass + + def set_system_cursor(self, cursor_name): + '''Set type of a mouse cursor in the Window. + + It can be one of 'arrow', 'ibeam', 'wait', 'crosshair', 'wait_arrow', + 'size_nwse', 'size_nesw', 'size_we', 'size_ns', 'size_all', 'no', or + 'hand'. + + On some platforms there might not be a specific cursor supported and + such an option falls back to one of the substitutable alternatives: + + +------------+-----------+------------+-----------+---------------+ + | | Windows | MacOS | Linux X11 | Linux Wayland | + +============+===========+============+===========+===============+ + | arrow | arrow | arrow | arrow | arrow | + +------------+-----------+------------+-----------+---------------+ + | ibeam | ibeam | ibeam | ibeam | ibeam | + +------------+-----------+------------+-----------+---------------+ + | wait | wait | arrow | wait | wait | + +------------+-----------+------------+-----------+---------------+ + | crosshair | crosshair | crosshair | crosshair | hand | + +------------+-----------+------------+-----------+---------------+ + | wait_arrow | arrow | arrow | wait | wait | + +------------+-----------+------------+-----------+---------------+ + | size_nwse | size_nwse | size_all | size_all | hand | + +------------+-----------+------------+-----------+---------------+ + | size_nesw | size_nesw | size_all | size_all | hand | + +------------+-----------+------------+-----------+---------------+ + | size_we | size_we | size_we | size_we | hand | + +------------+-----------+------------+-----------+---------------+ + | size_ns | size_ns | size_ns | size_ns | hand | + +------------+-----------+------------+-----------+---------------+ + | size_all | size_all | size_all | size_all | hand | + +------------+-----------+------------+-----------+---------------+ + | no | no | no | no | ibeam | + +------------+-----------+------------+-----------+---------------+ + | hand | hand | hand | hand | hand | + +------------+-----------+------------+-----------+---------------+ + + .. versionadded:: 1.10.1 + + .. note:: + This feature requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + pass + + def _get_window_pos(self): + pass + + def _set_window_pos(self, x, y): + pass + + def _get_left(self): + if not self.initialized: + return self._left + return self._get_window_pos()[0] + + def _set_left(self, value): + pos = self._get_window_pos() + self._set_window_pos(value, pos[1]) + + def _get_top(self): + if not self.initialized: + return self._top + return self._get_window_pos()[1] + + def _set_top(self, value): + pos = self._get_window_pos() + self._set_window_pos(pos[0], value) + + top = AliasProperty(_get_top, _set_top) + '''Top position of the window. + + .. note:: It's an SDL2 property with `[0, 0]` in the top-left corner. + + .. versionchanged:: 1.10.0 + :attr:`top` is now an :class:`~kivy.properties.AliasProperty` + + .. versionadded:: 1.9.1 + + :attr:`top` is an :class:`~kivy.properties.AliasProperty` and defaults to + the position set in :class:`~kivy.config.Config`. + ''' + + left = AliasProperty(_get_left, _set_left) + '''Left position of the window. + + .. note:: It's an SDL2 property with `[0, 0]` in the top-left corner. + + .. versionchanged:: 1.10.0 + :attr:`left` is now an :class:`~kivy.properties.AliasProperty` + + .. versionadded:: 1.9.1 + + :attr:`left` is an :class:`~kivy.properties.AliasProperty` and defaults to + the position set in :class:`~kivy.config.Config`. + ''' + + @property + def __self__(self): + return self + + position = OptionProperty('auto', options=['auto', 'custom']) + render_context = ObjectProperty(None) + canvas = ObjectProperty(None) + title = StringProperty('Kivy') + + event_managers = None + '''Holds a `list` of registered event managers. + + Don't change the property directly but use + :meth:`register_event_manager` and :meth:`unregister_event_manager` to + register and unregister an event manager. + + Event manager is an instance of + :class:`~kivy.eventmanager.EventManagerBase`. + + .. versionadded:: 2.1.0 + + .. warning:: + This is an experimental property and it remains so while this warning + is present. + ''' + + event_managers_dict = None + '''Holds a `dict` of `type_id` to `list` of event managers. + + Don't change the property directly but use + :meth:`register_event_manager` and :meth:`unregister_event_manager` to + register and unregister an event manager. + + Event manager is an instance of + :class:`~kivy.eventmanager.EventManagerBase`. + + .. versionadded:: 2.1.0 + + .. warning:: + This is an experimental property and it remains so while this warning + is present. + ''' + + trigger_create_window = None + + __events__ = ( + 'on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_move', + 'on_close', 'on_minimize', 'on_maximize', 'on_restore', + 'on_hide', 'on_show', '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_drop_begin', 'on_drop_file', + 'on_dropfile', 'on_drop_text', 'on_drop_end', 'on_request_close', + 'on_cursor_enter', 'on_cursor_leave', 'on_joy_axis', + 'on_joy_hat', 'on_joy_ball', 'on_joy_button_down', + 'on_joy_button_up', 'on_memorywarning', 'on_textedit', + # internal + 'on_pre_resize') + + 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.event_managers = [] + self.event_managers_dict = defaultdict(list) + 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) + self.bind(_kheight=lambda *args: self.update_viewport()) + + # set the default window parameter according to the configuration + if 'borderless' not in kwargs: + kwargs['borderless'] = Config.getboolean('graphics', 'borderless') + if 'custom_titlebar' not in kwargs: + kwargs['custom_titlebar'] = Config.getboolean('graphics', + 'custom_titlebar') + if 'fullscreen' not in kwargs: + fullscreen = Config.get('graphics', 'fullscreen') + if fullscreen not in ('auto', 'fake'): + fullscreen = fullscreen.lower() in ('true', '1', 'yes') + 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 'minimum_width' not in kwargs: + kwargs['minimum_width'] = Config.getint('graphics', + 'minimum_width') + if 'minimum_height' not in kwargs: + kwargs['minimum_height'] = Config.getint('graphics', + 'minimum_height') + if 'allow_screensaver' not in kwargs: + kwargs['allow_screensaver'] = Config.getboolean( + 'graphics', 'allow_screensaver') + 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' + self._top = kwargs['top'] + else: + self._top = Config.getint('graphics', 'top') + if 'left' in kwargs: + kwargs['position'] = 'custom' + self._left = kwargs['left'] + else: + self._left = Config.getint('graphics', 'left') + kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) + if 'show_cursor' not in kwargs: + kwargs['show_cursor'] = Config.getboolean( + 'graphics', 'show_cursor' + ) + if 'shape_image' not in kwargs: + kwargs['shape_image'] = Config.get('kivy', 'window_shape') + + self.fbind( + 'on_drop_file', + lambda win, filename, *args: win.dispatch('on_dropfile', filename) + ) + 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()) + + self.bind(show_cursor=lambda *dt: self._set_cursor_state(dt[1])) + + # 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() + self.register() + + # manage keyboard(s) + self.configure_keyboards() + + # assign the default context of the widget creation + if not hasattr(self, '_context'): + self._context = get_current_context() + + # because Window is created as soon as imported, if we bound earlier, + # metrics would be imported when dp is set during window creation. + # Instead, don't process dpi changes until everything is set + self.fbind('dpi', self._reset_metrics_dpi) + + # mark as initialized + self.initialized = True + + def _reset_metrics_dpi(self, *args): + from kivy.metrics import Metrics + Metrics.reset_dpi() + + 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 register(self): + if self.initialized: + return + # attach modules + listener event + EventLoop.set_window(self) + Modules.register_window(self) + EventLoop.add_event_listener(self) + + def register_event_manager(self, manager): + '''Register and start an event manager to handle events declared in + :attr:`~kivy.eventmanager.EventManagerBase.type_ids` attribute. + + .. versionadded:: 2.1.0 + + .. warning:: + This is an experimental method and it remains so until this warning + is present as it can be changed or removed in the next versions of + Kivy. + ''' + self.event_managers.insert(0, manager) + for type_id in manager.type_ids: + self.event_managers_dict[type_id].insert(0, manager) + manager.window = self + manager.start() + + def unregister_event_manager(self, manager): + '''Unregister and stop an event manager previously registered with + :meth:`register_event_manager`. + + .. versionadded:: 2.1.0 + + .. warning:: + This is an experimental method and it remains so until this warning + is present as it can be changed or removed in the next versions of + Kivy. + ''' + self.event_managers.remove(manager) + for type_id in manager.type_ids: + self.event_managers_dict[type_id].remove(manager) + manager.stop() + manager.window = None + + def mainloop(self): + '''Called by the EventLoop every frame after it idles. + ''' + pass + + @deprecated + 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 requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + 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 requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + 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 requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + 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 requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + 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 requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + Logger.warning('Window: show() is not implemented in the current ' + 'window provider.') + + def raise_window(self): + '''Raise the window. This method should be used on desktop + platforms only. + + .. versionadded:: 1.9.1 + + .. note:: + This feature requires the SDL2 window provider and is currently + only supported on desktop platforms. + ''' + Logger.warning('Window: raise_window is not implemented in the current' + ' window provider.') + + def close(self): + '''Close the window''' + self.dispatch('on_close') + + # Prevent any leftover that can crash the app later + # like if there is still some GL referenced values + # they may be collected later, but because it was already + # gone in the system, it may collect invalid GL resources + # Just clear everything to force reloading later on. + from kivy.cache import Cache + from kivy.graphics.context import get_context + Cache.remove('kv.loader') + Cache.remove('kv.image') + Cache.remove('kv.shader') + Cache.remove('kv.texture') + get_context().flush() + + shape_image = StringProperty('') + '''An image for the window shape (only works for sdl2 window provider). + + .. warning:: The image size has to be the same like the window's size! + + .. versionadded:: 1.10.1 + + :attr:`shape_image` is a :class:`~kivy.properties.StringProperty` and + defaults to 'data/images/defaultshape.png'. This value is taken from + :class:`~kivy.config.Config`. + ''' + def set_custom_titlebar(self, widget): + """ + Sets a Widget as a titlebar + + :widget: The widget you want to set as the titlebar + + .. versionadded:: 2.1.0 + + This function returns `True` on successfully setting the custom titlebar, + else false + + How to use this feature + + :: + + 1. first set Window.custom_titlebar to True + 2. then call Window.set_custom_titlebar with the widget/layout you want to set as titlebar as the argument # noqa: E501 + + If you want a child of the widget to receive touch events, in + that child define a property `draggable` and set it to False + + If you set the property `draggable` on a layout, + all the child in the layout will receive touch events + + If you want to override default behaviour, add function `in_drag_area(x,y)` + to the widget + + The function is call with two args x,y which are mouse.x, and mouse.y + the function should return + + | `True` if that point should be used to drag the window + | `False` if you want to receive the touch event at the point + + .. note:: + If you use :meth:`in_drag_area` property `draggable` + will not be checked + + .. note:: + This feature requires the SDL2 window provider and is currently + only supported on desktop platforms. + + .. warning:: + :mod:`~kivy.core.window.WindowBase.custom_titlebar` must be set to True + for the widget to be successfully set as a titlebar + + """ + + Logger.warning('Window: set_custom_titlebar ' + 'is not implemented in the current' + ' window provider.') + + def on_shape_image(self, instance, value): + if self.initialized: + self._set_shape( + shape_image=value, mode=self.shape_mode, + cutoff=self.shape_cutoff, color_key=self.shape_color_key + ) + + shape_cutoff = BooleanProperty(True) + '''The window :attr:`shape_image` cutoff property (only works for sdl2 + window provider). + + .. versionadded:: 1.10.1 + + :attr:`shape_cutoff` is a :class:`~kivy.properties.BooleanProperty` and + defaults to True. + ''' + + def on_shape_cutoff(self, instance, value): + self._set_shape( + shape_image=self.shape_image, mode=self.shape_mode, + cutoff=value, color_key=self.shape_color_key + ) + + def _get_shaped(self): + return self._is_shaped() + + shaped = AliasProperty(_get_shaped, None) + '''Read only property to check if the window is shapable or not (only works + for sdl2 window provider). + + .. versionadded:: 1.10.1 + + :attr:`shaped` is an :class:`~kivy.properties.AliasProperty`. + ''' + + def _get_shape_mode(self): + if not self.shaped: + return '' + + i = self._get_shaped_mode()['mode'] + modes = ('default', 'binalpha', 'reversebinalpha', 'colorkey') + return modes[i] + + def _set_shape_mode(self, value): + self._set_shaped_mode(value) + + shape_mode = AliasProperty(_get_shape_mode, _set_shape_mode) + '''Window mode for shaping (only works for sdl2 window provider). + + - can be RGB only + - `default` - does nothing special + - `colorkey` - hides a color of the :attr:`shape_color_key` + - has to contain alpha channel + - `binalpha` - hides an alpha channel of the :attr:`shape_image` + - `reversebinalpha` - shows only the alpha of the :attr:`shape_image` + + .. note:: + Before actually setting the mode make sure the Window has the same + size like the :attr:`shape_image`, preferably via Config before + the Window is actually created. + + If the :attr:`shape_image` isn't set, the default one will be used + and the mode might not take the desired visual effect. + + .. versionadded:: 1.10.1 + + :attr:`shape_mode` is an :class:`~kivy.properties.AliasProperty`. + ''' + + shape_color_key = ColorProperty([1, 1, 1, 1]) + '''Color key of the shaped window - sets which color will be hidden from + the window :attr:`shape_image` (only works for sdl2 window provider). + + .. versionadded:: 1.10.1 + + :attr:`shape_color_key` is a :class:`~kivy.properties.ColorProperty` + instance and defaults to [1, 1, 1, 1]. + + .. versionchanged:: 2.0.0 + Changed from :class:`~kivy.properties.ListProperty` to + :class:`~kivy.properties.ColorProperty`. + ''' + def on_shape_color_key(self, instance, value): + self._set_shape( + shape_image=self.shape_image, mode=self.shape_mode, + cutoff=self.shape_cutoff, color_key=value + ) + + def get_gl_backend_name(self): + """ + Returns the gl backend that will or is used with this window. + """ + return cgl_get_backend_name( + allowed=self.gl_backends_allowed, + ignored=self.gl_backends_ignored) + + def initialize_gl(self): + from kivy.core.gl import init_gl + init_gl(allowed=self.gl_backends_allowed, + ignored=self.gl_backends_ignored) + + 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 + self.trigger_create_window.cancel() + + # ensure the window creation will not be called twice + if platform in ('android', 'ios'): + self._unbind_create_window() + + if not self.initialized: + self.initialize_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_pre_resize', *self.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_pre_resize', *self.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''' + if widget.parent: + from kivy.uix.widget import WidgetException + raise WidgetException( + 'Cannot add %r to window, it already has a parent %r' % + (widget, widget.parent) + ) + + 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_hint_max=self._update_childsize, + size_hint_min=self._update_childsize, + size=self._update_childsize, + pos=self._update_childsize) + + def remove_widget(self, widget): + '''Remove a widget from a window + ''' + if widget not 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_hint_max=self._update_childsize, + size_hint_min=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 import opengl as gl + gl.glClearColor(*self.clearcolor) + gl.glClear( + gl.GL_COLOR_BUFFER_BIT + | gl.GL_DEPTH_BUFFER_BIT + | gl.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 to_normalized_pos(self, x, y): + '''Transforms absolute coordinates to normalized (0-1) coordinates + using :attr:`system_size`. + + .. versionadded:: 2.1.0 + ''' + x_max = self.system_size[0] - 1.0 + y_max = self.system_size[1] - 1.0 + return ( + x / x_max if x_max > 0 else 0.0, + y / y_max if y_max > 0 else 0.0 + ) + + def transform_motion_event_2d(self, me, widget=None): + '''Transforms the motion event `me` to this window size and then if + `widget` is passed transforms `me` to `widget`'s local coordinates. + + :raises: + `AttributeError`: If widget's ancestor is ``None``. + + .. note:: + Unless it's a specific case, call + :meth:`~kivy.input.motionevent.MotionEvent.push` before and + :meth:`~kivy.input.motionevent.MotionEvent.pop` after this method's + call to preserve previous values of `me`'s attributes. + + .. versionadded:: 2.1.0 + ''' + width, height = self._get_effective_size() + me.scale_for_screen( + width, height, + rotation=self.rotation, + smode=self.softinput_mode, + kheight=self.keyboard_height + ) + if widget is not None: + parent = widget.parent + try: + if parent: + me.apply_transform_2d(parent.to_widget) + else: + me.apply_transform_2d(widget.to_widget) + me.apply_transform_2d(widget.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. + raise + + 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" or "end". + `me`: :class:`~kivy.input.motionevent.MotionEvent` + The motion event currently dispatched. + + .. versionchanged:: 2.1.0 + Event managers get to handle the touch event first and if none of + them accepts the event (by returning `True`) then window will + dispatch `me` through "on_touch_down", "on_touch_move", + "on_touch_up" events depending on the `etype`. All non-touch events + will go only through managers. + ''' + accepted = False + for manager in self.event_managers_dict[me.type_id][:]: + accepted = manager.dispatch(etype, me) or accepted + if accepted: + if me.is_touch and etype == 'end': + FocusBehavior._handle_post_on_touch_up(me) + return accepted + if me.is_touch: + # TODO: Use me.push/me.pop methods because `me` is transformed + # Clock execution of partial ScrollView._on_touch_up method and + # other similar cases should be changed so that me.push/me.pop can + # be used restore previous values of event's attributes + # me.push() + self.transform_motion_event_2d(me) + 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) + # me.pop() + + 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_pre_resize(self, width, height): + key = (width, height) + if hasattr(self, '_last_resize') and self._last_resize == key: + return + self._last_resize = key + self.dispatch('on_resize', width, height) + + def on_resize(self, width, height): + '''Event called when the window is resized.''' + self.update_viewport() + + def on_move(self): + self.property('top').dispatch(self) + self.property('left').dispatch(self) + + def update_viewport(self): + from kivy.graphics.opengl import glViewport + from kivy.graphics.transformation import Matrix + from math import radians + + w, h = self._get_effective_size() + + smode = self.softinput_mode + target = self._system_keyboard.target + targettop = max(0, target.to_window(0, target.y)[1]) if target else 0 + kheight = self._kheight + + w2, h2 = w / 2., h / 2. + r = radians(self.rotation) + + y = 0 + _h = h + if smode == 'pan': + y = kheight + elif smode == 'below_target': + y = 0 if kheight < targettop else (kheight - targettop) + if smode == 'scale': + _h -= kheight + + # prepare the viewport + glViewport(0, 0, 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. - y + modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) + self.render_context['modelview_mat'] = modelview_mat + frag_modelview_mat = Matrix() + frag_modelview_mat.set(flat=modelview_mat.get()) + self.render_context['frag_modelview_mat'] = frag_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 + shw_min, shh_min = w.size_hint_min + shw_max, shh_max = w.size_hint_max + + if shw is not None and shh is not None: + c_w = shw * width + c_h = shh * height + + if shw_min is not None and c_w < shw_min: + c_w = shw_min + elif shw_max is not None and c_w > shw_max: + c_w = shw_max + + if shh_min is not None and c_h < shh_min: + c_h = shh_min + elif shh_max is not None and c_h > shh_max: + c_h = shh_max + w.size = c_w, c_h + elif shw is not None: + c_w = shw * width + + if shw_min is not None and c_w < shw_min: + c_w = shw_min + elif shw_max is not None and c_w > shw_max: + c_w = shw_max + w.width = c_w + elif shh is not None: + c_h = shh * height + + if shh_min is not None and c_h < shh_min: + c_h = shh_min + elif shh_max is not None and c_h > shh_max: + c_h = shh_max + w.height = c_h + + 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 to 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_minimize(self, *largs): + '''Event called when the window is minimized. + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def on_maximize(self, *largs): + '''Event called when the window is maximized. + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def on_restore(self, *largs): + '''Event called when the window is restored. + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def on_hide(self, *largs): + '''Event called when the window is hidden. + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def on_show(self, *largs): + '''Event called when the window is shown. + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + 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_cursor_enter(self, *largs): + '''Event called when the cursor enters the window. + + .. versionadded:: 1.9.1 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def on_cursor_leave(self, *largs): + '''Event called when the cursor leaves the window. + + .. versionadded:: 1.9.1 + + .. note:: + This feature requires the SDL2 window provider. + ''' + 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, xvalue, yvalue): + '''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 when text: i.e. alpha numeric non control keys or set + of keys is entered. As it is not guaranteed whether we get one + character or multiple ones, this event supports handling multiple + characters. + + .. versionadded:: 1.9.0 + ''' + pass + + def on_drop_begin(self, x, y, *args): + '''Event called when a text or a file drop on the application is about + to begin. It will be followed-up by a single or a multiple + `on_drop_text` or `on_drop_file` events ending with an `on_drop_end` + event. + + Arguments `x` and `y` are the mouse cursor position at the time of the + drop and you should only rely on them if the drop originated from the + mouse. + + :Parameters: + `x`: `int` + Cursor x position, relative to the window :attr:`left`, at the + time of the drop. + `y`: `int` + Cursor y position, relative to the window :attr:`top`, at the + time of the drop. + `*args`: `tuple` + Additional arugments. + + .. note:: + This event works with sdl2 window provider. + + .. versionadded:: 2.1.0 + ''' + pass + + def on_drop_file(self, filename, x, y, *args): + '''Event called when a file is dropped on the application. + + Arguments `x` and `y` are the mouse cursor position at the time of the + drop and you should only rely on them if the drop originated from the + mouse. + + :Parameters: + `filename`: `bytes` + Absolute path to a dropped file. + `x`: `int` + Cursor x position, relative to the window :attr:`left`, at the + time of the drop. + `y`: `int` + Cursor y position, relative to the window :attr:`top`, at the + time of the drop. + `*args`: `tuple` + Additional arugments. + + .. warning:: + This event currently works with sdl2 window provider, on pygame + window provider and OS X with a patched version of pygame. + This event is left in place for further evolution + (ios, android etc.) + + .. note:: + On Windows it is possible to drop a file on the window title bar + or on its edges and for that case :attr:`mouse_pos` won't be + updated as the mouse cursor is not within the window. + + .. note:: + This event doesn't work for apps with elevated permissions, + because the OS API calls are filtered. Check issue + `#4999 `_ for + pointers to workarounds. + + .. versionadded:: 1.2.0 + + .. versionchanged:: 2.1.0 + Renamed from `on_dropfile` to `on_drop_file`. + ''' + pass + + @deprecated(msg='Deprecated in 2.1.0, use on_drop_file event instead. ' + 'Event on_dropfile will be removed in the next two ' + 'releases.') + def on_dropfile(self, filename): + pass + + def on_drop_text(self, text, x, y, *args): + '''Event called when a text is dropped on the application. + + Arguments `x` and `y` are the mouse cursor position at the time of the + drop and you should only rely on them if the drop originated from the + mouse. + + :Parameters: + `text`: `bytes` + Text which is dropped. + `x`: `int` + Cursor x position, relative to the window :attr:`left`, at the + time of the drop. + `y`: `int` + Cursor y position, relative to the window :attr:`top`, at the + time of the drop. + `*args`: `tuple` + Additional arugments. + + .. note:: + This event works with sdl2 window provider on x11 window. + + .. note:: + On Windows it is possible to drop a text on the window title bar + or on its edges and for that case :attr:`mouse_pos` won't be + updated as the mouse cursor is not within the window. + + .. versionadded:: 2.1.0 + ''' + pass + + def on_drop_end(self, x, y, *args): + '''Event called when a text or a file drop on the application has + ended. + + Arguments `x` and `y` are the mouse cursor position at the time of the + drop and you should only rely on them if the drop originated from the + mouse. + + :Parameters: + `x`: `int` + Cursor x position, relative to the window :attr:`left`, at the + time of the drop. + `y`: `int` + Cursor y position, relative to the window :attr:`top`, at the + time of the drop. + `*args`: `tuple` + Additional arugments. + + .. note:: + This event works with sdl2 window provider. + + .. versionadded:: 2.1.0 + ''' + pass + + def on_memorywarning(self): + '''Event called when the platform have memory issue. + Your goal is to clear the cache in your app as much as you can, + release unused widgets, do garbage collection etc. + + Currently, this event is fired only from the SDL2 provider, for + iOS and Android. + + .. versionadded:: 1.9.0 + ''' + pass + + def on_textedit(self, text): + '''Event called when inputting with IME. + The string inputting with IME is set as the parameter of + this event. + + .. versionadded:: 1.10.1 + ''' + pass + + dpi = NumericProperty(96.) + '''Return the DPI of the screen as computed by the window. If the + implementation doesn't support DPI lookup, it's 96. + + .. warning:: + + This value is not cross-platform. Use + :attr:`kivy.metrics.Metrics.dpi` instead. + ''' + + 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', keyboard_suggestions=True + ): + '''.. 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 + 'null', 'text', 'number', 'url', 'mail', 'datetime', 'tel', + 'address'. + + .. note:: + + `input_type` is currently only honored on Android. + + .. versionadded:: 1.8.0 + + .. versionchanged:: 2.1.0 + Added `null` to soft keyboard types. + + `keyboard_suggestions`: bool + If True provides auto suggestions on top of keyboard. + This will only work if input_type is set to `text`, `url`, + `mail` or `address`. + + .. versionadded:: 2.1.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() + + # sets vkeyboard position according to Window.softinput_mode + if self.softinput_mode == 'pan': + keyboard.widget.top = 0 + elif self.softinput_mode == 'below_target': + keyboard.widget.top = keyboard.target.y + + 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 + + def grab_mouse(self): + '''Grab mouse - so won't leave window + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + def ungrab_mouse(self): + '''Ungrab mouse + + .. versionadded:: 1.10.0 + + .. note:: + This feature requires the SDL2 window provider. + ''' + pass + + +#: Instance of a :class:`WindowBase` implementation +window_impl = [] +if platform == 'linux' and (pi_version or 4) < 4: + 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) diff --git a/kivy/core/window/__pycache__/__init__.cpython-310.pyc b/kivy/core/window/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..26733e7 Binary files /dev/null and b/kivy/core/window/__pycache__/__init__.cpython-310.pyc differ diff --git a/kivy/core/window/__pycache__/window_egl_rpi.cpython-310.pyc b/kivy/core/window/__pycache__/window_egl_rpi.cpython-310.pyc new file mode 100644 index 0000000..6b8744f Binary files /dev/null and b/kivy/core/window/__pycache__/window_egl_rpi.cpython-310.pyc differ diff --git a/kivy/core/window/__pycache__/window_pygame.cpython-310.pyc b/kivy/core/window/__pycache__/window_pygame.cpython-310.pyc new file mode 100644 index 0000000..2bf0ae4 Binary files /dev/null and b/kivy/core/window/__pycache__/window_pygame.cpython-310.pyc differ diff --git a/kivy/core/window/__pycache__/window_sdl2.cpython-310.pyc b/kivy/core/window/__pycache__/window_sdl2.cpython-310.pyc new file mode 100644 index 0000000..75f441f Binary files /dev/null and b/kivy/core/window/__pycache__/window_sdl2.cpython-310.pyc differ diff --git a/kivy/core/window/_window_sdl2.cpython-310-x86_64-linux-gnu.so b/kivy/core/window/_window_sdl2.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..ee2869c Binary files /dev/null and b/kivy/core/window/_window_sdl2.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/window/window_attrs.pxi b/kivy/core/window/window_attrs.pxi new file mode 100644 index 0000000..6dc4daa --- /dev/null +++ b/kivy/core/window/window_attrs.pxi @@ -0,0 +1,30 @@ +include "../../include/config.pxi" + +IF USE_WAYLAND: + cdef extern from "wayland-client-protocol.h": + cdef struct wl_display: + pass + + cdef struct wl_surface: + pass + + cdef struct wl_shell_surface: + pass + +IF USE_X11: + cdef extern from "X11/Xlib.h": + cdef struct _XDisplay: + pass + + ctypedef _XDisplay Display + + ctypedef int XID + ctypedef XID Window + +IF UNAME_SYSNAME == 'Windows': + cdef extern from "windows.h": + ctypedef void *HANDLE + + ctypedef HANDLE HWND + ctypedef HANDLE HDC + ctypedef HANDLE HINSTANCE diff --git a/kivy/core/window/window_egl_rpi.py b/kivy/core/window/window_egl_rpi.py new file mode 100644 index 0000000..0cc2deb --- /dev/null +++ b/kivy/core/window/window_egl_rpi.py @@ -0,0 +1,86 @@ +''' +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, ExceptionManager, stopTouchApp +from kivy.lib.vidcore_lite import bcm, egl +from os import environ + +# Default display IDs. +(DISPMANX_ID_MAIN_LCD, + DISPMANX_ID_AUX_LCD, + DISPMANX_ID_HDMI, + DISPMANX_ID_SDTV, + DISPMANX_ID_FORCE_LCD, + DISPMANX_ID_FORCE_TV, + DISPMANX_ID_FORCE_OTHER) = range(7) + + +class WindowEglRpi(WindowBase): + + _rpi_dispmanx_id = int(environ.get("KIVY_BCM_DISPMANX_ID", "0")) + _rpi_dispmanx_layer = int(environ.get("KIVY_BCM_DISPMANX_LAYER", "0")) + + gl_backends_ignored = ['sdl2'] + + def create_window(self): + bcm.host_init() + + w, h = bcm.graphics_get_display_size(self._rpi_dispmanx_id) + 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(self._rpi_dispmanx_id) + update = egl.bcm_update_start(0) + element = egl.bcm_element_add( + update, display, self._rpi_dispmanx_layer, 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): + if not EventLoop.quit: + egl.SwapBuffers(self.egl_info[0], self.egl_info[1]) diff --git a/kivy/core/window/window_info.cpython-310-x86_64-linux-gnu.so b/kivy/core/window/window_info.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..5230f05 Binary files /dev/null and b/kivy/core/window/window_info.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/core/window/window_info.pxd b/kivy/core/window/window_info.pxd new file mode 100644 index 0000000..0fb8ff9 --- /dev/null +++ b/kivy/core/window/window_info.pxd @@ -0,0 +1,19 @@ +include "window_attrs.pxi" + +from libc.stdint cimport uintptr_t + +IF USE_WAYLAND: + cdef class WindowInfoWayland: + cdef wl_display *display + cdef wl_surface *surface + cdef wl_shell_surface *shell_surface + +IF USE_X11: + cdef class WindowInfoX11: + cdef Display *display + cdef Window window + +IF UNAME_SYSNAME == 'Windows': + cdef class WindowInfoWindows: + cdef HWND window + cdef HDC hdc diff --git a/kivy/core/window/window_pygame.py b/kivy/core/window/window_pygame.py new file mode 100644 index 0000000..2ee5f80 --- /dev/null +++ b/kivy/core/window/window_pygame.py @@ -0,0 +1,449 @@ +''' +Window Pygame: windowing provider based on Pygame + +.. warning:: + + Pygame has been deprecated and will be removed in the release after Kivy + 1.11.0. +''' + +__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 + +try: + android = None + if platform == 'android': + import android +except ImportError: + pass + +# late binding +glReadPixels = GL_RGBA = GL_UNSIGNED_BYTE = None + + +class WindowPygame(WindowBase): + + @deprecated( + msg='Pygame has been deprecated and will be removed after 1.11.0') + def __init__(self, *largs, **kwargs): + super(WindowPygame, self).__init__(*largs, **kwargs) + + 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 'SDL_VIDEO_FULLSCREEN_HEAD' not 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) + + if pygame.RESIZABLE & self.flags: + self._pygame_set_mode() + + 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 + self._set_cursor_state(self.show_cursor) + + # if we are on android platform, automatically create hooks + if android: + from kivy.support import install_android + install_android() + + def close(self): + pygame.display.quit() + super(WindowPygame, self).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 _set_cursor_state(self, value): + pygame.mouse.set_visible(value) + + 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) + 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): + 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: + drop_x, drop_y = pygame.mouse.get_pos() + self.dispatch('on_drop_file', event.filename, drop_x, drop_y) + + ''' + # unhandled event ! + else: + Logger.debug('WinPygame: Unhandled event %s' % str(event)) + ''' + if not pygame.display.get_active(): + pygame.time.wait(100) + + # + # 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_suggestions=True + ): + keyboard = super(WindowPygame, self).request_keyboard( + callback, target, input_type, keyboard_suggestions) + 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 diff --git a/kivy/core/window/window_sdl2.py b/kivy/core/window/window_sdl2.py new file mode 100644 index 0000000..76d60a1 --- /dev/null +++ b/kivy/core/window/window_sdl2.py @@ -0,0 +1,986 @@ +# 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__ = ('WindowSDL', ) + +from os.path import join +import sys +from typing import Optional +from kivy import kivy_data_dir +from kivy.logger import Logger +from kivy.base import EventLoop +from kivy.clock import Clock +from kivy.config import Config +from kivy.core.window import WindowBase +try: + from kivy.core.window._window_sdl2 import _WindowSDL2Storage +except ImportError: + from kivy.core import handle_win_lib_import_error + handle_win_lib_import_error( + 'window', 'sdl2', 'kivy.core.window._window_sdl2') + raise +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 + + +# SDL_keycode.h, https://wiki.libsdl.org/SDL_Keymod +KMOD_NONE = 0x0000 +KMOD_LSHIFT = 0x0001 +KMOD_RSHIFT = 0x0002 +KMOD_LCTRL = 0x0040 +KMOD_RCTRL = 0x0080 +KMOD_LALT = 0x0100 +KMOD_RALT = 0x0200 +KMOD_LGUI = 0x0400 +KMOD_RGUI = 0x0800 +KMOD_NUM = 0x1000 +KMOD_CAPS = 0x2000 +KMOD_MODE = 0x4000 + +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_KP_DIVIDE = 1073741908 +SDLK_KP_MULTIPLY = 1073741909 +SDLK_KP_MINUS = 1073741910 +SDLK_KP_PLUS = 1073741911 +SDLK_KP_ENTER = 1073741912 +SDLK_KP_1 = 1073741913 +SDLK_KP_2 = 1073741914 +SDLK_KP_3 = 1073741915 +SDLK_KP_4 = 1073741916 +SDLK_KP_5 = 1073741917 +SDLK_KP_6 = 1073741918 +SDLK_KP_7 = 1073741919 +SDLK_KP_8 = 1073741920 +SDLK_KP_9 = 1073741921 +SDLK_KP_0 = 1073741922 +SDLK_KP_DOT = 1073741923 +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 __init__(self, *args, **kwargs): + kwargs.setdefault('is_touch', True) + kwargs.setdefault('type_id', 'touch') + super().__init__(*args, **kwargs) + self.profile = ('pos', 'pressure') + + def depack(self, args): + self.sx, self.sy, self.pressure = args + super().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, pressure = value + y = 1 - y + if fid not in touchmap: + touchmap[fid] = me = SDL2MotionEvent( + 'sdl', fid, (x, y, pressure) + ) + else: + me = touchmap[fid] + me.move((x, y, pressure)) + 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): + + _win_dpi_watch: Optional['_WindowsSysDPIWatch'] = None + + _do_resize_ev = None + + managed_textinput = True + + def __init__(self, **kwargs): + self._pause_loop = False + self._cursor_entered = False + self._drop_pos = None + self._win = _WindowSDL2Storage() + super(WindowSDL, self).__init__() + self.titlebar_widget = None + self._mouse_x = self._mouse_y = -1 + self._meta_keys = ( + KMOD_LCTRL, KMOD_RCTRL, KMOD_RSHIFT, + KMOD_LSHIFT, KMOD_RALT, KMOD_LALT, KMOD_LGUI, + KMOD_RGUI, KMOD_NUM, KMOD_CAPS, KMOD_MODE) + 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() + self.key_map = {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, SDLK_KP_DIVIDE: 267, + SDLK_KP_MULTIPLY: 268, SDLK_KP_MINUS: 269, + SDLK_KP_PLUS: 270, SDLK_KP_ENTER: 271, + SDLK_KP_DOT: 266, SDLK_KP_0: 256, SDLK_KP_1: 257, + SDLK_KP_2: 258, SDLK_KP_3: 259, SDLK_KP_4: 260, + SDLK_KP_5: 261, SDLK_KP_6: 262, SDLK_KP_7: 263, + SDLK_KP_8: 264, SDLK_KP_9: 265} + if platform == 'ios': + # XXX ios keyboard suck, when backspace is hit, the delete + # keycode is sent. fix it. + self.key_map[127] = 8 + elif platform == 'android': + # map android back button to escape + self.key_map[1073742094] = 27 + + self.bind(minimum_width=self._set_minimum_size, + minimum_height=self._set_minimum_size) + + self.bind(allow_screensaver=self._set_allow_screensaver) + + def get_window_info(self): + return self._win.get_window_info() + + def _set_minimum_size(self, *args): + minimum_width = self.minimum_width + minimum_height = self.minimum_height + if minimum_width and minimum_height: + self._win.set_minimum_size(minimum_width, minimum_height) + elif minimum_width or minimum_height: + Logger.warning( + 'Both Window.minimum_width and Window.minimum_height must be ' + 'bigger than 0 for the size restriction to take effect.') + + def _set_allow_screensaver(self, *args): + self._win.set_allow_screensaver(self.allow_screensaver) + + def _event_filter(self, action, *largs): + from kivy.app import App + if action == 'app_terminating': + EventLoop.quit = True + + elif action == 'app_lowmemory': + self.dispatch('on_memorywarning') + + elif action == 'app_willenterbackground': + from kivy.base import stopTouchApp + app = App.get_running_app() + if not app: + Logger.info('WindowSDL: No running App found, pause.') + + elif not app.dispatch('on_pause'): + Logger.info( + 'WindowSDL: App doesn\'t support pause mode, stop.') + stopTouchApp() + return 0 + + self._pause_loop = True + + elif action == 'app_didenterforeground': + # on iOS, the did enter foreground is launched at the start + # of the application. in our case, we want it only when the app + # is resumed + if self._pause_loop: + self._pause_loop = False + app = App.get_running_app() + if app: + app.dispatch('on_resume') + + elif action == 'windowresized': + self._size = largs + self._win.resize_window(*self._size) + # Force kivy to render the frame now, so that the canvas is drawn. + EventLoop.idle() + + return 0 + + 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.custom_titlebar = \ + self.borderless = self._fake_fullscreen = False + elif self.custom_titlebar: + if platform == 'win': + # use custom behaviour + # To handle aero snapping and rounded corners + self.borderless = 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 + + # ensure we have an event filter + self._win.set_event_filter(self._event_filter) + + # setup window + 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, + self.get_gl_backend_name()) + + # calculate density/dpi + if platform == 'win': + from ctypes import windll + self._density = 1. + try: + hwnd = windll.user32.GetActiveWindow() + self.dpi = float(windll.user32.GetDpiForWindow(hwnd)) + except AttributeError: + pass + else: + 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) + self._set_minimum_size() + self._set_allow_screensaver() + + if state == 'hidden': + self._focus = False + else: + w, h = self.system_size + self._win.resize_window(w, h) + if platform == 'win': + if self.custom_titlebar: + # check dragging+resize or just dragging + if Config.getboolean('graphics', 'resizable'): + import win32con + import ctypes + self._win.set_border_state(False) + # make windows dispatch, + # WM_NCCALCSIZE explicitly + ctypes.windll.user32.SetWindowPos( + self._win.get_window_info().window, + win32con.HWND_TOP, + *self._win.get_window_pos(), + *self.system_size, + win32con.SWP_FRAMECHANGED + ) + else: + self._win.set_border_state(True) + else: + self._win.set_border_state(self.borderless) + else: + self._win.set_border_state(self.borderless + or self.custom_titlebar) + self._win.set_fullscreen_mode(self.fullscreen) + + super(WindowSDL, self).create_window() + # set mouse visibility + self._set_cursor_state(self.show_cursor) + + 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') + + if platform == 'win' and self._win_dpi_watch is None: + self._win_dpi_watch = _WindowsSysDPIWatch(window=self) + self._win_dpi_watch.start() + + def close(self): + self._win.teardown_window() + super(WindowSDL, self).close() + if self._win_dpi_watch is not None: + self._win_dpi_watch.stop() + self._win_dpi_watch = None + + self.initialized = False + + 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.') + + def raise_window(self): + if self._is_desktop: + self._win.raise_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 set_system_cursor(self, cursor_name): + result = self._win.set_system_cursor(cursor_name) + return result + + def _get_window_pos(self): + return self._win.get_window_pos() + + def _set_window_pos(self, x, y): + self._win.set_window_pos(x, y) + + # Transparent Window background + def _is_shaped(self): + return self._win.is_window_shaped() + + def _set_shape(self, shape_image, mode='default', + cutoff=False, color_key=None): + modes = ('default', 'binalpha', 'reversebinalpha', 'colorkey') + color_key = color_key or (0, 0, 0, 1) + if mode not in modes: + Logger.warning( + 'Window: shape mode can be only ' + '{}'.format(', '.join(modes)) + ) + return + if not isinstance(color_key, (tuple, list)): + return + if len(color_key) not in (3, 4): + return + if len(color_key) == 3: + color_key = (color_key[0], color_key[1], color_key[2], 1) + Logger.warning( + 'Window: Shape color_key must be only tuple or list' + ) + return + color_key = ( + color_key[0] * 255, + color_key[1] * 255, + color_key[2] * 255, + color_key[3] * 255 + ) + + assert cutoff in (1, 0) + shape_image = shape_image or Config.get('kivy', 'window_shape') + shape_image = resource_find(shape_image) or shape_image + self._win.set_shape(shape_image, mode, cutoff, color_key) + + def _get_shaped_mode(self): + return self._win.get_shaped_mode() + + def _set_shaped_mode(self, value): + self._set_shape( + shape_image=self.shape_image, + mode=value, cutoff=self.shape_cutoff, + color_key=self.shape_color_key + ) + return self._win.get_shaped_mode() + # twb end + + def _set_cursor_state(self, value): + self._win._set_cursor_state(value) + + def _fix_mouse_pos(self, x, y): + self.mouse_pos = ( + x * self._density, + (self.system_size[1] - 1 - y) * self._density + ) + return x, y + + def mainloop(self): + # for android/iOS, we don't want to have any event nor executing our + # main loop while the pause is going on. This loop wait any event (not + # handled by the event filter), and remove them from the queue. + # Nothing happen during the pause on iOS, except gyroscope value sent + # over joystick. So it's safe. + while self._pause_loop: + self._win.wait_event() + if not self._pause_loop: + break + event = self._win.poll() + if event is None: + continue + # A drop is send while the app is still in pause.loop + # we need to dispatch it + action, args = event[0], event[1:] + if action.startswith('drop'): + self._dispatch_drop_event(action, args) + # app_terminating event might be received while the app is paused + # in this case EventLoop.quit will be set at _event_filter + elif EventLoop.quit: + return + + 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': + if self.dispatch('on_request_close'): + continue + EventLoop.quit = True + 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 in ('ios', 'android'): + 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 + if not self._cursor_entered: + self._cursor_entered = True + self.dispatch('on_cursor_enter') + # 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) + self._mouse_x = x + self._mouse_y = y + if not self._cursor_entered: + self._cursor_entered = True + self.dispatch('on_cursor_enter') + btn = 'left' + if button == 3: + btn = 'right' + elif button == 2: + btn = 'middle' + elif button == 4: + btn = "mouse4" + elif button == 5: + btn = "mouse5" + eventname = 'on_mouse_down' + self._mouse_buttons_down.add(button) + if action == 'mousebuttonup': + eventname = 'on_mouse_up' + self._mouse_buttons_down.remove(button) + self.dispatch(eventname, x, y, btn, self.modifiers) + elif action.startswith('mousewheel'): + x, y = self._win.get_relative_mouse_pos() + if not self._collide_and_dispatch_cursor_enter(x, y): + # Ignore if the cursor position is on the window title bar + # or on its edges + continue + 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.startswith('drop'): + self._dispatch_drop_event(action, args) + # video resize + elif action == 'windowresized': + self._size = self._win.window_size + # don't use trigger here, we want to delay the resize event + ev = self._do_resize_ev + if ev is None: + ev = Clock.schedule_once(self._do_resize, .1) + self._do_resize_ev = ev + else: + ev() + + elif action == 'windowmoved': + self.dispatch('on_move') + + elif action == 'windowrestored': + self.dispatch('on_restore') + self.canvas.ask_update() + + elif action == 'windowexposed': + self.canvas.ask_update() + + elif action == 'windowminimized': + self.dispatch('on_minimize') + if Config.getboolean('kivy', 'pause_on_minimize'): + self.do_pause() + + elif action == 'windowmaximized': + self.dispatch('on_maximize') + + elif action == 'windowhidden': + self.dispatch('on_hide') + + elif action == 'windowshown': + self.dispatch('on_show') + + elif action == 'windowfocusgained': + self._focus = True + + elif action == 'windowfocuslost': + self._focus = False + + elif action == 'windowenter': + x, y = self._win.get_relative_mouse_pos() + self._collide_and_dispatch_cursor_enter(x, y) + + elif action == 'windowleave': + self._cursor_entered = False + self.dispatch('on_cursor_leave') + + 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 + + try: + key = self.key_map[key] + except KeyError: + pass + + if action == 'keydown': + self._update_modifiers(mod, key) + else: + # ignore the key, it has been released + self._update_modifiers(mod) + + # if mod in self._meta_keys: + if (key not in self._modifiers and + key not in self.command_keys.keys()): + try: + kstr_chr = unichr(key) + try: + # On android, there is no 'encoding' attribute. + # On other platforms, if stdout is redirected, + # 'encoding' may be None + encoding = getattr(sys.stdout, 'encoding', + 'utf8') or 'utf8' + kstr_chr.encode(encoding) + kstr = kstr_chr + except UnicodeError: + pass + 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) + + elif action == 'textedit': + text = args[0] + self.dispatch('on_textedit', text) + + # unhandled event ! + else: + Logger.trace('WindowSDL: Unhandled event %s' % str(event)) + + def _dispatch_drop_event(self, action, args): + x, y = (0, 0) if self._drop_pos is None else self._drop_pos + if action == 'dropfile': + self.dispatch('on_drop_file', args[0], x, y) + elif action == 'droptext': + self.dispatch('on_drop_text', args[0], x, y) + elif action == 'dropbegin': + self._drop_pos = x, y = self._win.get_relative_mouse_pos() + self._collide_and_dispatch_cursor_enter(x, y) + self.dispatch('on_drop_begin', x, y) + elif action == 'dropend': + self._drop_pos = None + self.dispatch('on_drop_end', x, y) + + def _collide_and_dispatch_cursor_enter(self, x, y): + # x, y are relative to window left/top position + w, h = self._win.window_size + if 0 <= x < w and 0 <= y < h: + self._mouse_x, self._mouse_y = self._fix_mouse_pos(x, y) + if not self._cursor_entered: + self._cursor_entered = True + self.dispatch('on_cursor_enter') + return True + + def _do_resize(self, dt): + Logger.debug('Window: Resize window to %s' % str(self.size)) + self._win.resize_window(*self._size) + self.dispatch('on_pre_resize', *self.size) + + def do_pause(self): + # should go to app pause mode (desktop style) + 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, pause.') + elif 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 + break + elif action == 'app_willenterforeground': + break + elif action == 'windowrestored': + break + + if app: + app.dispatch('on_resume') + + def _update_modifiers(self, mods=None, key=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 | KMOD_MODE): + modifiers.add('alt') + if mods & (KMOD_RCTRL | KMOD_LCTRL): + modifiers.add('ctrl') + if mods & (KMOD_RGUI | KMOD_LGUI): + modifiers.add('meta') + if mods & KMOD_NUM: + modifiers.add('numlock') + if mods & KMOD_CAPS: + modifiers.add('capslock') + + if key is not None: + if key in (KMOD_RSHIFT, KMOD_LSHIFT): + modifiers.add('shift') + if key in (KMOD_RALT, KMOD_LALT, KMOD_MODE): + modifiers.add('alt') + if key in (KMOD_RCTRL, KMOD_LCTRL): + modifiers.add('ctrl') + if key in (KMOD_RGUI, KMOD_LGUI): + modifiers.add('meta') + if key == KMOD_NUM: + modifiers.add('numlock') + if key == KMOD_CAPS: + modifiers.add('capslock') + + self._modifiers = list(modifiers) + return + + def request_keyboard( + self, callback, target, input_type='text', keyboard_suggestions=True + ): + self._sdl_keyboard = super(WindowSDL, self).\ + request_keyboard( + callback, target, input_type, keyboard_suggestions + ) + self._win.show_keyboard( + self._system_keyboard, + self.softinput_mode, + input_type, + keyboard_suggestions, + ) + 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() + + def map_key(self, original_key, new_key): + self.key_map[original_key] = new_key + + def unmap_key(self, key): + if key in self.key_map: + del self.key_map[key] + + def grab_mouse(self): + self._win.grab_mouse(True) + + def ungrab_mouse(self): + self._win.grab_mouse(False) + + def set_custom_titlebar(self, titlebar_widget): + if not self.custom_titlebar: + Logger.warning("Window: Window.custom_titlebar not set to True… " + "can't set custom titlebar") + return + self.titlebar_widget = titlebar_widget + return self._win.set_custom_titlebar(self.titlebar_widget) == 0 + + +class _WindowsSysDPIWatch: + + hwnd = None + + new_windProc = None + + old_windProc = None + + window: WindowBase = None + + def __init__(self, window: WindowBase): + self.window = window + + def start(self): + from kivy.input.providers.wm_common import WNDPROC, \ + SetWindowLong_WndProc_wrapper + from ctypes import windll + + self.hwnd = windll.user32.GetActiveWindow() + + # inject our own handler to handle messages before window manager + self.new_windProc = WNDPROC(self._wnd_proc) + self.old_windProc = SetWindowLong_WndProc_wrapper( + self.hwnd, self.new_windProc) + + def stop(self): + from kivy.input.providers.wm_common import \ + SetWindowLong_WndProc_wrapper + + if self.hwnd is None: + return + + self.new_windProc = SetWindowLong_WndProc_wrapper( + self.hwnd, self.old_windProc) + self.hwnd = self.new_windProc = self.old_windProc = None + + def _wnd_proc(self, hwnd, msg, wParam, lParam): + from kivy.input.providers.wm_common import WM_DPICHANGED, WM_NCCALCSIZE + from ctypes import windll + + if msg == WM_DPICHANGED: + ow, oh = self.window.size + old_dpi = self.window.dpi + + def clock_callback(*args): + if x_dpi != y_dpi: + raise ValueError( + 'Can only handle DPI that are same for x and y') + + self.window.dpi = x_dpi + + # maintain the same window size + ratio = x_dpi / old_dpi + self.window.size = ratio * ow, ratio * oh + + x_dpi = wParam & 0xFFFF + y_dpi = wParam >> 16 + Clock.schedule_once(clock_callback, -1) + elif Config.getboolean('graphics', 'resizable') \ + and msg == WM_NCCALCSIZE and self.window.custom_titlebar: + return 0 + return windll.user32.CallWindowProcW( + self.old_windProc, hwnd, msg, wParam, lParam) diff --git a/kivy/core/window/window_x11.cpython-310-x86_64-linux-gnu.so b/kivy/core/window/window_x11.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..8457d41 Binary files /dev/null and b/kivy/core/window/window_x11.cpython-310-x86_64-linux-gnu.so differ diff --git a/kivy/data/fonts/DejaVuSans.ttf b/kivy/data/fonts/DejaVuSans.ttf new file mode 100644 index 0000000..9d40c32 Binary files /dev/null and b/kivy/data/fonts/DejaVuSans.ttf differ diff --git a/kivy/data/fonts/Roboto-Bold.ttf b/kivy/data/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..a355c27 Binary files /dev/null and b/kivy/data/fonts/Roboto-Bold.ttf differ diff --git a/kivy/data/fonts/Roboto-BoldItalic.ttf b/kivy/data/fonts/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..3c9a7a3 Binary files /dev/null and b/kivy/data/fonts/Roboto-BoldItalic.ttf differ diff --git a/kivy/data/fonts/Roboto-Italic.ttf b/kivy/data/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..ff6046d Binary files /dev/null and b/kivy/data/fonts/Roboto-Italic.ttf differ diff --git a/kivy/data/fonts/Roboto-Regular.ttf b/kivy/data/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..8c082c8 Binary files /dev/null and b/kivy/data/fonts/Roboto-Regular.ttf differ diff --git a/kivy/data/fonts/RobotoMono-Regular.ttf b/kivy/data/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000..b158a33 Binary files /dev/null and b/kivy/data/fonts/RobotoMono-Regular.ttf differ diff --git a/kivy/data/glsl/default.fs b/kivy/data/glsl/default.fs new file mode 100644 index 0000000..19145d6 --- /dev/null +++ b/kivy/data/glsl/default.fs @@ -0,0 +1,4 @@ +$HEADER$ +void main (void){ + gl_FragColor = frag_color * texture2D(texture0, tex_coord0); +} diff --git a/kivy/data/glsl/default.png b/kivy/data/glsl/default.png new file mode 100644 index 0000000..308bfe2 Binary files /dev/null and b/kivy/data/glsl/default.png differ diff --git a/kivy/data/glsl/default.vs b/kivy/data/glsl/default.vs new file mode 100644 index 0000000..ac9ac4d --- /dev/null +++ b/kivy/data/glsl/default.vs @@ -0,0 +1,6 @@ +$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); +} diff --git a/kivy/data/glsl/header.fs b/kivy/data/glsl/header.fs new file mode 100644 index 0000000..cd38cc9 --- /dev/null +++ b/kivy/data/glsl/header.fs @@ -0,0 +1,12 @@ +#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; + +uniform mat4 frag_modelview_mat; diff --git a/kivy/data/glsl/header.vs b/kivy/data/glsl/header.vs new file mode 100644 index 0000000..a2638bf --- /dev/null +++ b/kivy/data/glsl/header.vs @@ -0,0 +1,17 @@ +#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; diff --git a/kivy/data/images/background.jpg b/kivy/data/images/background.jpg new file mode 100644 index 0000000..57ab046 Binary files /dev/null and b/kivy/data/images/background.jpg differ diff --git a/kivy/data/images/cursor.png b/kivy/data/images/cursor.png new file mode 100644 index 0000000..4151088 Binary files /dev/null and b/kivy/data/images/cursor.png differ diff --git a/kivy/data/images/defaultshape.png b/kivy/data/images/defaultshape.png new file mode 100644 index 0000000..ae02f8c Binary files /dev/null and b/kivy/data/images/defaultshape.png differ diff --git a/kivy/data/images/defaulttheme-0.png b/kivy/data/images/defaulttheme-0.png new file mode 100644 index 0000000..d72f3b3 Binary files /dev/null and b/kivy/data/images/defaulttheme-0.png differ diff --git a/kivy/data/images/defaulttheme.atlas b/kivy/data/images/defaulttheme.atlas new file mode 100644 index 0000000..7bf086d --- /dev/null +++ b/kivy/data/images/defaulttheme.atlas @@ -0,0 +1 @@ +{"defaulttheme-0.png": {"progressbar_background": [392, 227, 24, 24], "tab_btn_disabled": [332, 137, 32, 32], "tab_btn_pressed": [400, 137, 32, 32], "image-missing": [152, 171, 48, 48], "splitter_h": [174, 123, 32, 7], "splitter_down": [11, 10, 7, 32], "splitter_disabled_down": [2, 10, 7, 32], "vkeyboard_key_down": [2, 44, 32, 32], "vkeyboard_disabled_key_down": [434, 137, 32, 32], "selector_right": [438, 326, 64, 64], "player-background": [2, 287, 103, 103], "selector_middle": [372, 326, 64, 64], "spinner": [204, 82, 29, 37], "tab_btn_disabled_pressed": [366, 137, 32, 32], "switch-button_disabled": [375, 291, 43, 32], "textinput_disabled_active": [134, 221, 64, 64], "splitter_grip": [70, 50, 12, 26], "vkeyboard_key_normal": [36, 44, 32, 32], "button_disabled": [111, 82, 29, 37], "media-playback-stop": [302, 171, 48, 48], "splitter": [502, 137, 7, 32], "splitter_down_h": [140, 123, 32, 7], "sliderh_background_disabled": [115, 132, 41, 37], "modalview-background": [464, 456, 45, 54], "button": [80, 82, 29, 37], "splitter_disabled": [501, 87, 7, 32], "checkbox_radio_disabled_on": [467, 87, 32, 32], "slider_cursor": [352, 171, 48, 48], "vkeyboard_disabled_background": [266, 221, 64, 64], "checkbox_disabled_on": [331, 87, 32, 32], "sliderv_background_disabled": [41, 78, 37, 41], "button_disabled_pressed": [142, 82, 29, 37], "audio-volume-muted": [102, 171, 48, 48], "close": [487, 173, 20, 20], "action_group_disabled": [2, 121, 33, 48], "vkeyboard_background": [200, 221, 64, 64], "checkbox_off": [365, 87, 32, 32], "tab_disabled": [107, 291, 96, 32], "sliderh_background": [72, 132, 41, 37], "switch-button": [430, 253, 43, 32], "tree_closed": [418, 231, 20, 20], "bubble_btn_pressed": [454, 291, 32, 32], "selector_left": [306, 326, 64, 64], "filechooser_file": [174, 326, 64, 64], "checkbox_radio_disabled_off": [433, 87, 32, 32], "checkbox_radio_on": [230, 137, 32, 32], "checkbox_on": [399, 87, 32, 32], "button_pressed": [173, 82, 29, 37], "audio-volume-high": [464, 406, 48, 48], "audio-volume-low": [2, 171, 48, 48], "progressbar": [332, 227, 32, 24], "previous_normal": [488, 291, 19, 32], "separator": [504, 342, 5, 48], "filechooser_folder": [240, 326, 64, 64], "checkbox_radio_off": [196, 137, 32, 32], "textinput_active": [68, 221, 64, 64], "textinput": [2, 221, 64, 64], "player-play-overlay": [122, 395, 117, 115], "media-playback-pause": [202, 171, 48, 48], "sliderv_background": [2, 78, 37, 41], "ring": [354, 402, 108, 108], "bubble_arrow": [490, 241, 16, 10], "slider_cursor_disabled": [402, 171, 48, 48], "checkbox_disabled_off": [297, 87, 32, 32], "action_group_down": [37, 121, 33, 48], "spinner_disabled": [235, 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": [468, 137, 32, 32], "overflow": [264, 137, 32, 32], "tree_opened": [440, 231, 20, 20], "action_item": [487, 195, 24, 24], "bubble_btn": [420, 291, 32, 32], "audio-volume-medium": [52, 171, 48, 48], "action_group": [452, 171, 33, 48], "spinner_pressed": [266, 82, 29, 37], "filechooser_selected": [2, 392, 118, 118], "tab": [332, 253, 96, 32], "action_bar": [158, 133, 36, 36], "action_view": [366, 227, 24, 24], "tab_btn": [298, 137, 32, 32], "switch-background": [205, 291, 83, 32], "splitter_disabled_down_h": [72, 123, 32, 7], "action_item_down": [475, 253, 32, 32], "switch-background_disabled": [290, 291, 83, 32], "textinput_disabled": [241, 399, 111, 111], "splitter_grip_h": [462, 239, 26, 12]}} \ No newline at end of file diff --git a/kivy/data/images/image-loading.gif b/kivy/data/images/image-loading.gif new file mode 100644 index 0000000..859f31b Binary files /dev/null and b/kivy/data/images/image-loading.gif differ diff --git a/kivy/data/images/image-loading.zip b/kivy/data/images/image-loading.zip new file mode 100644 index 0000000..ac98293 Binary files /dev/null and b/kivy/data/images/image-loading.zip differ diff --git a/kivy/data/images/testpattern.png b/kivy/data/images/testpattern.png new file mode 100644 index 0000000..405e140 Binary files /dev/null and b/kivy/data/images/testpattern.png differ diff --git a/kivy/data/keyboards/azerty.json b/kivy/data/keyboards/azerty.json new file mode 100644 index 0000000..61b249b --- /dev/null +++ b/kivy/data/keyboards/azerty.json @@ -0,0 +1,68 @@ +{ + "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] + ] +} diff --git a/kivy/data/keyboards/de.json b/kivy/data/keyboards/de.json new file mode 100644 index 0000000..24598a7 --- /dev/null +++ b/kivy/data/keyboards/de.json @@ -0,0 +1,101 @@ +{ + "title": "de", + "description": "A true 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", "´", 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], ["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], + ["#", "#", "#", 1], ["\u23ce", null, "enter", 1.2] + ], + "normal_4": [ + ["\u21e7", null, "shift", 1.5], ["<", "<", "<", 1], ["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], ["$", "$", "$", 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], ["\u23ce", null, "enter", 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], + ["'", "'", "'", 1], ["\u23ce", null, "enter", 1.2] + ], + "shift_4": [ + ["\u21e7", null, "shift", 1.5], [">", ">", ">", 1], ["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], ["¢", "¢", "¢", 1], ["[", "[", "[", 1], + ["]", "]", "]", 1], ["|", "|", "|", 1], ["{", "{", "{", 1], + ["}", "}", "}", 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], ["\u23ce", null, "enter", 1.5] + ], + "special_3": [ + ["\u21ea", null, "capslock", 1.8], ["æ", "æ", "æ", 1], ["‚", "‚", "‚", 1], + ["∂", "∂", "∂", 1], ["ƒ", "ƒ", "ƒ", 1], ["©", "©", "©", 1], + ["ª", "ª", "ª", 1], ["º", "º", "º", 1], ["∆", "∆", "∆", 1], + ["@", "@", "@", 1], ["œ", "œ", "œ", 1], ["æ", "æ", "æ", 1], + ["‘", "‘", "‘", 1], ["\u23ce", null, "enter", 1.2] + ], + "special_4": [ + ["\u21e7", null, "shift", 1.5], ["≤", "≤", "≤", 1], ["¥", "¥", "¥", 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] + ] +} diff --git a/kivy/data/keyboards/de_CH.json b/kivy/data/keyboards/de_CH.json new file mode 100644 index 0000000..076a35a --- /dev/null +++ b/kivy/data/keyboards/de_CH.json @@ -0,0 +1,98 @@ +{ + "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] + ] +} diff --git a/kivy/data/keyboards/en_US.json b/kivy/data/keyboards/en_US.json new file mode 100644 index 0000000..630684e --- /dev/null +++ b/kivy/data/keyboards/en_US.json @@ -0,0 +1,98 @@ +{ + "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] + ] +} diff --git a/kivy/data/keyboards/fr_CH.json b/kivy/data/keyboards/fr_CH.json new file mode 100644 index 0000000..b158679 --- /dev/null +++ b/kivy/data/keyboards/fr_CH.json @@ -0,0 +1,98 @@ +{ + "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] + ] +} diff --git a/kivy/data/keyboards/qwerty.json b/kivy/data/keyboards/qwerty.json new file mode 100644 index 0000000..d1dac3c --- /dev/null +++ b/kivy/data/keyboards/qwerty.json @@ -0,0 +1,66 @@ +{ + "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] + ] +} diff --git a/kivy/data/keyboards/qwertz.json b/kivy/data/keyboards/qwertz.json new file mode 100644 index 0000000..c8ded5a --- /dev/null +++ b/kivy/data/keyboards/qwertz.json @@ -0,0 +1,66 @@ +{ + "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] + ] +} diff --git a/kivy/data/logo/kivy-icon-128.png b/kivy/data/logo/kivy-icon-128.png new file mode 100644 index 0000000..f51fa3f Binary files /dev/null and b/kivy/data/logo/kivy-icon-128.png differ diff --git a/kivy/data/logo/kivy-icon-16.png b/kivy/data/logo/kivy-icon-16.png new file mode 100644 index 0000000..1c35ca2 Binary files /dev/null and b/kivy/data/logo/kivy-icon-16.png differ diff --git a/kivy/data/logo/kivy-icon-24.png b/kivy/data/logo/kivy-icon-24.png new file mode 100644 index 0000000..bc62449 Binary files /dev/null and b/kivy/data/logo/kivy-icon-24.png differ diff --git a/kivy/data/logo/kivy-icon-256.png b/kivy/data/logo/kivy-icon-256.png new file mode 100644 index 0000000..e5fc31a Binary files /dev/null and b/kivy/data/logo/kivy-icon-256.png differ diff --git a/kivy/data/logo/kivy-icon-32.png b/kivy/data/logo/kivy-icon-32.png new file mode 100644 index 0000000..ced34d3 Binary files /dev/null and b/kivy/data/logo/kivy-icon-32.png differ diff --git a/kivy/data/logo/kivy-icon-48.png b/kivy/data/logo/kivy-icon-48.png new file mode 100644 index 0000000..bfe82a4 Binary files /dev/null and b/kivy/data/logo/kivy-icon-48.png differ diff --git a/kivy/data/logo/kivy-icon-512.png b/kivy/data/logo/kivy-icon-512.png new file mode 100644 index 0000000..1325da4 Binary files /dev/null and b/kivy/data/logo/kivy-icon-512.png differ diff --git a/kivy/data/logo/kivy-icon-64.ico b/kivy/data/logo/kivy-icon-64.ico new file mode 100644 index 0000000..98f36cd Binary files /dev/null and b/kivy/data/logo/kivy-icon-64.ico differ diff --git a/kivy/data/logo/kivy-icon-64.png b/kivy/data/logo/kivy-icon-64.png new file mode 100644 index 0000000..b5571a4 Binary files /dev/null and b/kivy/data/logo/kivy-icon-64.png differ diff --git a/kivy/data/settings_kivy.json b/kivy/data/settings_kivy.json new file mode 100644 index 0000000..b76970f --- /dev/null +++ b/kivy/data/settings_kivy.json @@ -0,0 +1,117 @@ +[ + { + "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" + } +] diff --git a/kivy/data/style.kv b/kivy/data/style.kv new file mode 100644 index 0000000..9e463d8 --- /dev/null +++ b/kivy/data/style.kv @@ -0,0 +1,1327 @@ +#:kivy 1.0 + +