Stav 23.06.2026

This commit is contained in:
2026-06-23 15:20:56 +02:00
commit 6d91e83e8c
5670 changed files with 1145969 additions and 0 deletions
+519
View File
@@ -0,0 +1,519 @@
'''
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 <https://en.wikipedia.org/wiki/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 <http://cython.org/>`_.
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
import importlib
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:
module_spec = importer.find_spec(modname)
mod = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(mod)
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('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:
Config.filename = kivy_config_fn
Config.write()
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()
+131
View File
@@ -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)
+68
View File
@@ -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
@@ -0,0 +1,5 @@
from kivy._event cimport EventObservers
cdef EventObservers pixel_scale_observers
cpdef float dpi2px(value, str ext) except *
+18
View File
@@ -0,0 +1,18 @@
# This file is imported from __init__.py and exec'd from setup.py
MAJOR = 2
MINOR = 3
MICRO = 1
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 = '20d74dcd30f143abbd1aa94c76bafc5bd934d5bd'
_kivy_build_date = '20241226'
+831
View File
@@ -0,0 +1,831 @@
'''
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):
Animation._instances.discard(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
File diff suppressed because it is too large Load Diff
+456
View File
@@ -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 ``<basename>.atlas`` is a json file formatted like this::
{
"<basename>-<index>.png": {
"id1": [ <x>, <y>, <width>, <height> ],
"id2": [ <x>, <y>, <width>, <height> ],
# ...
},
# ...
}
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 <basename> <size> <list of images...>
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'])
<kivy.graphics.texture.TextureRegion object at 0x2404d10>
'''
__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 ``-<idx>.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]] <outname> '
'<size|512x256> <img1.png> [<img2.png>, ...]')
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 <integer>x<integer>')
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'))
+617
View File
@@ -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:
`<empty>`
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()
+262
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+82
View File
@@ -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
File diff suppressed because it is too large Load Diff
+102
View File
@@ -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)
@@ -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 <debug>.cp<major><minor>-<platform>.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))
@@ -0,0 +1,223 @@
'''
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 <kivy.base.EventLoopBase>` 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 <https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop>`_
(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 <https://github.com/kivy/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.'''
__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 <position> (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)
@@ -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)
@@ -0,0 +1,82 @@
'''
AudioAvplayer: implementation of Sound using pyobjus / AVFoundation.
Works on iOS / OSX.
'''
__all__ = ('SoundAvplayer', )
from kivy.core.audio import Sound, SoundLoader
from pyobjus import autoclass, protocol
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.delegate = self
self._avplayer.play()
super(SoundAvplayer, self).play()
def stop(self):
if not self._avplayer:
return
self._avplayer.delegate = None
self._avplayer.stop()
super(SoundAvplayer, self).stop()
def seek(self, position):
if not self._avplayer:
return
avplayer = self._avplayer
avplayer.stop()
avplayer.currentTime = float(position)
if self.state == 'play':
avplayer.play()
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()
@protocol("AVAudioPlayerDelegate")
def audioPlayerDidFinishPlaying_successfully_(self, player, flag):
self.stop()
SoundLoader.register(SoundAvplayer)
@@ -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)
@@ -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)
@@ -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)
@@ -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):
'''Initialize 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 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))
@@ -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()
@@ -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()
@@ -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
@@ -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
@@ -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 initialized.
'''
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')
@@ -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']
@@ -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)
@@ -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']
@@ -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())
@@ -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']
@@ -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',)
@@ -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
@@ -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']
@@ -0,0 +1,107 @@
'''
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
GlobalLock = kernel32.GlobalLock
GlobalLock.argtypes = [wintypes.HGLOBAL]
GlobalLock.restype = wintypes.LPVOID
GlobalUnlock = kernel32.GlobalUnlock
GlobalUnlock.argtypes = [wintypes.HGLOBAL]
GlobalUnlock.restype = wintypes.BOOL
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
class ClipboardWindows(ClipboardBase):
def _copy(self, data):
self._ensure_clipboard()
self.put(data, self._clip_mime_type)
def get(self, mimetype='text/plain'):
GetClipboardData = user32.GetClipboardData
GetClipboardData.argtypes = [wintypes.UINT]
GetClipboardData.restype = wintypes.HANDLE
user32.OpenClipboard(user32.GetActiveWindow())
# GetClipboardData returns a HANDLE to the clipboard data
# which is a memory object containing the data
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata # noqa: E501
pcontents = GetClipboardData(CF_UNICODETEXT)
# 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 ''
# The handle returned by GetClipboardData is a memory object
# and needs to be locked to get the actual pointer to the data
pcontents_locked = GlobalLock(pcontents)
data = c_wchar_p(pcontents_locked).value
GlobalUnlock(pcontents)
user32.CloseClipboard()
return data
def put(self, text, mimetype="text/plain"):
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
user32.OpenClipboard(user32.GetActiveWindow())
user32.EmptyClipboard()
# The wsclen function returns the number of
# wide characters in a string (not including the null character)
text_len = msvcrt.wcslen(text) + 1
# According to the docs regarding SetClipboardDatam, if the hMem
# parameter identifies a memory object, the object must have
# been allocated using the GMEM_MOVEABLE flag.
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata # noqa: E501
# The size of the memory object is the number of wide characters in
# the string plus one for the terminating null character
hCd = GlobalAlloc(
GMEM_MOVEABLE, ctypes.sizeof(ctypes.c_wchar) * text_len
)
# Since the memory object is allocated with GMEM_MOVEABLE, should be
# locked to get the actual pointer to the data.
hCd_locked = GlobalLock(hCd)
ctypes.memmove(
c_wchar_p(hCd_locked),
c_wchar_p(text),
ctypes.sizeof(ctypes.c_wchar) * text_len,
)
GlobalUnlock(hCd)
# Finally, set the clipboard data (and then close the clipboard)
SetClipboardData(CF_UNICODETEXT, hCd)
user32.CloseClipboard()
def get_types(self):
return ['text/plain']
@@ -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)
@@ -0,0 +1,26 @@
'''
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')
import subprocess
p = subprocess.Popen(['xsel', '--version'], stdout=subprocess.PIPE)
p.communicate(timeout=1)
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)
@@ -0,0 +1,90 @@
# 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
@@ -0,0 +1,996 @@
'''
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
from filetype import guess_extension
__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 ('<ImageData width=%d height=%d fmt=%s '
'source=%r with %d images>' % (
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
uid = f'{fname}|{self._mipmap:d}|{count:d}'
texture = Cache.get('kv.texture', uid)
# if not create it and append to the cache
if texture is None:
imagedata = self._data[count]
source = (f"{'zip|' if fname.endswith('.zip') else ''}"
f"{self._nocache}|")
imagedata.source = f'{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 = f'atlas://{rfn}/{uid}'
cid = f'{fn}|0|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 = f'atlas://{rfn}/{nid}'
cid = f'{fn}|0|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)
# Get actual image format instead of extension if possible
ext = guess_extension(filename) or ext
# special case. When we are trying to load a "zip" file with image, we
# will use the special zip_loader in ImageLoader. This might return a
# sequence of images contained in the zip.
if ext == 'zip':
return ImageLoader.zip_loader(filename)
else:
im = None
for loader in ImageLoader.loaders:
if ext not in loader.extensions():
continue
Logger.debug(f'Image{loader.__name__[11:]}: Load <{filename}>')
im = loader(filename, **kwargs)
break
if im is None:
raise Exception(f'Unknown <{ext}> type, no loader found.')
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
uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
Cache.remove("kv.image", uid)
while Cache.get("kv.texture", uid):
Cache.remove("kv.texture", uid)
count += 1
uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
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 = f'{f}|{self._mipmap:d}|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(f'No inline loader found to load {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(f'Position ({x:d}, {y:d}) is out of range.')
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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,893 @@
'''
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 <http://en.wikipedia.org/wiki/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=<str>][/font]``
Change the font (note: this refers to a TTF file or registered alias)
``[font_context=<str>][/font_context]``
Change context for the font, use string value "none" for isolated context.
``[font_family=<str>][/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=<str>][/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>][/size]``
Change the font size. <size> should be an integer, optionally with a
unit (i.e. ``16sp``)
``[color=#<color>][/color]``
Change the text color
``[ref=<str>][/ref]``
Add an interactive zone. The reference + all the word box inside the
reference will be available in :attr:`MarkupLabel.refs`
``[anchor=<str>]``
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=<str>][/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'][0] + options['padding'][2]
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('&amp;', '&')
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 - 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):
padding_left = options['padding'][0]
padding_right = options['padding'][2]
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 = padding_left
if halign == 'center':
x = min(
int(w - lw),
max(
int(padding_left),
int((w - lw + padding_left - padding_right) / 2.0)
)
)
elif halign == 'right' or auto_halign_r:
x = max(0, int(w - lw - padding_right))
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 and (wh < pph)
else self.get_descent())
psp = script_pos
pph = wh
elif options['script'] == 'subscript':
script_pos = min(lh - wh, ((psp + pph) - wh)
if pph and (wh < 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"][0] - old_opts["padding"][2] - margin),
)
chr = type(self.text)
ssize = textwidth(' ')
c = old_opts['split_str']
line_height = old_opts['line_height']
xpad, ypad = (
old_opts["padding"][0] + old_opts["padding"][2],
old_opts["padding"][1] + old_opts["padding"][3],
)
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 + xpad,
lh + 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] + xpad,
s[1] * line_height + ypad,
[LayoutLine(
0, 0, s[0], s[1], 1, 0,
[LayoutWord(old_opts, s[0], s[1], '..')])]
)
else:
s = textwidth('.')
return (
s[0] + xpad,
s[1] * line_height + 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 + xpad,
lh + 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 + xpad,
lh + 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 + xpad,
lh + ypad,
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
)
@@ -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
@@ -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://<PATH>` - `FcInitLoadConfig()` + `FcAppFontAddDir()`
* `fontconfig://<PATH>` - `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('<span font="20">Hello <b>World!</b></span>')
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 collisions 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))
@@ -0,0 +1,84 @@
'''
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):
if self.options['font_size'] < 1:
return None
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()
if not font:
return 0, 0
left, top, right, bottom = font.getbbox(text)
ascent, descent = font.getmetrics()
if self.options['limit_render_to_text_bbox']:
h = bottom - top
else:
h = ascent + descent
w = right - left
return w, h
def get_cached_extents(self):
return self.get_extents
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):
font = self._select_font()
if not font:
return
color = tuple([int(c * 255) for c in self.options['color']])
# Adjust x and y position to avoid text cutoff
if self.options['limit_render_to_text_bbox']:
bbox = font.getbbox(text)
x -= bbox[0]
y -= bbox[1]
self._pil_draw.text((int(x), int(y)), text, font=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
@@ -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
@@ -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()
@@ -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)
@@ -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)
@@ -0,0 +1,449 @@
'''
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)
@property
def _is_stream(self):
# This is only used when building ff_opts, to prevent starting
# player paused and can probably be removed as soon as the 'eof'
# receiving issue is solved.
# See https://github.com/matham/ffpyplayer/issues/142
return self.filename.startswith('rtsp://')
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
# ffpyplayer reports src_pix_fmt as bytes. this may or may not
# change in future, so we check for both bytes and str
if src_pix_fmt in (b'yuv420p', 'yuv420p'):
self._out_fmt = 'yuv420p'
ffplayer.set_output_pix_fmt(self._out_fmt)
break
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'
# if no stream, it starts internally paused, but unpauses itself
# if stream and we start paused, we sometimes receive eof after a
# few frames, depending on the stream producer.
# XXX: This probably needs to be figured out in ffpyplayer, using
# ffplay directly works.
ff_opts = {
'paused': not self._is_stream,
'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)
@@ -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
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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])
@@ -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
@@ -0,0 +1,441 @@
'''
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()
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
@@ -0,0 +1,999 @@
# 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)
self.bind(always_on_top=self._set_always_on_top)
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_always_on_top(self, *args):
self._win.set_always_on_top(self.always_on_top)
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'):
if platform == 'android':
Logger.info(
'WindowSDL: App stopped, on_pause() returned False.')
from android import mActivity
mActivity.finishAndRemoveTask()
else:
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 behavior
# 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 = self._win.setup_window(
pos[0], pos[1], w, h, self.borderless,
self.fullscreen, resizable, state,
self.get_gl_backend_name())
# We don't have a density or dpi yet set, so let's ask for an update
self._update_density_and_dpi()
# 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()
self._set_always_on_top()
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')
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 _update_density_and_dpi(self):
if platform == 'win':
from ctypes import windll
self._density = 1.
try:
hwnd = windll.user32.GetActiveWindow()
self.dpi = float(windll.user32.GetDpiForWindow(hwnd))
self._density = self.dpi / 96
except AttributeError:
pass
else:
self._density = self._win._get_gl_size()[0] / self._size[0]
if self._is_desktop:
self.dpi = self._density * 96.
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.')
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)
def _get_window_opacity(self):
return self._win.get_window_opacity()
def _set_window_opacity(self, opacity):
if self.opacity != opacity:
return self._win.set_window_opacity(opacity)
# 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 == 'windowdisplaychanged':
Logger.info(f"WindowSDL: Window is now on display {args[0]}")
# The display has changed, so the density and dpi
# may have changed too.
self._update_density_and_dpi()
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:
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
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)
@@ -0,0 +1,4 @@
$HEADER$
void main (void){
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

@@ -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);
}
@@ -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;
@@ -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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

@@ -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]}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

@@ -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]
]
}
@@ -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]
]
}
@@ -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]
]
}
@@ -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]
]
}

Some files were not shown because too many files have changed in this diff Show More