first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

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:
with open(kivy_config_fn, 'w') as fd:
Config.write(fd)
except Exception as e:
Logger.exception('Core: error while saving default'
'configuration file:', str(e))
Logger.info('Core: Kivy configuration saved.')
sys.exit(0)
# configure all activated modules
from kivy.modules import Modules
Modules.configure()
# android hooks: force fullscreen and add android touch input provider
if platform in ('android', 'ios'):
from kivy.config import Config
Config.set('graphics', 'fullscreen', 'auto')
Config.remove_section('input')
Config.add_section('input')
if platform == 'android':
Config.set('input', 'androidtouch', 'android')
for msg in _logging_msgs:
Logger.info(msg)
if not _KIVY_RELEASE and _kivy_git_hash and _kivy_build_date:
Logger.info('Kivy: v%s, git-%s, %s' % (
__version__, _kivy_git_hash[:7], _kivy_build_date))
else:
Logger.info('Kivy: v%s' % __version__)
Logger.info('Kivy: Installed at "{}"'.format(__file__))
Logger.info('Python: v{}'.format(sys.version))
Logger.info('Python: Interpreter at "{}"'.format(sys.executable))
from kivy.logger import file_log_handler
if file_log_handler is not None:
file_log_handler.purge_logs()

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)

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

View file

@ -0,0 +1,5 @@
from kivy._event cimport EventObservers
cdef EventObservers pixel_scale_observers
cpdef float dpi2px(value, str ext) except *

View file

@ -0,0 +1,17 @@
# This file is imported from __init__.py and exec'd from setup.py
MAJOR = 2
MINOR = 3
MICRO = 0
RELEASE = True
__version__ = '%d.%d.%d' % (MAJOR, MINOR, MICRO)
if not RELEASE:
# if it's a rcx release, it's not proceeded by a period. If it is a
# devx release, it must start with a period
__version__ += ''
_kivy_git_hash = '9ef218027178115a66b417ae34b21f488bdd3617'
_kivy_build_date = '20240105'

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

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'))

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()

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

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

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)

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,72 @@
'''
AudioAvplayer: implementation of Sound using pyobjus / AVFoundation.
Works on iOS / OSX.
'''
__all__ = ('SoundAvplayer', )
from kivy.core.audio import Sound, SoundLoader
from pyobjus import autoclass
from pyobjus.dylib_manager import load_framework, INCLUDE
load_framework(INCLUDE.AVFoundation)
AVAudioPlayer = autoclass("AVAudioPlayer")
NSURL = autoclass("NSURL")
NSString = autoclass("NSString")
class SoundAvplayer(Sound):
@staticmethod
def extensions():
# taken from https://goo.gl/015kvU
return ("aac", "adts", "aif", "aiff", "aifc", "caf", "mp3", "mp4",
"m4a", "snd", "au", "sd2", "wav")
def __init__(self, **kwargs):
self._avplayer = None
super(SoundAvplayer, self).__init__(**kwargs)
def load(self):
self.unload()
fn = NSString.alloc().initWithUTF8String_(self.source)
url = NSURL.alloc().initFileURLWithPath_(fn)
self._avplayer = AVAudioPlayer.alloc().initWithContentsOfURL_error_(
url, None)
def unload(self):
self.stop()
self._avplayer = None
def play(self):
if not self._avplayer:
return
self._avplayer.play()
super(SoundAvplayer, self).play()
def stop(self):
if not self._avplayer:
return
self._avplayer.stop()
super(SoundAvplayer, self).stop()
def seek(self, position):
if not self._avplayer:
return
self._avplayer.playAtTime_(float(position))
def get_pos(self):
if self._avplayer:
return self._avplayer.currentTime
return super(SoundAvplayer, self).get_pos()
def on_volume(self, instance, volume):
if self._avplayer:
self._avplayer.volume = float(volume)
def _get_length(self):
if self._avplayer:
return self._avplayer.duration
return super(SoundAvplayer, self)._get_length()
SoundLoader.register(SoundAvplayer)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

@ -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')

View file

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

View file

@ -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)

View file

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

View file

@ -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())

View file

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

View file

@ -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',)

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -0,0 +1,29 @@
'''
Clipboard xsel: an implementation of the Clipboard using xsel command line
tool.
'''
__all__ = ('ClipboardXsel', )
from kivy.utils import platform
from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase
if platform != 'linux':
raise SystemError('unsupported platform for xsel clipboard')
try:
import subprocess
p = subprocess.Popen(['xsel'], stdout=subprocess.PIPE)
p.communicate()
except:
raise
class ClipboardXsel(ClipboardExternalBase):
@staticmethod
def _clip(inout, selection):
pipe = {'std' + inout: subprocess.PIPE}
sel = 'b' if selection == 'clipboard' else selection[0]
io = inout[0]
return subprocess.Popen(
['xsel', '-' + sel + io], **pipe)

View file

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

File diff suppressed because it is too large Load diff

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