Stav 23.06.2026
@@ -0,0 +1,519 @@
|
||||
'''
|
||||
Kivy framework
|
||||
==============
|
||||
|
||||
Kivy is an open source library for developing multi-touch applications. It is
|
||||
cross-platform (Linux/OSX/Windows/Android/iOS) and released under
|
||||
the terms of the `MIT License <https://en.wikipedia.org/wiki/MIT_License>`_.
|
||||
|
||||
It comes with native support for many multi-touch input devices, a growing
|
||||
library of multi-touch aware widgets and hardware accelerated OpenGL drawing.
|
||||
Kivy is designed to let you focus on building custom and highly interactive
|
||||
applications as quickly and easily as possible.
|
||||
|
||||
With Kivy, you can take full advantage of the dynamic nature of Python. There
|
||||
are thousands of high-quality, free libraries that can be integrated in your
|
||||
application. At the same time, performance-critical parts are implemented
|
||||
using `Cython <http://cython.org/>`_.
|
||||
|
||||
See http://kivy.org for more information.
|
||||
'''
|
||||
|
||||
__all__ = (
|
||||
'require', 'parse_kivy_version',
|
||||
'kivy_configure', 'kivy_register_post_configuration',
|
||||
'kivy_options', 'kivy_base_dir',
|
||||
'kivy_modules_dir', 'kivy_data_dir', 'kivy_shader_dir',
|
||||
'kivy_icons_dir', 'kivy_home_dir',
|
||||
'kivy_config_fn', 'kivy_usermodules_dir', 'kivy_examples_dir'
|
||||
)
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
from getopt import getopt, GetoptError
|
||||
import os
|
||||
from os import environ, mkdir
|
||||
from os.path import dirname, join, basename, exists, expanduser
|
||||
import pkgutil
|
||||
import re
|
||||
import importlib
|
||||
from kivy.logger import Logger, LOG_LEVELS
|
||||
from kivy.utils import platform
|
||||
from kivy._version import __version__, RELEASE as _KIVY_RELEASE, \
|
||||
_kivy_git_hash, _kivy_build_date
|
||||
|
||||
# internals for post-configuration
|
||||
__kivy_post_configuration = []
|
||||
|
||||
|
||||
if platform == 'macosx' and sys.maxsize < 9223372036854775807:
|
||||
r = '''Unsupported Python version detected!:
|
||||
Kivy requires a 64 bit version of Python to run on OS X. We strongly
|
||||
advise you to use the version of Python that is provided by Apple
|
||||
(don't use ports, fink or homebrew unless you know what you're
|
||||
doing).
|
||||
See http://kivy.org/docs/installation/installation-macosx.html for
|
||||
details.
|
||||
'''
|
||||
Logger.critical(r)
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
Logger.critical(
|
||||
'Unsupported Python version detected!: Kivy 2.0.0 and higher does not '
|
||||
'support Python 2. Please upgrade to Python 3, or downgrade Kivy to '
|
||||
'1.11.0 - the last Kivy release that still supports Python 2.')
|
||||
|
||||
|
||||
def parse_kivy_version(version):
|
||||
"""Parses the kivy version as described in :func:`require` into a 3-tuple
|
||||
of ([x, y, z], 'rc|a|b|dev|post', 'N') where N is the tag revision. The
|
||||
last two elements may be None.
|
||||
"""
|
||||
m = re.match(
|
||||
'^([0-9]+)\\.([0-9]+)\\.([0-9]+?)(rc|a|b|\\.dev|\\.post)?([0-9]+)?$',
|
||||
version)
|
||||
if m is None:
|
||||
raise Exception('Revision format must be X.Y.Z[-tag]')
|
||||
|
||||
major, minor, micro, tag, tagrev = m.groups()
|
||||
if tag == '.dev':
|
||||
tag = 'dev'
|
||||
if tag == '.post':
|
||||
tag = 'post'
|
||||
return [int(major), int(minor), int(micro)], tag, tagrev
|
||||
|
||||
|
||||
def require(version):
|
||||
'''Require can be used to check the minimum version required to run a Kivy
|
||||
application. For example, you can start your application code like this::
|
||||
|
||||
import kivy
|
||||
kivy.require('1.0.1')
|
||||
|
||||
If a user attempts to run your application with a version of Kivy that is
|
||||
older than the specified version, an Exception is raised.
|
||||
|
||||
The Kivy version string is built like this::
|
||||
|
||||
X.Y.Z[tag[tagrevision]]
|
||||
|
||||
X is the major version
|
||||
Y is the minor version
|
||||
Z is the bugfixes revision
|
||||
|
||||
The tag is optional, but may be one of '.dev', '.post', 'a', 'b', or 'rc'.
|
||||
The tagrevision is the revision number of the tag.
|
||||
|
||||
.. warning::
|
||||
|
||||
You must not ask for a version with a tag, except -dev. Asking for a
|
||||
'dev' version will just warn the user if the current Kivy
|
||||
version is not a -dev, but it will never raise an exception.
|
||||
You must not ask for a version with a tagrevision.
|
||||
|
||||
'''
|
||||
|
||||
# user version
|
||||
revision, tag, tagrev = parse_kivy_version(version)
|
||||
# current version
|
||||
sysrevision, systag, systagrev = parse_kivy_version(__version__)
|
||||
|
||||
if tag and not systag:
|
||||
Logger.warning('Application requested a dev version of Kivy. '
|
||||
'(You have %s, but the application requires %s)' % (
|
||||
__version__, version))
|
||||
# not tag rev (-alpha-1, -beta-x) allowed.
|
||||
if tagrev is not None:
|
||||
raise Exception('Revision format must not contain any tagrevision')
|
||||
|
||||
# finally, checking revision
|
||||
if sysrevision < revision:
|
||||
raise Exception('The version of Kivy installed on this system '
|
||||
'is too old. '
|
||||
'(You have %s, but the application requires %s)' % (
|
||||
__version__, version))
|
||||
|
||||
|
||||
def kivy_configure():
|
||||
'''Call post-configuration of Kivy.
|
||||
This function must be called if you create the window yourself.
|
||||
'''
|
||||
for callback in __kivy_post_configuration:
|
||||
callback()
|
||||
|
||||
|
||||
def get_includes():
|
||||
'''Retrieves the directories containing includes needed to build new Cython
|
||||
modules with Kivy as a dependency. Currently returns the location of the
|
||||
kivy.graphics module.
|
||||
|
||||
.. versionadded:: 1.9.1
|
||||
'''
|
||||
root_dir = dirname(__file__)
|
||||
return [join(root_dir, 'graphics'), join(root_dir, 'tools', 'gles_compat'),
|
||||
join(root_dir, 'include')]
|
||||
|
||||
|
||||
def kivy_register_post_configuration(callback):
|
||||
'''Register a function to be called when kivy_configure() is called.
|
||||
|
||||
.. warning::
|
||||
Internal use only.
|
||||
'''
|
||||
__kivy_post_configuration.append(callback)
|
||||
|
||||
|
||||
def kivy_usage():
|
||||
'''Kivy Usage: %s [KIVY OPTION...] [-- PROGRAM OPTIONS]::
|
||||
|
||||
Options placed after a '-- ' separator, will not be touched by kivy,
|
||||
and instead passed to your program.
|
||||
|
||||
Set KIVY_NO_ARGS=1 in your environment or before you import Kivy to
|
||||
disable Kivy's argument parser.
|
||||
|
||||
-h, --help
|
||||
Prints this help message.
|
||||
-d, --debug
|
||||
Shows debug log.
|
||||
-a, --auto-fullscreen
|
||||
Force 'auto' fullscreen mode (no resolution change).
|
||||
Uses your display's resolution. This is most likely what you want.
|
||||
-c, --config section:key[:value]
|
||||
Set a custom [section] key=value in the configuration object.
|
||||
-f, --fullscreen
|
||||
Force running in fullscreen mode.
|
||||
-k, --fake-fullscreen
|
||||
Force 'fake' fullscreen mode (no window border/decoration).
|
||||
Uses the resolution specified by width and height in your config.
|
||||
-w, --windowed
|
||||
Force running in a window.
|
||||
-p, --provider id:provider[,options]
|
||||
Add an input provider (eg: ccvtable1:tuio,192.168.0.1:3333).
|
||||
-m mod, --module=mod
|
||||
Activate a module (use "list" to get a list of available modules).
|
||||
-r, --rotation
|
||||
Rotate the window's contents (0, 90, 180, 270).
|
||||
-s, --save
|
||||
Save current Kivy configuration.
|
||||
--size=640x480
|
||||
Size of window geometry.
|
||||
--dpi=96
|
||||
Manually overload the Window DPI (for testing only.)
|
||||
'''
|
||||
print(kivy_usage.__doc__ % (basename(sys.argv[0])))
|
||||
|
||||
|
||||
#: Global settings options for kivy
|
||||
kivy_options = {
|
||||
'window': ('egl_rpi', 'sdl2', 'pygame', 'sdl', 'x11'),
|
||||
'text': ('pil', 'sdl2', 'pygame', 'sdlttf'),
|
||||
'video': (
|
||||
'gstplayer', 'ffmpeg', 'ffpyplayer', 'null'),
|
||||
'audio': (
|
||||
'gstplayer', 'pygame', 'ffpyplayer', 'sdl2',
|
||||
'avplayer'),
|
||||
'image': ('tex', 'imageio', 'dds', 'sdl2', 'pygame', 'pil', 'ffpy', 'gif'),
|
||||
'camera': ('opencv', 'gi', 'avfoundation',
|
||||
'android', 'picamera'),
|
||||
'spelling': ('enchant', 'osxappkit', ),
|
||||
'clipboard': (
|
||||
'android', 'winctypes', 'xsel', 'xclip', 'dbusklipper', 'nspaste',
|
||||
'sdl2', 'pygame', 'dummy', 'gtk3', )}
|
||||
|
||||
# Read environment
|
||||
for option in kivy_options:
|
||||
key = 'KIVY_%s' % option.upper()
|
||||
if key in environ:
|
||||
try:
|
||||
if type(kivy_options[option]) in (list, tuple):
|
||||
kivy_options[option] = environ[key].split(',')
|
||||
else:
|
||||
kivy_options[option] = environ[key].lower() in \
|
||||
('true', '1', 'yes')
|
||||
except Exception:
|
||||
Logger.warning('Core: Wrong value for %s environment key' % key)
|
||||
Logger.exception('')
|
||||
|
||||
# Extract all needed path in kivy
|
||||
#: Kivy directory
|
||||
kivy_base_dir = dirname(sys.modules[__name__].__file__)
|
||||
#: Kivy modules directory
|
||||
|
||||
kivy_modules_dir = environ.get('KIVY_MODULES_DIR',
|
||||
join(kivy_base_dir, 'modules'))
|
||||
#: Kivy data directory
|
||||
kivy_data_dir = environ.get('KIVY_DATA_DIR',
|
||||
join(kivy_base_dir, 'data'))
|
||||
#: Kivy binary deps directory
|
||||
kivy_binary_deps_dir = environ.get('KIVY_BINARY_DEPS',
|
||||
join(kivy_base_dir, 'binary_deps'))
|
||||
#: Kivy glsl shader directory
|
||||
kivy_shader_dir = join(kivy_data_dir, 'glsl')
|
||||
#: Kivy icons config path (don't remove the last '')
|
||||
kivy_icons_dir = join(kivy_data_dir, 'icons', '')
|
||||
#: Kivy user-home storage directory
|
||||
kivy_home_dir = ''
|
||||
#: Kivy configuration filename
|
||||
kivy_config_fn = ''
|
||||
#: Kivy user modules directory
|
||||
kivy_usermodules_dir = ''
|
||||
#: Kivy examples directory
|
||||
kivy_examples_dir = ''
|
||||
for examples_dir in (
|
||||
join(dirname(dirname(__file__)), 'examples'),
|
||||
join(sys.exec_prefix, 'share', 'kivy-examples'),
|
||||
join(sys.prefix, 'share', 'kivy-examples'),
|
||||
'/usr/share/kivy-examples', '/usr/local/share/kivy-examples',
|
||||
expanduser('~/.local/share/kivy-examples')):
|
||||
if exists(examples_dir):
|
||||
kivy_examples_dir = examples_dir
|
||||
break
|
||||
|
||||
|
||||
def _patch_mod_deps_win(dep_mod, mod_name):
|
||||
import site
|
||||
dep_bins = []
|
||||
|
||||
for d in [sys.prefix, site.USER_BASE]:
|
||||
p = join(d, 'share', mod_name, 'bin')
|
||||
if os.path.isdir(p):
|
||||
os.environ["PATH"] = p + os.pathsep + os.environ["PATH"]
|
||||
if hasattr(os, 'add_dll_directory'):
|
||||
os.add_dll_directory(p)
|
||||
dep_bins.append(p)
|
||||
|
||||
dep_mod.dep_bins = dep_bins
|
||||
|
||||
|
||||
# if there are deps, import them so they can do their magic.
|
||||
_packages = []
|
||||
try:
|
||||
from kivy import deps as old_deps
|
||||
for importer, modname, ispkg in pkgutil.iter_modules(old_deps.__path__):
|
||||
if not ispkg:
|
||||
continue
|
||||
if modname.startswith('gst'):
|
||||
_packages.insert(0, (importer, modname, 'kivy.deps'))
|
||||
else:
|
||||
_packages.append((importer, modname, 'kivy.deps'))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import kivy_deps
|
||||
for importer, modname, ispkg in pkgutil.iter_modules(kivy_deps.__path__):
|
||||
if not ispkg:
|
||||
continue
|
||||
if modname.startswith('gst'):
|
||||
_packages.insert(0, (importer, modname, 'kivy_deps'))
|
||||
else:
|
||||
_packages.append((importer, modname, 'kivy_deps'))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
_logging_msgs = []
|
||||
for importer, modname, package in _packages:
|
||||
try:
|
||||
module_spec = importer.find_spec(modname)
|
||||
mod = importlib.util.module_from_spec(module_spec)
|
||||
module_spec.loader.exec_module(mod)
|
||||
|
||||
version = ''
|
||||
if hasattr(mod, '__version__'):
|
||||
version = ' {}'.format(mod.__version__)
|
||||
_logging_msgs.append(
|
||||
'deps: Successfully imported "{}.{}"{}'.
|
||||
format(package, modname, version))
|
||||
|
||||
if modname.startswith('gst') and version == '0.3.3':
|
||||
_patch_mod_deps_win(mod, modname)
|
||||
|
||||
except ImportError as e:
|
||||
Logger.warning(
|
||||
'deps: Error importing dependency "{}.{}": {}'.
|
||||
format(package, modname, str(e)))
|
||||
|
||||
# Don't go further if we generate documentation
|
||||
if any(name in sys.argv[0] for name in (
|
||||
'sphinx-build', 'autobuild.py', 'sphinx'
|
||||
)):
|
||||
environ['KIVY_DOC'] = '1'
|
||||
if 'sphinx-build' in sys.argv[0]:
|
||||
environ['KIVY_DOC_INCLUDE'] = '1'
|
||||
if any('pytest' in arg for arg in sys.argv):
|
||||
environ['KIVY_UNITTEST'] = '1'
|
||||
if any('pyinstaller' in arg.lower() for arg in sys.argv):
|
||||
environ['KIVY_PACKAGING'] = '1'
|
||||
|
||||
if not environ.get('KIVY_DOC_INCLUDE'):
|
||||
# Configuration management
|
||||
if 'KIVY_HOME' in environ:
|
||||
kivy_home_dir = expanduser(environ['KIVY_HOME'])
|
||||
else:
|
||||
user_home_dir = expanduser('~')
|
||||
if platform == 'android':
|
||||
user_home_dir = environ['ANDROID_APP_PATH']
|
||||
elif platform == 'ios':
|
||||
user_home_dir = join(expanduser('~'), 'Documents')
|
||||
kivy_home_dir = join(user_home_dir, '.kivy')
|
||||
|
||||
kivy_config_fn = join(kivy_home_dir, 'config.ini')
|
||||
kivy_usermodules_dir = join(kivy_home_dir, 'mods')
|
||||
icon_dir = join(kivy_home_dir, 'icon')
|
||||
|
||||
if 'KIVY_NO_CONFIG' not in environ:
|
||||
if not exists(kivy_home_dir):
|
||||
mkdir(kivy_home_dir)
|
||||
if not exists(kivy_usermodules_dir):
|
||||
mkdir(kivy_usermodules_dir)
|
||||
if not exists(icon_dir):
|
||||
try:
|
||||
shutil.copytree(join(kivy_data_dir, 'logo'), icon_dir)
|
||||
except:
|
||||
Logger.exception('Error when copying logo directory')
|
||||
|
||||
# configuration
|
||||
from kivy.config import Config
|
||||
|
||||
# Set level of logger
|
||||
level = LOG_LEVELS.get(Config.get('kivy', 'log_level'))
|
||||
Logger.setLevel(level=level)
|
||||
|
||||
# Can be overridden in command line
|
||||
if ('KIVY_UNITTEST' not in environ and
|
||||
'KIVY_PACKAGING' not in environ and
|
||||
environ.get('KIVY_NO_ARGS', "false") not in ('true', '1', 'yes')):
|
||||
# save sys argv, otherwise, gstreamer use it and display help..
|
||||
sys_argv = sys.argv
|
||||
sys.argv = sys.argv[:1]
|
||||
|
||||
try:
|
||||
opts, args = getopt(sys_argv[1:], 'hp:fkawFem:sr:dc:', [
|
||||
'help', 'fullscreen', 'windowed', 'fps', 'event',
|
||||
'module=', 'save', 'fake-fullscreen', 'auto-fullscreen',
|
||||
'multiprocessing-fork', 'display=', 'size=', 'rotate=',
|
||||
'config=', 'debug', 'dpi='])
|
||||
|
||||
except GetoptError as err:
|
||||
Logger.error('Core: %s' % str(err))
|
||||
kivy_usage()
|
||||
sys.exit(2)
|
||||
|
||||
mp_fork = None
|
||||
try:
|
||||
for opt, arg in opts:
|
||||
if opt == '--multiprocessing-fork':
|
||||
mp_fork = True
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# set argv to the non-read args
|
||||
sys.argv = sys_argv[0:1] + args
|
||||
if mp_fork is not None:
|
||||
# Needs to be first opt for support_freeze to work
|
||||
sys.argv.insert(1, '--multiprocessing-fork')
|
||||
|
||||
else:
|
||||
opts = []
|
||||
args = []
|
||||
|
||||
need_save = False
|
||||
for opt, arg in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
kivy_usage()
|
||||
sys.exit(0)
|
||||
elif opt in ('-p', '--provider'):
|
||||
try:
|
||||
pid, args = arg.split(':', 1)
|
||||
Config.set('input', pid, args)
|
||||
except ValueError:
|
||||
# when we are doing an executable on macosx with
|
||||
# pyinstaller, they are passing information with -p. so
|
||||
# it will conflict with our current -p option. since the
|
||||
# format is not the same, just avoid it.
|
||||
pass
|
||||
elif opt in ('-a', '--auto-fullscreen'):
|
||||
Config.set('graphics', 'fullscreen', 'auto')
|
||||
elif opt in ('-c', '--config'):
|
||||
ol = arg.split(':', 2)
|
||||
if len(ol) == 2:
|
||||
Config.set(ol[0], ol[1], '')
|
||||
elif len(ol) == 3:
|
||||
Config.set(ol[0], ol[1], ol[2])
|
||||
else:
|
||||
raise Exception('Invalid --config value')
|
||||
if ol[0] == 'kivy' and ol[1] == 'log_level':
|
||||
level = LOG_LEVELS.get(Config.get('kivy', 'log_level'))
|
||||
Logger.setLevel(level=level)
|
||||
elif opt in ('-k', '--fake-fullscreen'):
|
||||
Config.set('graphics', 'fullscreen', 'fake')
|
||||
elif opt in ('-f', '--fullscreen'):
|
||||
Config.set('graphics', 'fullscreen', '1')
|
||||
elif opt in ('-w', '--windowed'):
|
||||
Config.set('graphics', 'fullscreen', '0')
|
||||
elif opt in ('--size', ):
|
||||
w, h = str(arg).split('x')
|
||||
Config.set('graphics', 'width', w)
|
||||
Config.set('graphics', 'height', h)
|
||||
elif opt in ('--display', ):
|
||||
Config.set('graphics', 'display', str(arg))
|
||||
elif opt in ('-m', '--module'):
|
||||
if str(arg) == 'list':
|
||||
from kivy.modules import Modules
|
||||
Modules.usage_list()
|
||||
sys.exit(0)
|
||||
args = arg.split(':', 1)
|
||||
if len(args) == 1:
|
||||
args += ['']
|
||||
Config.set('modules', args[0], args[1])
|
||||
elif opt in ('-s', '--save'):
|
||||
need_save = True
|
||||
elif opt in ('-r', '--rotation'):
|
||||
Config.set('graphics', 'rotation', arg)
|
||||
elif opt in ('-d', '--debug'):
|
||||
level = LOG_LEVELS.get('debug')
|
||||
Logger.setLevel(level=level)
|
||||
elif opt == '--dpi':
|
||||
environ['KIVY_DPI'] = arg
|
||||
|
||||
if need_save and 'KIVY_NO_CONFIG' not in environ:
|
||||
try:
|
||||
Config.filename = kivy_config_fn
|
||||
Config.write()
|
||||
except Exception as e:
|
||||
Logger.exception('Core: error while saving default'
|
||||
'configuration file:', str(e))
|
||||
Logger.info('Core: Kivy configuration saved.')
|
||||
sys.exit(0)
|
||||
|
||||
# configure all activated modules
|
||||
from kivy.modules import Modules
|
||||
Modules.configure()
|
||||
|
||||
# android hooks: force fullscreen and add android touch input provider
|
||||
if platform in ('android', 'ios'):
|
||||
from kivy.config import Config
|
||||
Config.set('graphics', 'fullscreen', 'auto')
|
||||
Config.remove_section('input')
|
||||
Config.add_section('input')
|
||||
|
||||
if platform == 'android':
|
||||
Config.set('input', 'androidtouch', 'android')
|
||||
|
||||
for msg in _logging_msgs:
|
||||
Logger.info(msg)
|
||||
|
||||
if not _KIVY_RELEASE and _kivy_git_hash and _kivy_build_date:
|
||||
Logger.info('Kivy: v%s, git-%s, %s' % (
|
||||
__version__, _kivy_git_hash[:7], _kivy_build_date))
|
||||
else:
|
||||
Logger.info('Kivy: v%s' % __version__)
|
||||
Logger.info('Kivy: Installed at "{}"'.format(__file__))
|
||||
Logger.info('Python: v{}'.format(sys.version))
|
||||
Logger.info('Python: Interpreter at "{}"'.format(sys.executable))
|
||||
|
||||
from kivy.logger import file_log_handler
|
||||
if file_log_handler is not None:
|
||||
file_log_handler.purge_logs()
|
||||
@@ -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)
|
||||
@@ -0,0 +1,68 @@
|
||||
from cpython.ref cimport PyObject
|
||||
|
||||
cdef dict cache_properties_per_cls
|
||||
|
||||
cdef class ObjectWithUid(object):
|
||||
cdef readonly int uid
|
||||
|
||||
|
||||
cdef class Observable(ObjectWithUid):
|
||||
cdef object __fbind_mapping
|
||||
cdef object bound_uid
|
||||
|
||||
|
||||
cdef class EventDispatcher(ObjectWithUid):
|
||||
cdef dict __event_stack
|
||||
cdef dict __properties
|
||||
cdef dict __storage
|
||||
cdef object __weakref__
|
||||
cdef public set _kwargs_applied_init
|
||||
cdef public object _proxy_ref
|
||||
cpdef dict properties(self)
|
||||
|
||||
|
||||
cdef enum BoundLock:
|
||||
# the state of the BoundCallback, i.e. whether it can be deleted
|
||||
unlocked # whether the BoundCallback is unlocked and can be deleted
|
||||
locked # whether the BoundCallback is locked and cannot be deleted
|
||||
deleted # whether the locked BoundCallback was marked for deletion
|
||||
|
||||
cdef class BoundCallback:
|
||||
cdef object func
|
||||
cdef tuple largs
|
||||
cdef dict kwargs
|
||||
cdef int is_ref # if func is a ref to the function
|
||||
cdef BoundLock lock # see BoundLock
|
||||
cdef BoundCallback next # next callback in chain
|
||||
cdef BoundCallback prev # previous callback in chain
|
||||
cdef object uid # the uid given for this callback, None if not given
|
||||
cdef EventObservers observers
|
||||
|
||||
cdef void set_largs(self, tuple largs)
|
||||
|
||||
|
||||
cdef class EventObservers:
|
||||
# If dispatching should occur in normal or reverse order of binding.
|
||||
cdef int dispatch_reverse
|
||||
# If in dispatch, the value parameter is dispatched or ignored.
|
||||
cdef int dispatch_value
|
||||
# The first callback bound
|
||||
cdef BoundCallback first_callback
|
||||
# The last callback bound
|
||||
cdef BoundCallback last_callback
|
||||
# The uid to assign to the next bound callback.
|
||||
cdef object uid
|
||||
|
||||
cdef inline BoundCallback make_callback(self, object observer, tuple largs, dict kwargs, int is_ref, uid=*)
|
||||
cdef inline void bind(self, object observer, object src_observer, int is_ref) except *
|
||||
cdef inline object fbind(self, object observer, tuple largs, dict kwargs, int is_ref)
|
||||
cdef inline BoundCallback fbind_callback(self, object observer, tuple largs, dict kwargs, int is_ref)
|
||||
cdef inline void fbind_existing_callback(self, BoundCallback callback)
|
||||
cdef inline void unbind(self, object observer, int stop_on_first) except *
|
||||
cdef inline void funbind(self, object observer, tuple largs, dict kwargs) except *
|
||||
cdef inline object unbind_uid(self, object uid)
|
||||
cdef inline object unbind_callback(self, BoundCallback callback)
|
||||
cdef inline void remove_callback(self, BoundCallback callback, int force=*) except *
|
||||
cdef inline object _dispatch(
|
||||
self, object f, tuple slargs, dict skwargs, object obj, object value, tuple largs, dict kwargs)
|
||||
cdef inline int dispatch(self, object obj, object value, tuple largs, dict kwargs, int stop_on_true) except 2
|
||||
@@ -0,0 +1,5 @@
|
||||
from kivy._event cimport EventObservers
|
||||
|
||||
cdef EventObservers pixel_scale_observers
|
||||
|
||||
cpdef float dpi2px(value, str ext) except *
|
||||
@@ -0,0 +1,18 @@
|
||||
# This file is imported from __init__.py and exec'd from setup.py
|
||||
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
MICRO = 1
|
||||
RELEASE = True
|
||||
|
||||
__version__ = '%d.%d.%d' % (MAJOR, MINOR, MICRO)
|
||||
|
||||
if not RELEASE:
|
||||
# if it's a rcx release, it's not proceeded by a period. If it is a
|
||||
# devx release, it must start with a period
|
||||
__version__ += ''
|
||||
|
||||
|
||||
_kivy_git_hash = '20d74dcd30f143abbd1aa94c76bafc5bd934d5bd'
|
||||
_kivy_build_date = '20241226'
|
||||
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,102 @@
|
||||
'''
|
||||
Context
|
||||
=======
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
.. warning::
|
||||
|
||||
This is experimental and subject to change as long as this warning notice
|
||||
is present.
|
||||
|
||||
Kivy has a few "global" instances that are used directly by many pieces of the
|
||||
framework: `Cache`, `Builder`, `Clock`.
|
||||
|
||||
TODO: document this module.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Context', 'ProxyContext', 'register_context',
|
||||
'get_current_context')
|
||||
|
||||
_contexts = {}
|
||||
_default_context = None
|
||||
_context_stack = []
|
||||
|
||||
|
||||
class ProxyContext(object):
|
||||
|
||||
__slots__ = ['_obj']
|
||||
|
||||
def __init__(self, obj):
|
||||
object.__init__(self)
|
||||
object.__setattr__(self, '_obj', obj)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
return getattr(object.__getattribute__(self, '_obj'), name)
|
||||
|
||||
def __delattr__(self, name):
|
||||
delattr(object.__getattribute__(self, '_obj'), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
setattr(object.__getattribute__(self, '_obj'), name, value)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(object.__getattribute__(self, '_obj'))
|
||||
|
||||
def __str__(self):
|
||||
return str(object.__getattribute__(self, '_obj'))
|
||||
|
||||
def __repr__(self):
|
||||
return repr(object.__getattribute__(self, '_obj'))
|
||||
|
||||
|
||||
class Context(dict):
|
||||
|
||||
def __init__(self, init=False):
|
||||
dict.__init__(self)
|
||||
self.sandbox = None
|
||||
if not init:
|
||||
return
|
||||
|
||||
for name in _contexts:
|
||||
context = _contexts[name]
|
||||
instance = context['cls'](*context['args'], **context['kwargs'])
|
||||
self[name] = instance
|
||||
|
||||
def push(self):
|
||||
_context_stack.append(self)
|
||||
for name, instance in self.items():
|
||||
object.__setattr__(_contexts[name]['proxy'], '_obj', instance)
|
||||
|
||||
def pop(self):
|
||||
# After popping context from stack. Update proxy's _obj with
|
||||
# instances in current context
|
||||
_context_stack.pop(-1)
|
||||
for name, instance in get_current_context().items():
|
||||
object.__setattr__(_contexts[name]['proxy'], '_obj', instance)
|
||||
|
||||
|
||||
def register_context(name, cls, *args, **kwargs):
|
||||
'''Register a new context.
|
||||
'''
|
||||
instance = cls(*args, **kwargs)
|
||||
proxy = ProxyContext(instance)
|
||||
_contexts[name] = {
|
||||
'cls': cls,
|
||||
'args': args,
|
||||
'kwargs': kwargs,
|
||||
'proxy': proxy}
|
||||
_default_context[name] = instance
|
||||
return proxy
|
||||
|
||||
|
||||
def get_current_context():
|
||||
'''Return the current context.
|
||||
'''
|
||||
if not _context_stack:
|
||||
return _default_context
|
||||
return _context_stack[-1]
|
||||
|
||||
|
||||
_default_context = Context(init=False)
|
||||
@@ -0,0 +1,247 @@
|
||||
'''
|
||||
Core Abstraction
|
||||
================
|
||||
|
||||
This module defines the abstraction layers for our core providers and their
|
||||
implementations. For further information, please refer to
|
||||
:ref:`architecture` and the :ref:`providers` section of the documentation.
|
||||
|
||||
In most cases, you shouldn't directly use a library that's already covered
|
||||
by the core abstraction. Always try to use our providers first.
|
||||
In case we are missing a feature or method, please let us know by
|
||||
opening a new Bug report instead of relying on your library.
|
||||
|
||||
.. warning::
|
||||
These are **not** widgets! These are just abstractions of the respective
|
||||
functionality. For example, you cannot add a core image to your window.
|
||||
You have to use the image **widget** class instead. If you're really
|
||||
looking for widgets, please refer to :mod:`kivy.uix` instead.
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import sysconfig
|
||||
import sys
|
||||
import traceback
|
||||
import tempfile
|
||||
import subprocess
|
||||
import importlib
|
||||
import kivy
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
class CoreCriticalException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def core_select_lib(category, llist, create_instance=False,
|
||||
base='kivy.core', basemodule=None):
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
return
|
||||
category = category.lower()
|
||||
basemodule = basemodule or category
|
||||
libs_ignored = []
|
||||
errs = []
|
||||
for option, modulename, classname in llist:
|
||||
try:
|
||||
# module activated in config ?
|
||||
try:
|
||||
if option not in kivy.kivy_options[category]:
|
||||
libs_ignored.append(modulename)
|
||||
Logger.debug(
|
||||
'{0}: Provider <{1}> ignored by config'.format(
|
||||
category.capitalize(), option))
|
||||
continue
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# import module
|
||||
mod = importlib.__import__(name='{2}.{0}.{1}'.format(
|
||||
basemodule, modulename, base),
|
||||
globals=globals(),
|
||||
locals=locals(),
|
||||
fromlist=[modulename], level=0)
|
||||
cls = mod.__getattribute__(classname)
|
||||
|
||||
# ok !
|
||||
Logger.info('{0}: Provider: {1}{2}'.format(
|
||||
category.capitalize(), option,
|
||||
'({0} ignored)'.format(libs_ignored) if libs_ignored else ''))
|
||||
if create_instance:
|
||||
cls = cls()
|
||||
return cls
|
||||
|
||||
except ImportError as e:
|
||||
errs.append((option, e, sys.exc_info()[2]))
|
||||
libs_ignored.append(modulename)
|
||||
Logger.debug('{0}: Ignored <{1}> (import error)'.format(
|
||||
category.capitalize(), option))
|
||||
Logger.trace('', exc_info=e)
|
||||
|
||||
except CoreCriticalException as e:
|
||||
errs.append((option, e, sys.exc_info()[2]))
|
||||
Logger.error('{0}: Unable to use {1}'.format(
|
||||
category.capitalize(), option))
|
||||
Logger.error(
|
||||
'{0}: The module raised an important error: {1!r}'.format(
|
||||
category.capitalize(), e.message))
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
errs.append((option, e, sys.exc_info()[2]))
|
||||
libs_ignored.append(modulename)
|
||||
Logger.trace('{0}: Unable to use {1}'.format(
|
||||
category.capitalize(), option))
|
||||
Logger.trace('', exc_info=e)
|
||||
|
||||
err = '\n'.join(['{} - {}: {}\n{}'.format(opt, e.__class__.__name__, e,
|
||||
''.join(traceback.format_tb(tb))) for opt, e, tb in errs])
|
||||
Logger.critical(
|
||||
'{0}: Unable to find any valuable {0} provider. Please enable '
|
||||
'debug logging (e.g. add -d if running from the command line, or '
|
||||
'change the log level in the config) and re-run your app to '
|
||||
'identify potential causes\n{1}'.format(category.capitalize(), err))
|
||||
|
||||
|
||||
def core_register_libs(category, libs, base='kivy.core'):
|
||||
if 'KIVY_DOC' in os.environ:
|
||||
return
|
||||
category = category.lower()
|
||||
kivy_options = kivy.kivy_options[category]
|
||||
libs_loadable = {}
|
||||
libs_ignored = []
|
||||
|
||||
for option, lib in libs:
|
||||
# module activated in config ?
|
||||
if option not in kivy_options:
|
||||
Logger.debug('{0}: option <{1}> ignored by config'.format(
|
||||
category.capitalize(), option))
|
||||
libs_ignored.append(lib)
|
||||
continue
|
||||
libs_loadable[option] = lib
|
||||
|
||||
libs_loaded = []
|
||||
for item in kivy_options:
|
||||
try:
|
||||
# import module
|
||||
try:
|
||||
lib = libs_loadable[item]
|
||||
except KeyError:
|
||||
continue
|
||||
importlib.__import__(name='{2}.{0}.{1}'.format(category, lib, base),
|
||||
globals=globals(),
|
||||
locals=locals(),
|
||||
fromlist=[lib],
|
||||
level=0)
|
||||
|
||||
libs_loaded.append(lib)
|
||||
|
||||
except Exception as e:
|
||||
Logger.trace('{0}: Unable to use <{1}> as loader!'.format(
|
||||
category.capitalize(), option))
|
||||
Logger.trace('', exc_info=e)
|
||||
libs_ignored.append(lib)
|
||||
|
||||
Logger.info('{0}: Providers: {1} {2}'.format(
|
||||
category.capitalize(),
|
||||
', '.join(libs_loaded),
|
||||
'({0} ignored)'.format(
|
||||
', '.join(libs_ignored)) if libs_ignored else ''))
|
||||
return libs_loaded
|
||||
|
||||
|
||||
def handle_win_lib_import_error(category, provider, mod_name):
|
||||
if sys.platform != 'win32':
|
||||
return
|
||||
|
||||
assert mod_name.startswith('kivy.')
|
||||
kivy_root = os.path.dirname(kivy.__file__)
|
||||
dirs = mod_name[5:].split('.')
|
||||
mod_path = os.path.join(kivy_root, *dirs)
|
||||
|
||||
# get the full expected path to the compiled pyd file
|
||||
# filename is <debug>.cp<major><minor>-<platform>.pyd
|
||||
# https://github.com/python/cpython/blob/master/Doc/whatsnew/3.5.rst
|
||||
if hasattr(sys, 'gettotalrefcount'): # debug
|
||||
mod_path += '._d'
|
||||
mod_path += '.cp{}{}-{}.pyd'.format(
|
||||
sys.version_info.major, sys.version_info.minor,
|
||||
sysconfig.get_platform().replace('-', '_'))
|
||||
|
||||
# does the compiled pyd exist at all?
|
||||
if not os.path.exists(mod_path):
|
||||
Logger.debug(
|
||||
'{}: Failed trying to import "{}" for provider {}. Compiled file '
|
||||
'does not exist. Have you perhaps forgotten to compile Kivy, or '
|
||||
'did not install all required dependencies?'.format(
|
||||
category, provider, mod_path))
|
||||
return
|
||||
|
||||
# tell user to provide dependency walker
|
||||
env_var = 'KIVY_{}_DEPENDENCY_WALKER'.format(provider.upper())
|
||||
if env_var not in os.environ:
|
||||
Logger.debug(
|
||||
'{0}: Failed trying to import the "{1}" provider from "{2}". '
|
||||
'This error is often encountered when a dependency is missing,'
|
||||
' or if there are multiple copies of the same dependency dll on '
|
||||
'the Windows PATH and they are incompatible with each other. '
|
||||
'This can occur if you are mixing installations (such as different'
|
||||
' python installations, like anaconda python and a system python) '
|
||||
'or if another unrelated program added its directory to the PATH. '
|
||||
'Please examine your PATH and python installation for potential '
|
||||
'issues. To further troubleshoot a "DLL load failed" error, '
|
||||
'please download '
|
||||
'"Dependency Walker" (64 or 32 bit version - matching your python '
|
||||
'bitness) from dependencywalker.com and set the environment '
|
||||
'variable {3} to the full path of the downloaded depends.exe file '
|
||||
'and rerun your application to generate an error report'.
|
||||
format(category, provider, mod_path, env_var))
|
||||
return
|
||||
|
||||
depends_bin = os.environ[env_var]
|
||||
if not os.path.exists(depends_bin):
|
||||
raise ValueError('"{}" provided in {} does not exist'.format(
|
||||
depends_bin, env_var))
|
||||
|
||||
# make file for the resultant log
|
||||
fd, temp_file = tempfile.mkstemp(
|
||||
suffix='.dwi', prefix='kivy_depends_{}_log_'.format(provider),
|
||||
dir=os.path.expanduser('~/'))
|
||||
os.close(fd)
|
||||
|
||||
Logger.info(
|
||||
'{}: Running dependency walker "{}" on "{}" to generate '
|
||||
'troubleshooting log. Please wait for it to complete'.format(
|
||||
category, depends_bin, mod_path))
|
||||
Logger.debug(
|
||||
'{}: Dependency walker command is "{}"'.format(
|
||||
category,
|
||||
[depends_bin, '/c', '/od:{}'.format(temp_file), mod_path]))
|
||||
|
||||
try:
|
||||
subprocess.check_output([
|
||||
depends_bin, '/c', '/od:{}'.format(temp_file), mod_path])
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if exc.returncode >= 0x00010000:
|
||||
Logger.error(
|
||||
'{}: Dependency walker failed with error code "{}". No '
|
||||
'error report was generated'.
|
||||
format(category, exc.returncode))
|
||||
return
|
||||
|
||||
Logger.info(
|
||||
'{}: dependency walker generated "{}" containing troubleshooting '
|
||||
'information about provider {} and its failing file "{} ({})". You '
|
||||
'can open the file in dependency walker to view any potential issues '
|
||||
'and troubleshoot it yourself. '
|
||||
'To share the file with the Kivy developers and request support, '
|
||||
'please contact us at our support channels '
|
||||
'https://kivy.org/doc/master/contact.html (not on github, unless '
|
||||
'it\'s truly a bug). Make sure to provide the generated file as well '
|
||||
'as the *complete* Kivy log being printed here. Keep in mind the '
|
||||
'generated dependency walker log file contains paths to dlls on your '
|
||||
'system used by kivy or its dependencies to help troubleshoot them, '
|
||||
'and these paths may include your name in them. Please view the '
|
||||
'log file in dependency walker before sharing to ensure you are not '
|
||||
'sharing sensitive paths'.format(
|
||||
category, temp_file, provider, mod_name, mod_path))
|
||||
@@ -0,0 +1,223 @@
|
||||
'''
|
||||
Audio
|
||||
=====
|
||||
|
||||
Load an audio sound and play it with::
|
||||
|
||||
from kivy.core.audio import SoundLoader
|
||||
|
||||
sound = SoundLoader.load('mytest.wav')
|
||||
if sound:
|
||||
print("Sound found at %s" % sound.source)
|
||||
print("Sound is %.3f seconds" % sound.length)
|
||||
sound.play()
|
||||
|
||||
You should not use the Sound class directly. The class returned by
|
||||
:func:`SoundLoader.load` will be the best sound provider for that particular
|
||||
file type, so it might return different Sound classes depending the file type.
|
||||
|
||||
Event dispatching and state changes
|
||||
-----------------------------------
|
||||
|
||||
Audio is often processed in parallel to your code. This means you often need to
|
||||
enter the Kivy :func:`eventloop <kivy.base.EventLoopBase>` in order to allow
|
||||
events and state changes to be dispatched correctly.
|
||||
|
||||
You seldom need to worry about this as Kivy apps typically always
|
||||
require this event loop for the GUI to remain responsive, but it is good to
|
||||
keep this in mind when debugging or running in a
|
||||
`REPL <https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop>`_
|
||||
(Read-eval-print loop).
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The pygst and gi providers have been removed.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
There are now 2 distinct Gstreamer implementations: one using Gi/Gst
|
||||
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
|
||||
working only for Python 2 + Gstreamer 0.10.
|
||||
|
||||
.. note::
|
||||
|
||||
The core audio library does not support recording audio. If you require
|
||||
this functionality, please refer to the
|
||||
`audiostream <https://github.com/kivy/audiostream>`_ extension.
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Sound', 'SoundLoader')
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.core import core_register_libs
|
||||
from kivy.resources import resource_find
|
||||
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
|
||||
AliasProperty, BooleanProperty, BoundedNumericProperty
|
||||
from kivy.utils import platform
|
||||
from kivy.setupconfig import USE_SDL2
|
||||
|
||||
from sys import float_info
|
||||
|
||||
|
||||
class SoundLoader:
|
||||
'''Load a sound, using the best loader for the given file type.
|
||||
'''
|
||||
|
||||
_classes = []
|
||||
|
||||
@staticmethod
|
||||
def register(classobj):
|
||||
'''Register a new class to load the sound.'''
|
||||
Logger.debug('Audio: register %s' % classobj.__name__)
|
||||
SoundLoader._classes.append(classobj)
|
||||
|
||||
@staticmethod
|
||||
def load(filename):
|
||||
'''Load a sound, and return a Sound() instance.'''
|
||||
rfn = resource_find(filename)
|
||||
if rfn is not None:
|
||||
filename = rfn
|
||||
ext = filename.split('.')[-1].lower()
|
||||
if '?' in ext:
|
||||
ext = ext.split('?')[0]
|
||||
for classobj in SoundLoader._classes:
|
||||
if ext in classobj.extensions():
|
||||
return classobj(source=filename)
|
||||
Logger.warning('Audio: Unable to find a loader for <%s>' %
|
||||
filename)
|
||||
return None
|
||||
|
||||
|
||||
class Sound(EventDispatcher):
|
||||
'''Represents a sound to play. This class is abstract, and cannot be used
|
||||
directly.
|
||||
|
||||
Use SoundLoader to load a sound.
|
||||
|
||||
:Events:
|
||||
`on_play`: None
|
||||
Fired when the sound is played.
|
||||
`on_stop`: None
|
||||
Fired when the sound is stopped.
|
||||
'''
|
||||
|
||||
source = StringProperty(None)
|
||||
'''Filename / source of your audio file.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
:attr:`source` is a :class:`~kivy.properties.StringProperty` that defaults
|
||||
to None and is read-only. Use the :meth:`SoundLoader.load` for loading
|
||||
audio.
|
||||
'''
|
||||
|
||||
volume = NumericProperty(1.)
|
||||
'''Volume, in the range 0-1. 1 means full volume, 0 means mute.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
:attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 1.
|
||||
'''
|
||||
|
||||
pitch = BoundedNumericProperty(1., min=float_info.epsilon)
|
||||
'''Pitch of a sound. 2 is an octave higher, .5 one below. This is only
|
||||
implemented for SDL2 audio provider yet.
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
:attr:`pitch` is a :class:`~kivy.properties.NumericProperty` and defaults
|
||||
to 1.
|
||||
'''
|
||||
|
||||
state = OptionProperty('stop', options=('stop', 'play'))
|
||||
'''State of the sound, one of 'stop' or 'play'.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
:attr:`state` is a read-only :class:`~kivy.properties.OptionProperty`.'''
|
||||
|
||||
loop = BooleanProperty(False)
|
||||
'''Set to True if the sound should automatically loop when it finishes.
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
:attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and defaults to
|
||||
False.'''
|
||||
|
||||
__events__ = ('on_play', 'on_stop')
|
||||
|
||||
def on_source(self, instance, filename):
|
||||
self.unload()
|
||||
if filename is None:
|
||||
return
|
||||
self.load()
|
||||
|
||||
def get_pos(self):
|
||||
'''
|
||||
Returns the current position of the audio file.
|
||||
Returns 0 if not playing.
|
||||
|
||||
.. versionadded:: 1.4.1
|
||||
'''
|
||||
return 0
|
||||
|
||||
def _get_length(self):
|
||||
return 0
|
||||
|
||||
length = property(lambda self: self._get_length(),
|
||||
doc='Get length of the sound (in seconds).')
|
||||
|
||||
def load(self):
|
||||
'''Load the file into memory.'''
|
||||
pass
|
||||
|
||||
def unload(self):
|
||||
'''Unload the file from memory.'''
|
||||
pass
|
||||
|
||||
def play(self):
|
||||
'''Play the file.'''
|
||||
self.state = 'play'
|
||||
self.dispatch('on_play')
|
||||
|
||||
def stop(self):
|
||||
'''Stop playback.'''
|
||||
self.state = 'stop'
|
||||
self.dispatch('on_stop')
|
||||
|
||||
def seek(self, position):
|
||||
'''Go to the <position> (in seconds).
|
||||
|
||||
.. note::
|
||||
Most sound providers cannot seek when the audio is stopped.
|
||||
Play then seek.
|
||||
'''
|
||||
pass
|
||||
|
||||
def on_play(self):
|
||||
pass
|
||||
|
||||
def on_stop(self):
|
||||
pass
|
||||
|
||||
|
||||
# Little trick here, don't activate gstreamer on window
|
||||
# seem to have lot of crackle or something...
|
||||
audio_libs = []
|
||||
if platform == 'android':
|
||||
audio_libs += [('android', 'audio_android')]
|
||||
elif platform in ('macosx', 'ios'):
|
||||
audio_libs += [('avplayer', 'audio_avplayer')]
|
||||
try:
|
||||
from kivy.lib.gstplayer import GstPlayer # NOQA
|
||||
audio_libs += [('gstplayer', 'audio_gstplayer')]
|
||||
except ImportError:
|
||||
pass
|
||||
audio_libs += [('ffpyplayer', 'audio_ffpyplayer')]
|
||||
if USE_SDL2:
|
||||
audio_libs += [('sdl2', 'audio_sdl2')]
|
||||
else:
|
||||
audio_libs += [('pygame', 'audio_pygame')]
|
||||
|
||||
libs_loaded = core_register_libs('audio', audio_libs)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
AudioAndroid: Kivy audio implementation for Android using native API
|
||||
"""
|
||||
|
||||
__all__ = ("SoundAndroidPlayer", )
|
||||
|
||||
from jnius import autoclass, java_method, PythonJavaClass
|
||||
from android import api_version
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
|
||||
|
||||
MediaPlayer = autoclass("android.media.MediaPlayer")
|
||||
AudioManager = autoclass("android.media.AudioManager")
|
||||
if api_version >= 21:
|
||||
AudioAttributesBuilder = autoclass("android.media.AudioAttributes$Builder")
|
||||
|
||||
|
||||
class OnCompletionListener(PythonJavaClass):
|
||||
__javainterfaces__ = ["android/media/MediaPlayer$OnCompletionListener"]
|
||||
__javacontext__ = "app"
|
||||
|
||||
def __init__(self, callback, **kwargs):
|
||||
super(OnCompletionListener, self).__init__(**kwargs)
|
||||
self.callback = callback
|
||||
|
||||
@java_method("(Landroid/media/MediaPlayer;)V")
|
||||
def onCompletion(self, mp):
|
||||
self.callback()
|
||||
|
||||
|
||||
class SoundAndroidPlayer(Sound):
|
||||
@staticmethod
|
||||
def extensions():
|
||||
return ("mp3", "mp4", "aac", "3gp", "flac", "mkv", "wav", "ogg", "m4a",
|
||||
"gsm", "mid", "xmf", "mxmf", "rtttl", "rtx", "ota", "imy")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._mediaplayer = None
|
||||
self._completion_listener = None
|
||||
super(SoundAndroidPlayer, self).__init__(**kwargs)
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
self._mediaplayer = MediaPlayer()
|
||||
if api_version >= 21:
|
||||
self._mediaplayer.setAudioAttributes(
|
||||
AudioAttributesBuilder()
|
||||
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
|
||||
.build())
|
||||
else:
|
||||
self._mediaplayer.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
||||
self._mediaplayer.setDataSource(self.source)
|
||||
self._completion_listener = OnCompletionListener(
|
||||
self._completion_callback
|
||||
)
|
||||
self._mediaplayer.setOnCompletionListener(self._completion_listener)
|
||||
self._mediaplayer.prepare()
|
||||
|
||||
def unload(self):
|
||||
if self._mediaplayer:
|
||||
self._mediaplayer.release()
|
||||
self._mediaplayer = None
|
||||
|
||||
def play(self):
|
||||
if not self._mediaplayer:
|
||||
return
|
||||
self._mediaplayer.start()
|
||||
super(SoundAndroidPlayer, self).play()
|
||||
|
||||
def stop(self):
|
||||
if not self._mediaplayer:
|
||||
return
|
||||
self._mediaplayer.stop()
|
||||
self._mediaplayer.prepare()
|
||||
|
||||
def seek(self, position):
|
||||
if not self._mediaplayer:
|
||||
return
|
||||
self._mediaplayer.seekTo(float(position) * 1000)
|
||||
|
||||
def get_pos(self):
|
||||
if self._mediaplayer:
|
||||
return self._mediaplayer.getCurrentPosition() / 1000.
|
||||
return super(SoundAndroidPlayer, self).get_pos()
|
||||
|
||||
def on_volume(self, instance, volume):
|
||||
if self._mediaplayer:
|
||||
volume = float(volume)
|
||||
self._mediaplayer.setVolume(volume, volume)
|
||||
|
||||
def _completion_callback(self):
|
||||
super(SoundAndroidPlayer, self).stop()
|
||||
|
||||
def _get_length(self):
|
||||
if self._mediaplayer:
|
||||
return self._mediaplayer.getDuration() / 1000.
|
||||
return super(SoundAndroidPlayer, self)._get_length()
|
||||
|
||||
def on_loop(self, instance, loop):
|
||||
if self._mediaplayer:
|
||||
self._mediaplayer.setLooping(loop)
|
||||
|
||||
|
||||
SoundLoader.register(SoundAndroidPlayer)
|
||||
@@ -0,0 +1,82 @@
|
||||
'''
|
||||
AudioAvplayer: implementation of Sound using pyobjus / AVFoundation.
|
||||
Works on iOS / OSX.
|
||||
'''
|
||||
|
||||
__all__ = ('SoundAvplayer', )
|
||||
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
from pyobjus import autoclass, protocol
|
||||
from pyobjus.dylib_manager import load_framework, INCLUDE
|
||||
|
||||
load_framework(INCLUDE.AVFoundation)
|
||||
AVAudioPlayer = autoclass("AVAudioPlayer")
|
||||
NSURL = autoclass("NSURL")
|
||||
NSString = autoclass("NSString")
|
||||
|
||||
|
||||
class SoundAvplayer(Sound):
|
||||
@staticmethod
|
||||
def extensions():
|
||||
# taken from https://goo.gl/015kvU
|
||||
return ("aac", "adts", "aif", "aiff", "aifc", "caf", "mp3", "mp4",
|
||||
"m4a", "snd", "au", "sd2", "wav")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._avplayer = None
|
||||
super(SoundAvplayer, self).__init__(**kwargs)
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
fn = NSString.alloc().initWithUTF8String_(self.source)
|
||||
url = NSURL.alloc().initFileURLWithPath_(fn)
|
||||
self._avplayer = AVAudioPlayer.alloc().initWithContentsOfURL_error_(
|
||||
url, None)
|
||||
|
||||
def unload(self):
|
||||
self.stop()
|
||||
self._avplayer = None
|
||||
|
||||
def play(self):
|
||||
if not self._avplayer:
|
||||
return
|
||||
self._avplayer.delegate = self
|
||||
self._avplayer.play()
|
||||
super(SoundAvplayer, self).play()
|
||||
|
||||
def stop(self):
|
||||
if not self._avplayer:
|
||||
return
|
||||
self._avplayer.delegate = None
|
||||
self._avplayer.stop()
|
||||
super(SoundAvplayer, self).stop()
|
||||
|
||||
def seek(self, position):
|
||||
if not self._avplayer:
|
||||
return
|
||||
avplayer = self._avplayer
|
||||
avplayer.stop()
|
||||
avplayer.currentTime = float(position)
|
||||
if self.state == 'play':
|
||||
avplayer.play()
|
||||
|
||||
def get_pos(self):
|
||||
if self._avplayer:
|
||||
return self._avplayer.currentTime
|
||||
return super(SoundAvplayer, self).get_pos()
|
||||
|
||||
def on_volume(self, instance, volume):
|
||||
if self._avplayer:
|
||||
self._avplayer.volume = float(volume)
|
||||
|
||||
def _get_length(self):
|
||||
if self._avplayer:
|
||||
return self._avplayer.duration
|
||||
return super(SoundAvplayer, self)._get_length()
|
||||
|
||||
@protocol("AVAudioPlayerDelegate")
|
||||
def audioPlayerDidFinishPlaying_successfully_(self, player, flag):
|
||||
self.stop()
|
||||
|
||||
|
||||
SoundLoader.register(SoundAvplayer)
|
||||
@@ -0,0 +1,185 @@
|
||||
'''
|
||||
FFmpeg based audio player
|
||||
=========================
|
||||
|
||||
To use, you need to install ffpyplayer and have a compiled ffmpeg shared
|
||||
library.
|
||||
|
||||
https://github.com/matham/ffpyplayer
|
||||
|
||||
The docs there describe how to set this up. But briefly, first you need to
|
||||
compile ffmpeg using the shared flags while disabling the static flags (you'll
|
||||
probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here's some
|
||||
instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
|
||||
can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
|
||||
Similarly, you should download SDL.
|
||||
|
||||
Now, you should a ffmpeg and sdl directory. In each, you should have a include,
|
||||
bin, and lib directory, where e.g. for Windows, lib contains the .dll.a files,
|
||||
while bin contains the actual dlls. The include directory holds the headers.
|
||||
The bin directory is only needed if the shared libraries are not already on
|
||||
the path. In the environment define FFMPEG_ROOT and SDL_ROOT, each pointing to
|
||||
the ffmpeg, and SDL directories, respectively. (If you're using SDL2,
|
||||
the include directory will contain a directory called SDL2, which then holds
|
||||
the headers).
|
||||
|
||||
Once defined, download the ffpyplayer git and run
|
||||
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
Finally, before running you need to ensure that ffpyplayer is in python's path.
|
||||
|
||||
..Note::
|
||||
|
||||
When kivy exits by closing the window while the audio is playing,
|
||||
it appears that the __del__method of SoundFFPy
|
||||
is not called. Because of this the SoundFFPy object is not
|
||||
properly deleted when kivy exits. The consequence is that because
|
||||
MediaPlayer creates internal threads which do not have their daemon
|
||||
flag set, when the main threads exists it'll hang and wait for the other
|
||||
MediaPlayer threads to exit. But since __del__ is not called to delete the
|
||||
MediaPlayer object, those threads will remain alive hanging kivy. What this
|
||||
means is that you have to be sure to delete the MediaPlayer object before
|
||||
kivy exits by setting it to None.
|
||||
'''
|
||||
|
||||
__all__ = ('SoundFFPy', )
|
||||
|
||||
try:
|
||||
import ffpyplayer
|
||||
from ffpyplayer.player import MediaPlayer
|
||||
from ffpyplayer.tools import set_log_callback, get_log_callback, formats_in
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
from kivy.weakmethod import WeakMethod
|
||||
import time
|
||||
|
||||
try:
|
||||
Logger.info(
|
||||
'SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.__version__))
|
||||
except:
|
||||
Logger.info('SoundFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
|
||||
|
||||
|
||||
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
|
||||
'fatal': Logger.critical, 'error': Logger.error,
|
||||
'warning': Logger.warning, 'info': Logger.info,
|
||||
'verbose': Logger.debug, 'debug': Logger.debug}
|
||||
|
||||
|
||||
def _log_callback(message, level):
|
||||
message = message.strip()
|
||||
if message:
|
||||
logger_func[level]('ffpyplayer: {}'.format(message))
|
||||
|
||||
|
||||
class SoundFFPy(Sound):
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
return formats_in
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._ffplayer = None
|
||||
self.quitted = False
|
||||
self._log_callback_set = False
|
||||
self._state = ''
|
||||
self.state = 'stop'
|
||||
|
||||
if not get_log_callback():
|
||||
set_log_callback(_log_callback)
|
||||
self._log_callback_set = True
|
||||
|
||||
super(SoundFFPy, self).__init__(**kwargs)
|
||||
|
||||
def __del__(self):
|
||||
self.unload()
|
||||
if self._log_callback_set:
|
||||
set_log_callback(None)
|
||||
|
||||
def _player_callback(self, selector, value):
|
||||
if self._ffplayer is None:
|
||||
return
|
||||
if selector == 'quit':
|
||||
def close(*args):
|
||||
self.quitted = True
|
||||
self.unload()
|
||||
Clock.schedule_once(close, 0)
|
||||
elif selector == 'eof':
|
||||
Clock.schedule_once(self._do_eos, 0)
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
ff_opts = {'vn': True, 'sn': True} # only audio
|
||||
self._ffplayer = MediaPlayer(self.source,
|
||||
callback=self._player_callback,
|
||||
loglevel='info', ff_opts=ff_opts)
|
||||
player = self._ffplayer
|
||||
player.set_volume(self.volume)
|
||||
player.toggle_pause()
|
||||
self._state = 'paused'
|
||||
# wait until loaded or failed, shouldn't take long, but just to make
|
||||
# sure metadata is available.
|
||||
s = time.perf_counter()
|
||||
while (player.get_metadata()['duration'] is None and
|
||||
not self.quitted and time.perf_counter() - s < 10.):
|
||||
time.sleep(0.005)
|
||||
|
||||
def unload(self):
|
||||
if self._ffplayer:
|
||||
self._ffplayer = None
|
||||
self._state = ''
|
||||
self.state = 'stop'
|
||||
self.quitted = False
|
||||
|
||||
def play(self):
|
||||
if self._state == 'playing':
|
||||
super(SoundFFPy, self).play()
|
||||
return
|
||||
if not self._ffplayer:
|
||||
self.load()
|
||||
self._ffplayer.toggle_pause()
|
||||
self._state = 'playing'
|
||||
self.state = 'play'
|
||||
super(SoundFFPy, self).play()
|
||||
self.seek(0)
|
||||
|
||||
def stop(self):
|
||||
if self._ffplayer and self._state == 'playing':
|
||||
self._ffplayer.toggle_pause()
|
||||
self._state = 'paused'
|
||||
self.state = 'stop'
|
||||
super(SoundFFPy, self).stop()
|
||||
|
||||
def seek(self, position):
|
||||
if self._ffplayer is None:
|
||||
return
|
||||
self._ffplayer.seek(position, relative=False)
|
||||
|
||||
def get_pos(self):
|
||||
if self._ffplayer is not None:
|
||||
return self._ffplayer.get_pts()
|
||||
return 0
|
||||
|
||||
def on_volume(self, instance, volume):
|
||||
if self._ffplayer is not None:
|
||||
self._ffplayer.set_volume(volume)
|
||||
|
||||
def _get_length(self):
|
||||
if self._ffplayer is None:
|
||||
return super(SoundFFPy, self)._get_length()
|
||||
return self._ffplayer.get_metadata()['duration']
|
||||
|
||||
def _do_eos(self, *args):
|
||||
if not self.loop:
|
||||
self.stop()
|
||||
else:
|
||||
self.seek(0.)
|
||||
|
||||
|
||||
SoundLoader.register(SoundFFPy)
|
||||
@@ -0,0 +1,101 @@
|
||||
'''
|
||||
Audio Gstplayer
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer`
|
||||
This player is the preferred player, using Gstreamer 1.0, working on both
|
||||
Python 2 and 3.
|
||||
'''
|
||||
|
||||
from kivy.lib.gstplayer import GstPlayer, get_gst_version
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
from kivy.logger import Logger
|
||||
from kivy.compat import PY2
|
||||
from kivy.clock import Clock
|
||||
from os.path import realpath
|
||||
|
||||
if PY2:
|
||||
from urllib import pathname2url
|
||||
else:
|
||||
from urllib.request import pathname2url
|
||||
|
||||
Logger.info('AudioGstplayer: Using Gstreamer {}'.format(
|
||||
'.'.join(map(str, get_gst_version()))))
|
||||
|
||||
|
||||
def _on_gstplayer_message(mtype, message):
|
||||
if mtype == 'error':
|
||||
Logger.error('AudioGstplayer: {}'.format(message))
|
||||
elif mtype == 'warning':
|
||||
Logger.warning('AudioGstplayer: {}'.format(message))
|
||||
elif mtype == 'info':
|
||||
Logger.info('AudioGstplayer: {}'.format(message))
|
||||
|
||||
|
||||
class SoundGstplayer(Sound):
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
return ('wav', 'ogg', 'mp3', 'm4a', 'flac', 'mp4')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.player = None
|
||||
super(SoundGstplayer, self).__init__(**kwargs)
|
||||
|
||||
def _on_gst_eos_sync(self):
|
||||
Clock.schedule_once(self._on_gst_eos, 0)
|
||||
|
||||
def _on_gst_eos(self, *dt):
|
||||
if self.loop:
|
||||
self.player.stop()
|
||||
self.player.play()
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
uri = self._get_uri()
|
||||
self.player = GstPlayer(uri, None, self._on_gst_eos_sync,
|
||||
_on_gstplayer_message)
|
||||
self.player.load()
|
||||
|
||||
def play(self):
|
||||
# we need to set the volume everytime, it seems that stopping + playing
|
||||
# the sound reset the volume.
|
||||
self.player.set_volume(self.volume)
|
||||
self.player.play()
|
||||
super(SoundGstplayer, self).play()
|
||||
|
||||
def stop(self):
|
||||
self.player.stop()
|
||||
super(SoundGstplayer, self).stop()
|
||||
|
||||
def unload(self):
|
||||
if self.player:
|
||||
self.player.unload()
|
||||
self.player = None
|
||||
|
||||
def seek(self, position):
|
||||
self.player.seek(position / self.length)
|
||||
|
||||
def get_pos(self):
|
||||
return self.player.get_position()
|
||||
|
||||
def _get_length(self):
|
||||
return self.player.get_duration()
|
||||
|
||||
def on_volume(self, instance, volume):
|
||||
self.player.set_volume(volume)
|
||||
|
||||
def _get_uri(self):
|
||||
uri = self.source
|
||||
if not uri:
|
||||
return
|
||||
if '://' not in uri:
|
||||
uri = 'file:' + pathname2url(realpath(uri))
|
||||
return uri
|
||||
|
||||
|
||||
SoundLoader.register(SoundGstplayer)
|
||||
@@ -0,0 +1,127 @@
|
||||
'''
|
||||
AudioPygame: implementation of Sound with Pygame
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('SoundPygame', )
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.utils import platform, deprecated
|
||||
from kivy.core.audio import Sound, SoundLoader
|
||||
|
||||
_platform = platform
|
||||
try:
|
||||
if _platform == 'android':
|
||||
try:
|
||||
import android.mixer as mixer
|
||||
except ImportError:
|
||||
# old python-for-android version
|
||||
import android_mixer as mixer
|
||||
else:
|
||||
from pygame import mixer
|
||||
except:
|
||||
raise
|
||||
|
||||
# init pygame sound
|
||||
mixer.pre_init(44100, -16, 2, 1024)
|
||||
mixer.init()
|
||||
mixer.set_num_channels(32)
|
||||
|
||||
|
||||
class SoundPygame(Sound):
|
||||
|
||||
# XXX we don't set __slots__ here, to automatically add
|
||||
# a dictionary. We need that to be able to use weakref for
|
||||
# SoundPygame object. Otherwise, it failed with:
|
||||
# TypeError: cannot create weak reference to 'SoundPygame' object
|
||||
# We use our clock in play() method.
|
||||
# __slots__ = ('_data', '_channel')
|
||||
_check_play_ev = None
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
if _platform == 'android':
|
||||
return ('wav', 'ogg', 'mp3', 'm4a')
|
||||
return ('wav', 'ogg')
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, **kwargs):
|
||||
self._data = None
|
||||
self._channel = None
|
||||
super(SoundPygame, self).__init__(**kwargs)
|
||||
|
||||
def _check_play(self, dt):
|
||||
if self._channel is None:
|
||||
return False
|
||||
if self._channel.get_busy():
|
||||
return
|
||||
if self.loop:
|
||||
def do_loop(dt):
|
||||
self.play()
|
||||
Clock.schedule_once(do_loop)
|
||||
else:
|
||||
self.stop()
|
||||
return False
|
||||
|
||||
def play(self):
|
||||
if not self._data:
|
||||
return
|
||||
self._data.set_volume(self.volume)
|
||||
self._channel = self._data.play()
|
||||
self.start_time = Clock.time()
|
||||
# schedule event to check if the sound is still playing or not
|
||||
self._check_play_ev = Clock.schedule_interval(self._check_play, 0.1)
|
||||
super(SoundPygame, self).play()
|
||||
|
||||
def stop(self):
|
||||
if not self._data:
|
||||
return
|
||||
self._data.stop()
|
||||
# ensure we don't have anymore the callback
|
||||
if self._check_play_ev is not None:
|
||||
self._check_play_ev.cancel()
|
||||
self._check_play_ev = None
|
||||
self._channel = None
|
||||
super(SoundPygame, self).stop()
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
if self.source is None:
|
||||
return
|
||||
self._data = mixer.Sound(self.source)
|
||||
|
||||
def unload(self):
|
||||
self.stop()
|
||||
self._data = None
|
||||
|
||||
def seek(self, position):
|
||||
if not self._data:
|
||||
return
|
||||
if _platform == 'android' and self._channel:
|
||||
self._channel.seek(position)
|
||||
|
||||
def get_pos(self):
|
||||
if self._data is not None and self._channel:
|
||||
if _platform == 'android':
|
||||
return self._channel.get_pos()
|
||||
return Clock.time() - self.start_time
|
||||
return 0
|
||||
|
||||
def on_volume(self, instance, volume):
|
||||
if self._data is not None:
|
||||
self._data.set_volume(volume)
|
||||
|
||||
def _get_length(self):
|
||||
if _platform == 'android' and self._channel:
|
||||
return self._channel.get_length()
|
||||
if self._data is not None:
|
||||
return self._data.get_length()
|
||||
return super(SoundPygame, self)._get_length()
|
||||
|
||||
|
||||
SoundLoader.register(SoundPygame)
|
||||
@@ -0,0 +1,151 @@
|
||||
'''
|
||||
Camera
|
||||
======
|
||||
|
||||
Core class for acquiring the camera and converting its input into a
|
||||
:class:`~kivy.graphics.texture.Texture`.
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The pygst and videocapture providers have been removed.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
There is now 2 distinct Gstreamer implementation: one using Gi/Gst
|
||||
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
|
||||
working only for Python 2 + Gstreamer 0.10.
|
||||
'''
|
||||
|
||||
__all__ = ('CameraBase', 'Camera')
|
||||
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.logger import Logger
|
||||
from kivy.core import core_select_lib
|
||||
|
||||
|
||||
class CameraBase(EventDispatcher):
|
||||
'''Abstract Camera Widget class.
|
||||
|
||||
Concrete camera classes must implement initialization and
|
||||
frame capturing to a buffer that can be uploaded to the gpu.
|
||||
|
||||
:Parameters:
|
||||
`index`: int
|
||||
Source index of the camera.
|
||||
`size`: tuple (int, int)
|
||||
Size at which the image is drawn. If no size is specified,
|
||||
it defaults to the resolution of the camera image.
|
||||
`resolution`: tuple (int, int)
|
||||
Resolution to try to request from the camera.
|
||||
Used in the gstreamer pipeline by forcing the appsink caps
|
||||
to this resolution. If the camera doesn't support the resolution,
|
||||
a negotiation error might be thrown.
|
||||
|
||||
:Events:
|
||||
`on_load`
|
||||
Fired when the camera is loaded and the texture has become
|
||||
available.
|
||||
`on_texture`
|
||||
Fired each time the camera texture is updated.
|
||||
'''
|
||||
|
||||
__events__ = ('on_load', 'on_texture')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('stopped', False)
|
||||
kwargs.setdefault('resolution', (640, 480))
|
||||
kwargs.setdefault('index', 0)
|
||||
|
||||
self.stopped = kwargs.get('stopped')
|
||||
self._resolution = kwargs.get('resolution')
|
||||
self._index = kwargs.get('index')
|
||||
self._buffer = None
|
||||
self._format = 'rgb'
|
||||
self._texture = None
|
||||
self.capture_device = None
|
||||
kwargs.setdefault('size', self._resolution)
|
||||
|
||||
super(CameraBase, self).__init__()
|
||||
|
||||
self.init_camera()
|
||||
|
||||
if not self.stopped:
|
||||
self.start()
|
||||
|
||||
def _set_resolution(self, res):
|
||||
self._resolution = res
|
||||
self.init_camera()
|
||||
|
||||
def _get_resolution(self):
|
||||
return self._resolution
|
||||
|
||||
resolution = property(lambda self: self._get_resolution(),
|
||||
lambda self, x: self._set_resolution(x),
|
||||
doc='Resolution of camera capture (width, height)')
|
||||
|
||||
def _set_index(self, x):
|
||||
if x == self._index:
|
||||
return
|
||||
self._index = x
|
||||
self.init_camera()
|
||||
|
||||
def _get_index(self):
|
||||
return self._x
|
||||
|
||||
index = property(lambda self: self._get_index(),
|
||||
lambda self, x: self._set_index(x),
|
||||
doc='Source index of the camera')
|
||||
|
||||
def _get_texture(self):
|
||||
return self._texture
|
||||
texture = property(lambda self: self._get_texture(),
|
||||
doc='Return the camera texture with the latest capture')
|
||||
|
||||
def init_camera(self):
|
||||
'''Initialize the camera (internal)'''
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
'''Start the camera acquire'''
|
||||
self.stopped = False
|
||||
|
||||
def stop(self):
|
||||
'''Release the camera'''
|
||||
self.stopped = True
|
||||
|
||||
def _update(self, dt):
|
||||
'''Update the camera (internal)'''
|
||||
pass
|
||||
|
||||
def _copy_to_gpu(self):
|
||||
'''Copy the buffer into the texture.'''
|
||||
if self._texture is None:
|
||||
Logger.debug('Camera: copy_to_gpu() failed, _texture is None !')
|
||||
return
|
||||
self._texture.blit_buffer(self._buffer, colorfmt=self._format)
|
||||
self._buffer = None
|
||||
self.dispatch('on_texture')
|
||||
|
||||
def on_texture(self):
|
||||
pass
|
||||
|
||||
def on_load(self):
|
||||
pass
|
||||
|
||||
|
||||
# Load the appropriate providers
|
||||
providers = ()
|
||||
|
||||
if platform in ['macosx', 'ios']:
|
||||
providers += (('avfoundation', 'camera_avfoundation',
|
||||
'CameraAVFoundation'), )
|
||||
elif platform == 'android':
|
||||
providers += (('android', 'camera_android', 'CameraAndroid'), )
|
||||
else:
|
||||
providers += (('picamera', 'camera_picamera', 'CameraPiCamera'), )
|
||||
providers += (('gi', 'camera_gi', 'CameraGi'), )
|
||||
|
||||
providers += (('opencv', 'camera_opencv', 'CameraOpenCV'), )
|
||||
|
||||
|
||||
Camera = core_select_lib('camera', (providers))
|
||||
@@ -0,0 +1,206 @@
|
||||
from jnius import autoclass, PythonJavaClass, java_method
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.graphics import Fbo, Callback, Rectangle
|
||||
from kivy.core.camera import CameraBase
|
||||
import threading
|
||||
|
||||
|
||||
Camera = autoclass('android.hardware.Camera')
|
||||
SurfaceTexture = autoclass('android.graphics.SurfaceTexture')
|
||||
GL_TEXTURE_EXTERNAL_OES = autoclass(
|
||||
'android.opengl.GLES11Ext').GL_TEXTURE_EXTERNAL_OES
|
||||
ImageFormat = autoclass('android.graphics.ImageFormat')
|
||||
|
||||
|
||||
class PreviewCallback(PythonJavaClass):
|
||||
"""
|
||||
Interface used to get back the preview frame of the Android Camera
|
||||
"""
|
||||
__javainterfaces__ = ('android.hardware.Camera$PreviewCallback', )
|
||||
|
||||
def __init__(self, callback):
|
||||
super(PreviewCallback, self).__init__()
|
||||
self._callback = callback
|
||||
|
||||
@java_method('([BLandroid/hardware/Camera;)V')
|
||||
def onPreviewFrame(self, data, camera):
|
||||
self._callback(data, camera)
|
||||
|
||||
|
||||
class CameraAndroid(CameraBase):
|
||||
"""
|
||||
Implementation of CameraBase using Android API
|
||||
"""
|
||||
|
||||
_update_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._android_camera = None
|
||||
self._preview_cb = PreviewCallback(self._on_preview_frame)
|
||||
self._buflock = threading.Lock()
|
||||
super(CameraAndroid, self).__init__(**kwargs)
|
||||
|
||||
def __del__(self):
|
||||
self._release_camera()
|
||||
|
||||
def init_camera(self):
|
||||
self._release_camera()
|
||||
self._android_camera = Camera.open(self._index)
|
||||
params = self._android_camera.getParameters()
|
||||
width, height = self._resolution
|
||||
params.setPreviewSize(width, height)
|
||||
supported_focus_modes = self._android_camera.getParameters() \
|
||||
.getSupportedFocusModes()
|
||||
if supported_focus_modes.contains('continuous-picture'):
|
||||
params.setFocusMode('continuous-picture')
|
||||
self._android_camera.setParameters(params)
|
||||
# self._android_camera.setDisplayOrientation()
|
||||
self.fps = 30.
|
||||
|
||||
pf = params.getPreviewFormat()
|
||||
assert pf == ImageFormat.NV21 # default format is NV21
|
||||
self._bufsize = int(ImageFormat.getBitsPerPixel(pf) / 8. *
|
||||
width * height)
|
||||
|
||||
self._camera_texture = Texture(width=width, height=height,
|
||||
target=GL_TEXTURE_EXTERNAL_OES,
|
||||
colorfmt='rgba')
|
||||
self._surface_texture = SurfaceTexture(int(self._camera_texture.id))
|
||||
self._android_camera.setPreviewTexture(self._surface_texture)
|
||||
|
||||
self._fbo = Fbo(size=self._resolution)
|
||||
self._fbo['resolution'] = (float(width), float(height))
|
||||
self._fbo.shader.fs = '''
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
/* Outputs from the vertex shader */
|
||||
varying vec4 frag_color;
|
||||
varying vec2 tex_coord0;
|
||||
|
||||
/* uniform texture samplers */
|
||||
uniform sampler2D texture0;
|
||||
uniform samplerExternalOES texture1;
|
||||
uniform vec2 resolution;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 coord = vec2(tex_coord0.y * (
|
||||
resolution.y / resolution.x), 1. -tex_coord0.x);
|
||||
gl_FragColor = texture2D(texture1, tex_coord0);
|
||||
}
|
||||
'''
|
||||
with self._fbo:
|
||||
self._texture_cb = Callback(lambda instr:
|
||||
self._camera_texture.bind)
|
||||
Rectangle(size=self._resolution)
|
||||
|
||||
def _release_camera(self):
|
||||
if self._android_camera is None:
|
||||
return
|
||||
|
||||
self.stop()
|
||||
self._android_camera.release()
|
||||
self._android_camera = None
|
||||
|
||||
# clear texture and it'll be reset in `_update` pointing to new FBO
|
||||
self._texture = None
|
||||
del self._fbo, self._surface_texture, self._camera_texture
|
||||
|
||||
def _on_preview_frame(self, data, camera):
|
||||
with self._buflock:
|
||||
if self._buffer is not None:
|
||||
# add buffer back for reuse
|
||||
self._android_camera.addCallbackBuffer(self._buffer)
|
||||
self._buffer = data
|
||||
# check if frame grabbing works
|
||||
# print self._buffer, len(self.frame_data)
|
||||
|
||||
def _refresh_fbo(self):
|
||||
self._texture_cb.ask_update()
|
||||
self._fbo.draw()
|
||||
|
||||
def start(self):
|
||||
super(CameraAndroid, self).start()
|
||||
|
||||
with self._buflock:
|
||||
self._buffer = None
|
||||
for k in range(2): # double buffer
|
||||
buf = b'\x00' * self._bufsize
|
||||
self._android_camera.addCallbackBuffer(buf)
|
||||
self._android_camera.setPreviewCallbackWithBuffer(self._preview_cb)
|
||||
|
||||
self._android_camera.startPreview()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = Clock.schedule_interval(self._update, 1 / self.fps)
|
||||
|
||||
def stop(self):
|
||||
super(CameraAndroid, self).stop()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = None
|
||||
self._android_camera.stopPreview()
|
||||
|
||||
self._android_camera.setPreviewCallbackWithBuffer(None)
|
||||
# buffer queue cleared as well, to be recreated on next start
|
||||
with self._buflock:
|
||||
self._buffer = None
|
||||
|
||||
def _update(self, dt):
|
||||
self._surface_texture.updateTexImage()
|
||||
self._refresh_fbo()
|
||||
if self._texture is None:
|
||||
self._texture = self._fbo.texture
|
||||
self.dispatch('on_load')
|
||||
self._copy_to_gpu()
|
||||
|
||||
def _copy_to_gpu(self):
|
||||
"""
|
||||
A dummy placeholder (the image is already in GPU) to be consistent
|
||||
with other providers.
|
||||
"""
|
||||
self.dispatch('on_texture')
|
||||
|
||||
def grab_frame(self):
|
||||
"""
|
||||
Grab current frame (thread-safe, minimal overhead)
|
||||
"""
|
||||
with self._buflock:
|
||||
if self._buffer is None:
|
||||
return None
|
||||
buf = self._buffer.tostring()
|
||||
return buf
|
||||
|
||||
def decode_frame(self, buf):
|
||||
"""
|
||||
Decode image data from grabbed frame.
|
||||
|
||||
This method depends on OpenCV and NumPy - however it is only used for
|
||||
fetching the current frame as a NumPy array, and not required when
|
||||
this :class:`CameraAndroid` provider is simply used by a
|
||||
:class:`~kivy.uix.camera.Camera` widget.
|
||||
"""
|
||||
import numpy as np
|
||||
from cv2 import cvtColor
|
||||
|
||||
w, h = self._resolution
|
||||
arr = np.fromstring(buf, 'uint8').reshape((h + h // 2, w))
|
||||
arr = cvtColor(arr, 93) # NV21 -> BGR
|
||||
return arr
|
||||
|
||||
def read_frame(self):
|
||||
"""
|
||||
Grab and decode frame in one call
|
||||
"""
|
||||
return self.decode_frame(self.grab_frame())
|
||||
|
||||
@staticmethod
|
||||
def get_camera_count():
|
||||
"""
|
||||
Get the number of available cameras.
|
||||
"""
|
||||
return Camera.getNumberOfCameras()
|
||||
@@ -0,0 +1,170 @@
|
||||
'''
|
||||
Gi Camera
|
||||
=========
|
||||
|
||||
Implement CameraBase with Gi / Gstreamer, working on both Python 2 and 3
|
||||
'''
|
||||
|
||||
__all__ = ('CameraGi', )
|
||||
|
||||
from gi.repository import Gst
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.core.camera import CameraBase
|
||||
from kivy.support import install_gobject_iteration
|
||||
from kivy.logger import Logger
|
||||
from ctypes import Structure, c_void_p, c_int, string_at
|
||||
from weakref import ref
|
||||
import atexit
|
||||
|
||||
# initialize the camera/gi. if the older version is used, don't use camera_gi.
|
||||
Gst.init(None)
|
||||
version = Gst.version()
|
||||
if version < (1, 0, 0, 0):
|
||||
raise Exception('Cannot use camera_gi, Gstreamer < 1.0 is not supported.')
|
||||
Logger.info('CameraGi: Using Gstreamer {}'.format(
|
||||
'.'.join(['{}'.format(x) for x in Gst.version()])))
|
||||
install_gobject_iteration()
|
||||
|
||||
|
||||
class _MapInfo(Structure):
|
||||
_fields_ = [
|
||||
('memory', c_void_p),
|
||||
('flags', c_int),
|
||||
('data', c_void_p)]
|
||||
# we don't care about the rest
|
||||
|
||||
|
||||
def _on_cameragi_unref(obj):
|
||||
if obj in CameraGi._instances:
|
||||
CameraGi._instances.remove(obj)
|
||||
|
||||
|
||||
class CameraGi(CameraBase):
|
||||
'''Implementation of CameraBase using GStreamer
|
||||
|
||||
:Parameters:
|
||||
`video_src`: str, default is 'v4l2src'
|
||||
Other tested options are: 'dc1394src' for firewire
|
||||
dc camera (e.g. firefly MV). Any gstreamer video source
|
||||
should potentially work.
|
||||
Theoretically a longer string using "!" can be used
|
||||
describing the first part of a gstreamer pipeline.
|
||||
'''
|
||||
|
||||
_instances = []
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._pipeline = None
|
||||
self._camerasink = None
|
||||
self._decodebin = None
|
||||
self._texturesize = None
|
||||
self._video_src = kwargs.get('video_src', 'v4l2src')
|
||||
wk = ref(self, _on_cameragi_unref)
|
||||
CameraGi._instances.append(wk)
|
||||
super(CameraGi, self).__init__(**kwargs)
|
||||
|
||||
def init_camera(self):
|
||||
# TODO: This doesn't work when camera resolution is resized at runtime.
|
||||
# There must be some other way to release the camera?
|
||||
if self._pipeline:
|
||||
self._pipeline = None
|
||||
|
||||
video_src = self._video_src
|
||||
if video_src == 'v4l2src':
|
||||
video_src += ' device=/dev/video%d' % self._index
|
||||
elif video_src == 'dc1394src':
|
||||
video_src += ' camera-number=%d' % self._index
|
||||
|
||||
if Gst.version() < (1, 0, 0, 0):
|
||||
caps = ('video/x-raw-rgb,red_mask=(int)0xff0000,'
|
||||
'green_mask=(int)0x00ff00,blue_mask=(int)0x0000ff')
|
||||
pl = ('{} ! decodebin name=decoder ! ffmpegcolorspace ! '
|
||||
'appsink name=camerasink emit-signals=True caps={}')
|
||||
else:
|
||||
caps = 'video/x-raw,format=RGB'
|
||||
pl = '{} ! decodebin name=decoder ! videoconvert ! appsink ' + \
|
||||
'name=camerasink emit-signals=True caps={}'
|
||||
|
||||
self._pipeline = Gst.parse_launch(pl.format(video_src, caps))
|
||||
self._camerasink = self._pipeline.get_by_name('camerasink')
|
||||
self._camerasink.connect('new-sample', self._gst_new_sample)
|
||||
self._decodebin = self._pipeline.get_by_name('decoder')
|
||||
|
||||
if self._camerasink and not self.stopped:
|
||||
self.start()
|
||||
|
||||
def _gst_new_sample(self, *largs):
|
||||
sample = self._camerasink.emit('pull-sample')
|
||||
if sample is None:
|
||||
return False
|
||||
|
||||
self._sample = sample
|
||||
|
||||
if self._texturesize is None:
|
||||
# try to get the camera image size
|
||||
for pad in self._decodebin.srcpads:
|
||||
s = pad.get_current_caps().get_structure(0)
|
||||
self._texturesize = (
|
||||
s.get_value('width'),
|
||||
s.get_value('height'))
|
||||
Clock.schedule_once(self._update)
|
||||
return False
|
||||
|
||||
Clock.schedule_once(self._update)
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
super(CameraGi, self).start()
|
||||
self._pipeline.set_state(Gst.State.PLAYING)
|
||||
|
||||
def stop(self):
|
||||
super(CameraGi, self).stop()
|
||||
self._pipeline.set_state(Gst.State.PAUSED)
|
||||
|
||||
def unload(self):
|
||||
self._pipeline.set_state(Gst.State.NULL)
|
||||
|
||||
def _update(self, dt):
|
||||
sample, self._sample = self._sample, None
|
||||
if sample is None:
|
||||
return
|
||||
|
||||
if self._texture is None and self._texturesize is not None:
|
||||
self._texture = Texture.create(
|
||||
size=self._texturesize, colorfmt='rgb')
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
# decode sample
|
||||
# read the data from the buffer memory
|
||||
try:
|
||||
buf = sample.get_buffer()
|
||||
result, mapinfo = buf.map(Gst.MapFlags.READ)
|
||||
|
||||
# We cannot get the data out of mapinfo, using Gst 1.0.6 + Gi 3.8.0
|
||||
# related bug report:
|
||||
# https://bugzilla.gnome.org/show_bug.cgi?id=6t8663
|
||||
# ie: mapinfo.data is normally a char*, but here, we have an int
|
||||
# So right now, we use ctypes instead to read the mapinfo ourself.
|
||||
addr = mapinfo.__hash__()
|
||||
c_mapinfo = _MapInfo.from_address(addr)
|
||||
|
||||
# now get the memory
|
||||
self._buffer = string_at(c_mapinfo.data, mapinfo.size)
|
||||
self._copy_to_gpu()
|
||||
finally:
|
||||
if mapinfo is not None:
|
||||
buf.unmap(mapinfo)
|
||||
|
||||
|
||||
@atexit.register
|
||||
def camera_gi_clean():
|
||||
# if we leave the python process with some video running, we can hit a
|
||||
# segfault. This is forcing the stop/unload of all remaining videos before
|
||||
# exiting the python process.
|
||||
for weakcamera in CameraGi._instances:
|
||||
camera = weakcamera()
|
||||
if isinstance(camera, CameraGi):
|
||||
camera.stop()
|
||||
camera.unload()
|
||||
@@ -0,0 +1,163 @@
|
||||
'''
|
||||
OpenCV Camera: Implement CameraBase with OpenCV
|
||||
'''
|
||||
|
||||
#
|
||||
# TODO: make usage of thread or multiprocess
|
||||
#
|
||||
|
||||
from __future__ import division
|
||||
|
||||
__all__ = ('CameraOpenCV')
|
||||
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.core.camera import CameraBase
|
||||
|
||||
try:
|
||||
# opencv 1 case
|
||||
import opencv as cv
|
||||
|
||||
try:
|
||||
import opencv.highgui as hg
|
||||
except ImportError:
|
||||
class Hg(object):
|
||||
'''
|
||||
On OSX, not only are the import names different,
|
||||
but the API also differs.
|
||||
There is no module called 'highgui' but the names are
|
||||
directly available in the 'cv' module.
|
||||
Some of them even have a different names.
|
||||
|
||||
Therefore we use this proxy object.
|
||||
'''
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr.startswith('cv'):
|
||||
attr = attr[2:]
|
||||
got = getattr(cv, attr)
|
||||
return got
|
||||
|
||||
hg = Hg()
|
||||
|
||||
except ImportError:
|
||||
# opencv 2 case (and also opencv 3, because it still uses cv2 module name)
|
||||
try:
|
||||
import cv2
|
||||
# here missing this OSX specific highgui thing.
|
||||
# I'm not on OSX so don't know if it is still valid in opencv >= 2
|
||||
except ImportError:
|
||||
raise
|
||||
|
||||
|
||||
class CameraOpenCV(CameraBase):
|
||||
'''
|
||||
Implementation of CameraBase using OpenCV
|
||||
'''
|
||||
_update_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# we will need it, because constants have
|
||||
# different access paths between ver. 2 and 3
|
||||
try:
|
||||
self.opencvMajorVersion = int(cv.__version__[0])
|
||||
except NameError:
|
||||
self.opencvMajorVersion = int(cv2.__version__[0])
|
||||
|
||||
self._device = None
|
||||
super(CameraOpenCV, self).__init__(**kwargs)
|
||||
|
||||
def init_camera(self):
|
||||
# consts have changed locations between versions 2 and 3
|
||||
if self.opencvMajorVersion in (3, 4):
|
||||
PROPERTY_WIDTH = cv2.CAP_PROP_FRAME_WIDTH
|
||||
PROPERTY_HEIGHT = cv2.CAP_PROP_FRAME_HEIGHT
|
||||
PROPERTY_FPS = cv2.CAP_PROP_FPS
|
||||
elif self.opencvMajorVersion == 2:
|
||||
PROPERTY_WIDTH = cv2.cv.CV_CAP_PROP_FRAME_WIDTH
|
||||
PROPERTY_HEIGHT = cv2.cv.CV_CAP_PROP_FRAME_HEIGHT
|
||||
PROPERTY_FPS = cv2.cv.CV_CAP_PROP_FPS
|
||||
elif self.opencvMajorVersion == 1:
|
||||
PROPERTY_WIDTH = cv.CV_CAP_PROP_FRAME_WIDTH
|
||||
PROPERTY_HEIGHT = cv.CV_CAP_PROP_FRAME_HEIGHT
|
||||
PROPERTY_FPS = cv.CV_CAP_PROP_FPS
|
||||
|
||||
Logger.debug('Using opencv ver.' + str(self.opencvMajorVersion))
|
||||
|
||||
if self.opencvMajorVersion == 1:
|
||||
# create the device
|
||||
self._device = hg.cvCreateCameraCapture(self._index)
|
||||
# Set preferred resolution
|
||||
cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_WIDTH,
|
||||
self.resolution[0])
|
||||
cv.SetCaptureProperty(self._device, cv.CV_CAP_PROP_FRAME_HEIGHT,
|
||||
self.resolution[1])
|
||||
# and get frame to check if it's ok
|
||||
frame = hg.cvQueryFrame(self._device)
|
||||
# Just set the resolution to the frame we just got, but don't use
|
||||
# self.resolution for that as that would cause an infinite
|
||||
# recursion with self.init_camera (but slowly as we'd have to
|
||||
# always get a frame).
|
||||
self._resolution = (int(frame.width), int(frame.height))
|
||||
# get fps
|
||||
self.fps = cv.GetCaptureProperty(self._device, cv.CV_CAP_PROP_FPS)
|
||||
|
||||
elif self.opencvMajorVersion in (2, 3, 4):
|
||||
# create the device
|
||||
self._device = cv2.VideoCapture(self._index)
|
||||
# Set preferred resolution
|
||||
self._device.set(PROPERTY_WIDTH,
|
||||
self.resolution[0])
|
||||
self._device.set(PROPERTY_HEIGHT,
|
||||
self.resolution[1])
|
||||
# and get frame to check if it's ok
|
||||
ret, frame = self._device.read()
|
||||
|
||||
# source:
|
||||
# http://stackoverflow.com/questions/32468371/video-capture-propid-parameters-in-opencv # noqa
|
||||
self._resolution = (int(frame.shape[1]), int(frame.shape[0]))
|
||||
# get fps
|
||||
self.fps = self._device.get(PROPERTY_FPS)
|
||||
|
||||
if self.fps == 0 or self.fps == 1:
|
||||
self.fps = 1.0 / 30
|
||||
elif self.fps > 1:
|
||||
self.fps = 1.0 / self.fps
|
||||
|
||||
if not self.stopped:
|
||||
self.start()
|
||||
|
||||
def _update(self, dt):
|
||||
if self.stopped:
|
||||
return
|
||||
if self._texture is None:
|
||||
# Create the texture
|
||||
self._texture = Texture.create(self._resolution)
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
try:
|
||||
ret, frame = self._device.read()
|
||||
self._format = 'bgr'
|
||||
try:
|
||||
self._buffer = frame.imageData
|
||||
except AttributeError:
|
||||
# frame is already of type ndarray
|
||||
# which can be reshaped to 1-d.
|
||||
self._buffer = frame.reshape(-1)
|
||||
self._copy_to_gpu()
|
||||
except:
|
||||
Logger.exception('OpenCV: Couldn\'t get image from Camera')
|
||||
|
||||
def start(self):
|
||||
super(CameraOpenCV, self).start()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = Clock.schedule_interval(self._update, self.fps)
|
||||
|
||||
def stop(self):
|
||||
super(CameraOpenCV, self).stop()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = None
|
||||
@@ -0,0 +1,96 @@
|
||||
'''
|
||||
PiCamera Camera: Implement CameraBase with PiCamera
|
||||
'''
|
||||
|
||||
#
|
||||
# TODO: make usage of thread or multiprocess
|
||||
#
|
||||
|
||||
__all__ = ('CameraPiCamera', )
|
||||
|
||||
from math import ceil
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.core.camera import CameraBase
|
||||
|
||||
from picamera import PiCamera
|
||||
import numpy
|
||||
|
||||
|
||||
class CameraPiCamera(CameraBase):
|
||||
'''Implementation of CameraBase using PiCamera
|
||||
'''
|
||||
_update_ev = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._camera = None
|
||||
self._format = 'bgr'
|
||||
self._framerate = kwargs.get('framerate', 30)
|
||||
super(CameraPiCamera, self).__init__(**kwargs)
|
||||
|
||||
def init_camera(self):
|
||||
if self._camera is not None:
|
||||
self._camera.close()
|
||||
|
||||
self._camera = PiCamera()
|
||||
self._camera.resolution = self.resolution
|
||||
self._camera.framerate = self._framerate
|
||||
self._camera.iso = 800
|
||||
|
||||
self.fps = 1. / self._framerate
|
||||
|
||||
if not self.stopped:
|
||||
self.start()
|
||||
|
||||
def raw_buffer_size(self):
|
||||
'''Round buffer size up to 32x16 blocks.
|
||||
|
||||
See https://picamera.readthedocs.io/en/release-1.13/recipes2.html#capturing-to-a-numpy-array
|
||||
''' # noqa
|
||||
return (
|
||||
ceil(self.resolution[0] / 32.) * 32,
|
||||
ceil(self.resolution[1] / 16.) * 16
|
||||
)
|
||||
|
||||
def _update(self, dt):
|
||||
if self.stopped:
|
||||
return
|
||||
|
||||
if self._texture is None:
|
||||
# Create the texture
|
||||
self._texture = Texture.create(self._resolution)
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
try:
|
||||
bufsize = self.raw_buffer_size()
|
||||
output = numpy.empty(
|
||||
(bufsize[0] * bufsize[1] * 3,), dtype=numpy.uint8)
|
||||
self._camera.capture(output, self._format, use_video_port=True)
|
||||
|
||||
# Trim the buffer to fit the actual requested resolution.
|
||||
# TODO: Is there a simpler way to do all this reshuffling?
|
||||
output = output.reshape((bufsize[0], bufsize[1], 3))
|
||||
output = output[:self.resolution[0], :self.resolution[1], :]
|
||||
self._buffer = output.reshape(
|
||||
(self.resolution[0] * self.resolution[1] * 3,))
|
||||
|
||||
self._copy_to_gpu()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
Logger.exception('PiCamera: Couldn\'t get image from Camera')
|
||||
|
||||
def start(self):
|
||||
super(CameraPiCamera, self).start()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = Clock.schedule_interval(self._update, self.fps)
|
||||
|
||||
def stop(self):
|
||||
super(CameraPiCamera, self).stop()
|
||||
if self._update_ev is not None:
|
||||
self._update_ev.cancel()
|
||||
self._update_ev = None
|
||||
@@ -0,0 +1,157 @@
|
||||
'''
|
||||
Clipboard
|
||||
=========
|
||||
|
||||
Core class for accessing the Clipboard. If we are not able to access the
|
||||
system clipboard, a fake one will be used.
|
||||
|
||||
Usage example:
|
||||
|
||||
.. code-block:: kv
|
||||
|
||||
#:import Clipboard kivy.core.clipboard.Clipboard
|
||||
|
||||
Button:
|
||||
on_release:
|
||||
self.text = Clipboard.paste()
|
||||
Clipboard.copy('Data')
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardBase', 'Clipboard')
|
||||
|
||||
from kivy import Logger
|
||||
from kivy.core import core_select_lib
|
||||
from kivy.utils import platform
|
||||
from kivy.setupconfig import USE_SDL2
|
||||
|
||||
|
||||
class ClipboardBase(object):
|
||||
|
||||
def get(self, mimetype):
|
||||
'''Get the current data in clipboard, using the mimetype if possible.
|
||||
You not use this method directly. Use :meth:`paste` instead.
|
||||
'''
|
||||
pass
|
||||
|
||||
def put(self, data, mimetype):
|
||||
'''Put data on the clipboard, and attach a mimetype.
|
||||
You should not use this method directly. Use :meth:`copy` instead.
|
||||
'''
|
||||
pass
|
||||
|
||||
def get_types(self):
|
||||
'''Return a list of supported mimetypes
|
||||
'''
|
||||
return []
|
||||
|
||||
def _ensure_clipboard(self):
|
||||
''' Ensure that the clipboard has been properly initialized.
|
||||
'''
|
||||
|
||||
if hasattr(self, '_clip_mime_type'):
|
||||
return
|
||||
|
||||
if platform == 'win':
|
||||
self._clip_mime_type = 'text/plain;charset=utf-8'
|
||||
# windows clipboard uses a utf-16 little endian encoding
|
||||
self._encoding = 'utf-16-le'
|
||||
elif platform == 'linux':
|
||||
self._clip_mime_type = 'text/plain;charset=utf-8'
|
||||
self._encoding = 'utf-8'
|
||||
else:
|
||||
self._clip_mime_type = 'text/plain'
|
||||
self._encoding = 'utf-8'
|
||||
|
||||
def copy(self, data=''):
|
||||
''' Copy the value provided in argument `data` into current clipboard.
|
||||
If data is not of type string it will be converted to string.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
'''
|
||||
if data:
|
||||
self._copy(data)
|
||||
|
||||
def paste(self):
|
||||
''' Get text from the system clipboard and return it a usable string.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
'''
|
||||
return self._paste()
|
||||
|
||||
def _copy(self, data):
|
||||
self._ensure_clipboard()
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode(self._encoding)
|
||||
self.put(data, self._clip_mime_type)
|
||||
|
||||
def _paste(self):
|
||||
self._ensure_clipboard()
|
||||
_clip_types = Clipboard.get_types()
|
||||
|
||||
mime_type = self._clip_mime_type
|
||||
if mime_type not in _clip_types:
|
||||
mime_type = 'text/plain'
|
||||
|
||||
data = self.get(mime_type)
|
||||
if data is not None:
|
||||
# decode only if we don't have unicode
|
||||
# we would still need to decode from utf-16 (windows)
|
||||
# data is of type bytes in PY3
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode(self._encoding, 'ignore')
|
||||
# remove null strings mostly a windows issue
|
||||
data = data.replace(u'\x00', u'')
|
||||
return data
|
||||
return u''
|
||||
|
||||
|
||||
# load clipboard implementation
|
||||
_clipboards = []
|
||||
if platform == 'android':
|
||||
_clipboards.append(
|
||||
('android', 'clipboard_android', 'ClipboardAndroid'))
|
||||
elif platform == 'macosx':
|
||||
_clipboards.append(
|
||||
('nspaste', 'clipboard_nspaste', 'ClipboardNSPaste'))
|
||||
elif platform == 'win':
|
||||
_clipboards.append(
|
||||
('winctypes', 'clipboard_winctypes', 'ClipboardWindows'))
|
||||
elif platform == 'linux':
|
||||
_clipboards.append(
|
||||
('xclip', 'clipboard_xclip', 'ClipboardXclip'))
|
||||
_clipboards.append(
|
||||
('xsel', 'clipboard_xsel', 'ClipboardXsel'))
|
||||
_clipboards.append(
|
||||
('dbusklipper', 'clipboard_dbusklipper', 'ClipboardDbusKlipper'))
|
||||
_clipboards.append(
|
||||
('gtk3', 'clipboard_gtk3', 'ClipboardGtk3'))
|
||||
|
||||
if USE_SDL2:
|
||||
_clipboards.append(
|
||||
('sdl2', 'clipboard_sdl2', 'ClipboardSDL2'))
|
||||
else:
|
||||
_clipboards.append(
|
||||
('pygame', 'clipboard_pygame', 'ClipboardPygame'))
|
||||
|
||||
_clipboards.append(
|
||||
('dummy', 'clipboard_dummy', 'ClipboardDummy'))
|
||||
|
||||
Clipboard = core_select_lib('clipboard', _clipboards, True)
|
||||
CutBuffer = None
|
||||
|
||||
if platform == 'linux':
|
||||
_cutbuffers = [
|
||||
('xclip', 'clipboard_xclip', 'ClipboardXclip'),
|
||||
('xsel', 'clipboard_xsel', 'ClipboardXsel'),
|
||||
]
|
||||
|
||||
if Clipboard.__class__.__name__ in (c[2] for c in _cutbuffers):
|
||||
CutBuffer = Clipboard
|
||||
else:
|
||||
CutBuffer = core_select_lib('cutbuffer', _cutbuffers, True,
|
||||
basemodule='clipboard')
|
||||
|
||||
if CutBuffer:
|
||||
Logger.info('CutBuffer: cut buffer support enabled')
|
||||
@@ -0,0 +1,36 @@
|
||||
'''
|
||||
Clipboard ext: base class for external command clipboards
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardExternalBase', )
|
||||
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
|
||||
class ClipboardExternalBase(ClipboardBase):
|
||||
@staticmethod
|
||||
def _clip(inout, selection):
|
||||
raise NotImplementedError('clip method not implemented')
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
p = self._clip('out', 'clipboard')
|
||||
data, _ = p.communicate()
|
||||
return data
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
p = self._clip('in', 'clipboard')
|
||||
p.communicate(data)
|
||||
|
||||
def get_cutbuffer(self):
|
||||
p = self._clip('out', 'primary')
|
||||
data, _ = p.communicate()
|
||||
return data.decode('utf8')
|
||||
|
||||
def set_cutbuffer(self, data):
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf8')
|
||||
p = self._clip('in', 'primary')
|
||||
p.communicate(data)
|
||||
|
||||
def get_types(self):
|
||||
return [u'text/plain']
|
||||
@@ -0,0 +1,91 @@
|
||||
'''
|
||||
Clipboard Android
|
||||
=================
|
||||
|
||||
Android implementation of Clipboard provider, using Pyjnius.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardAndroid', )
|
||||
|
||||
from kivy import Logger
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
from jnius import autoclass, cast
|
||||
from android.runnable import run_on_ui_thread
|
||||
from android import python_act
|
||||
|
||||
AndroidString = autoclass('java.lang.String')
|
||||
PythonActivity = python_act
|
||||
Context = autoclass('android.content.Context')
|
||||
VER = autoclass('android.os.Build$VERSION')
|
||||
sdk = VER.SDK_INT
|
||||
|
||||
|
||||
class ClipboardAndroid(ClipboardBase):
|
||||
|
||||
def __init__(self):
|
||||
super(ClipboardAndroid, self).__init__()
|
||||
self._clipboard = None
|
||||
self._data = dict()
|
||||
self._data['text/plain'] = None
|
||||
self._data['application/data'] = None
|
||||
PythonActivity._clipboard = None
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
return self._get(mimetype).encode('utf-8')
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
self._set(data, mimetype)
|
||||
|
||||
def get_types(self):
|
||||
return list(self._data.keys())
|
||||
|
||||
@run_on_ui_thread
|
||||
def _initialize_clipboard(self):
|
||||
PythonActivity._clipboard = cast(
|
||||
'android.app.Activity',
|
||||
PythonActivity.mActivity).getSystemService(
|
||||
Context.CLIPBOARD_SERVICE)
|
||||
|
||||
def _get_clipboard(f):
|
||||
def called(*args, **kargs):
|
||||
self = args[0]
|
||||
if not PythonActivity._clipboard:
|
||||
self._initialize_clipboard()
|
||||
import time
|
||||
while not PythonActivity._clipboard:
|
||||
time.sleep(.01)
|
||||
return f(*args, **kargs)
|
||||
return called
|
||||
|
||||
@_get_clipboard
|
||||
def _get(self, mimetype='text/plain'):
|
||||
clippy = PythonActivity._clipboard
|
||||
data = ''
|
||||
if sdk < 11:
|
||||
data = clippy.getText()
|
||||
else:
|
||||
ClipDescription = autoclass('android.content.ClipDescription')
|
||||
primary_clip = clippy.getPrimaryClip()
|
||||
if primary_clip:
|
||||
try:
|
||||
data = primary_clip.getItemAt(0)
|
||||
if data:
|
||||
data = data.coerceToText(
|
||||
PythonActivity.mActivity.getApplicationContext())
|
||||
except Exception:
|
||||
Logger.exception('Clipboard: failed to paste')
|
||||
return data
|
||||
|
||||
@_get_clipboard
|
||||
def _set(self, data, mimetype):
|
||||
clippy = PythonActivity._clipboard
|
||||
|
||||
if sdk < 11:
|
||||
# versions previous to honeycomb
|
||||
clippy.setText(AndroidString(data))
|
||||
else:
|
||||
ClipData = autoclass('android.content.ClipData')
|
||||
new_clip = ClipData.newPlainText(AndroidString(""),
|
||||
AndroidString(data))
|
||||
# put text data onto clipboard
|
||||
clippy.setPrimaryClip(new_clip)
|
||||
@@ -0,0 +1,41 @@
|
||||
'''
|
||||
Clipboard Dbus: an implementation of the Clipboard using dbus and klipper.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardDbusKlipper', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
if platform != 'linux':
|
||||
raise SystemError('unsupported platform for dbus kde clipboard')
|
||||
|
||||
try:
|
||||
import dbus
|
||||
bus = dbus.SessionBus()
|
||||
proxy = bus.get_object("org.kde.klipper", "/klipper")
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
class ClipboardDbusKlipper(ClipboardBase):
|
||||
|
||||
_is_init = False
|
||||
|
||||
def init(self):
|
||||
if ClipboardDbusKlipper._is_init:
|
||||
return
|
||||
self.iface = dbus.Interface(proxy, "org.kde.klipper.klipper")
|
||||
ClipboardDbusKlipper._is_init = True
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
self.init()
|
||||
return str(self.iface.getClipboardContents())
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
self.init()
|
||||
self.iface.setClipboardContents(data.replace('\x00', ''))
|
||||
|
||||
def get_types(self):
|
||||
self.init()
|
||||
return [u'text/plain']
|
||||
@@ -0,0 +1,26 @@
|
||||
'''
|
||||
Clipboard Dummy: an internal implementation that does not use the system
|
||||
clipboard.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardDummy', )
|
||||
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
|
||||
class ClipboardDummy(ClipboardBase):
|
||||
|
||||
def __init__(self):
|
||||
super(ClipboardDummy, self).__init__()
|
||||
self._data = dict()
|
||||
self._data['text/plain'] = None
|
||||
self._data['application/data'] = None
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
return self._data.get(mimetype, None)
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
self._data[mimetype] = data
|
||||
|
||||
def get_types(self):
|
||||
return list(self._data.keys())
|
||||
@@ -0,0 +1,47 @@
|
||||
'''
|
||||
Clipboard Gtk3: an implementation of the Clipboard using Gtk3.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardGtk3',)
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.support import install_gobject_iteration
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
if platform != 'linux':
|
||||
raise SystemError('unsupported platform for gtk3 clipboard')
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
|
||||
|
||||
class ClipboardGtk3(ClipboardBase):
|
||||
|
||||
_is_init = False
|
||||
|
||||
def init(self):
|
||||
if self._is_init:
|
||||
return
|
||||
install_gobject_iteration()
|
||||
self._is_init = True
|
||||
|
||||
def get(self, mimetype='text/plain;charset=utf-8'):
|
||||
self.init()
|
||||
if mimetype == 'text/plain;charset=utf-8':
|
||||
contents = clipboard.wait_for_text()
|
||||
if contents:
|
||||
return contents
|
||||
return ''
|
||||
|
||||
def put(self, data, mimetype='text/plain;charset=utf-8'):
|
||||
self.init()
|
||||
if mimetype == 'text/plain;charset=utf-8':
|
||||
text = data.decode(self._encoding)
|
||||
clipboard.set_text(text, -1)
|
||||
clipboard.store()
|
||||
|
||||
def get_types(self):
|
||||
self.init()
|
||||
return ['text/plain;charset=utf-8']
|
||||
@@ -0,0 +1,44 @@
|
||||
'''
|
||||
Clipboard OsX: implementation of clipboard using Appkit
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardNSPaste', )
|
||||
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
from kivy.utils import platform
|
||||
|
||||
if platform != 'macosx':
|
||||
raise SystemError('Unsupported platform for appkit clipboard.')
|
||||
try:
|
||||
from pyobjus import autoclass
|
||||
from pyobjus.dylib_manager import load_framework, INCLUDE
|
||||
load_framework(INCLUDE.AppKit)
|
||||
except ImportError:
|
||||
raise SystemError('Pyobjus not installed. Please run the following'
|
||||
' command to install it. `pip install --user pyobjus`')
|
||||
|
||||
NSPasteboard = autoclass('NSPasteboard')
|
||||
NSString = autoclass('NSString')
|
||||
|
||||
|
||||
class ClipboardNSPaste(ClipboardBase):
|
||||
|
||||
def __init__(self):
|
||||
super(ClipboardNSPaste, self).__init__()
|
||||
self._clipboard = NSPasteboard.generalPasteboard()
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
pb = self._clipboard
|
||||
data = pb.stringForType_('public.utf8-plain-text')
|
||||
if not data:
|
||||
return ""
|
||||
return data.UTF8String()
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
pb = self._clipboard
|
||||
pb.clearContents()
|
||||
utf8 = NSString.alloc().initWithUTF8String_(data)
|
||||
pb.setString_forType_(utf8, 'public.utf8-plain-text')
|
||||
|
||||
def get_types(self):
|
||||
return list('text/plain',)
|
||||
@@ -0,0 +1,67 @@
|
||||
'''
|
||||
Clipboard Pygame: an implementation of the Clipboard using pygame.scrap.
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardPygame', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
from kivy.utils import deprecated
|
||||
|
||||
if platform not in ('win', 'linux', 'macosx'):
|
||||
raise SystemError('unsupported platform for pygame clipboard')
|
||||
|
||||
try:
|
||||
import pygame
|
||||
import pygame.scrap
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
class ClipboardPygame(ClipboardBase):
|
||||
|
||||
_is_init = False
|
||||
_types = None
|
||||
|
||||
_aliases = {
|
||||
'text/plain;charset=utf-8': 'UTF8_STRING'
|
||||
}
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, *largs, **kwargs):
|
||||
super(ClipboardPygame, self).__init__(*largs, **kwargs)
|
||||
|
||||
def init(self):
|
||||
if ClipboardPygame._is_init:
|
||||
return
|
||||
pygame.scrap.init()
|
||||
ClipboardPygame._is_init = True
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
self.init()
|
||||
mimetype = self._aliases.get(mimetype, mimetype)
|
||||
text = pygame.scrap.get(mimetype)
|
||||
return text
|
||||
|
||||
def put(self, data, mimetype='text/plain'):
|
||||
self.init()
|
||||
mimetype = self._aliases.get(mimetype, mimetype)
|
||||
pygame.scrap.put(mimetype, data)
|
||||
|
||||
def get_types(self):
|
||||
if not self._types:
|
||||
self.init()
|
||||
types = pygame.scrap.get_types()
|
||||
for mime, pygtype in list(self._aliases.items())[:]:
|
||||
if mime in types:
|
||||
del self._aliases[mime]
|
||||
if pygtype in types:
|
||||
types.append(mime)
|
||||
self._types = types
|
||||
return self._types
|
||||
@@ -0,0 +1,36 @@
|
||||
'''
|
||||
Clipboard SDL2: an implementation of the Clipboard using sdl2.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardSDL2', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
if platform not in ('win', 'linux', 'macosx', 'android', 'ios'):
|
||||
raise SystemError('unsupported platform for sdl2 clipboard')
|
||||
|
||||
try:
|
||||
from kivy.core.clipboard._clipboard_sdl2 import (
|
||||
_get_text, _has_text, _set_text)
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'Clipboard', 'sdl2', 'kivy.core.clipboard._clipboard_sdl2')
|
||||
raise
|
||||
|
||||
|
||||
class ClipboardSDL2(ClipboardBase):
|
||||
|
||||
def get(self, mimetype):
|
||||
return _get_text() if _has_text() else ''
|
||||
|
||||
def _ensure_clipboard(self):
|
||||
super(ClipboardSDL2, self)._ensure_clipboard()
|
||||
self._encoding = 'utf8'
|
||||
|
||||
def put(self, data=b'', mimetype='text/plain'):
|
||||
_set_text(data)
|
||||
|
||||
def get_types(self):
|
||||
return ['text/plain']
|
||||
@@ -0,0 +1,107 @@
|
||||
'''
|
||||
Clipboard windows: an implementation of the Clipboard using ctypes.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardWindows', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard import ClipboardBase
|
||||
|
||||
if platform != 'win':
|
||||
raise SystemError('unsupported platform for Windows clipboard')
|
||||
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
msvcrt = ctypes.cdll.msvcrt
|
||||
c_char_p = ctypes.c_char_p
|
||||
c_wchar_p = ctypes.c_wchar_p
|
||||
|
||||
GlobalLock = kernel32.GlobalLock
|
||||
GlobalLock.argtypes = [wintypes.HGLOBAL]
|
||||
GlobalLock.restype = wintypes.LPVOID
|
||||
|
||||
GlobalUnlock = kernel32.GlobalUnlock
|
||||
GlobalUnlock.argtypes = [wintypes.HGLOBAL]
|
||||
GlobalUnlock.restype = wintypes.BOOL
|
||||
|
||||
CF_UNICODETEXT = 13
|
||||
GMEM_MOVEABLE = 0x0002
|
||||
|
||||
|
||||
class ClipboardWindows(ClipboardBase):
|
||||
|
||||
def _copy(self, data):
|
||||
self._ensure_clipboard()
|
||||
self.put(data, self._clip_mime_type)
|
||||
|
||||
def get(self, mimetype='text/plain'):
|
||||
GetClipboardData = user32.GetClipboardData
|
||||
GetClipboardData.argtypes = [wintypes.UINT]
|
||||
GetClipboardData.restype = wintypes.HANDLE
|
||||
|
||||
user32.OpenClipboard(user32.GetActiveWindow())
|
||||
|
||||
# GetClipboardData returns a HANDLE to the clipboard data
|
||||
# which is a memory object containing the data
|
||||
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata # noqa: E501
|
||||
pcontents = GetClipboardData(CF_UNICODETEXT)
|
||||
|
||||
# if someone pastes a FILE, the content is None for SCF 13
|
||||
# and the clipboard is locked if not closed properly
|
||||
if not pcontents:
|
||||
user32.CloseClipboard()
|
||||
return ''
|
||||
|
||||
# The handle returned by GetClipboardData is a memory object
|
||||
# and needs to be locked to get the actual pointer to the data
|
||||
pcontents_locked = GlobalLock(pcontents)
|
||||
data = c_wchar_p(pcontents_locked).value
|
||||
GlobalUnlock(pcontents)
|
||||
|
||||
user32.CloseClipboard()
|
||||
return data
|
||||
|
||||
def put(self, text, mimetype="text/plain"):
|
||||
SetClipboardData = user32.SetClipboardData
|
||||
SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE]
|
||||
SetClipboardData.restype = wintypes.HANDLE
|
||||
|
||||
GlobalAlloc = kernel32.GlobalAlloc
|
||||
GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t]
|
||||
GlobalAlloc.restype = wintypes.HGLOBAL
|
||||
|
||||
user32.OpenClipboard(user32.GetActiveWindow())
|
||||
user32.EmptyClipboard()
|
||||
|
||||
# The wsclen function returns the number of
|
||||
# wide characters in a string (not including the null character)
|
||||
text_len = msvcrt.wcslen(text) + 1
|
||||
|
||||
# According to the docs regarding SetClipboardDatam, if the hMem
|
||||
# parameter identifies a memory object, the object must have
|
||||
# been allocated using the GMEM_MOVEABLE flag.
|
||||
# See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata # noqa: E501
|
||||
# The size of the memory object is the number of wide characters in
|
||||
# the string plus one for the terminating null character
|
||||
hCd = GlobalAlloc(
|
||||
GMEM_MOVEABLE, ctypes.sizeof(ctypes.c_wchar) * text_len
|
||||
)
|
||||
|
||||
# Since the memory object is allocated with GMEM_MOVEABLE, should be
|
||||
# locked to get the actual pointer to the data.
|
||||
hCd_locked = GlobalLock(hCd)
|
||||
ctypes.memmove(
|
||||
c_wchar_p(hCd_locked),
|
||||
c_wchar_p(text),
|
||||
ctypes.sizeof(ctypes.c_wchar) * text_len,
|
||||
)
|
||||
GlobalUnlock(hCd)
|
||||
|
||||
# Finally, set the clipboard data (and then close the clipboard)
|
||||
SetClipboardData(CF_UNICODETEXT, hCd)
|
||||
user32.CloseClipboard()
|
||||
|
||||
def get_types(self):
|
||||
return ['text/plain']
|
||||
@@ -0,0 +1,29 @@
|
||||
'''
|
||||
Clipboard xclip: an implementation of the Clipboard using xclip
|
||||
command line tool.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardXclip', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase
|
||||
|
||||
if platform != 'linux':
|
||||
raise SystemError('unsupported platform for xclip clipboard')
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
p = subprocess.Popen(['xclip', '-version'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL)
|
||||
p.communicate()
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
class ClipboardXclip(ClipboardExternalBase):
|
||||
@staticmethod
|
||||
def _clip(inout, selection):
|
||||
pipe = {'std' + inout: subprocess.PIPE}
|
||||
return subprocess.Popen(
|
||||
['xclip', '-' + inout, '-selection', selection], **pipe)
|
||||
@@ -0,0 +1,26 @@
|
||||
'''
|
||||
Clipboard xsel: an implementation of the Clipboard using xsel command line
|
||||
tool.
|
||||
'''
|
||||
|
||||
__all__ = ('ClipboardXsel', )
|
||||
|
||||
from kivy.utils import platform
|
||||
from kivy.core.clipboard._clipboard_ext import ClipboardExternalBase
|
||||
|
||||
if platform != 'linux':
|
||||
raise SystemError('unsupported platform for xsel clipboard')
|
||||
|
||||
import subprocess
|
||||
p = subprocess.Popen(['xsel', '--version'], stdout=subprocess.PIPE)
|
||||
p.communicate(timeout=1)
|
||||
|
||||
|
||||
class ClipboardXsel(ClipboardExternalBase):
|
||||
@staticmethod
|
||||
def _clip(inout, selection):
|
||||
pipe = {'std' + inout: subprocess.PIPE}
|
||||
sel = 'b' if selection == 'clipboard' else selection[0]
|
||||
io = inout[0]
|
||||
return subprocess.Popen(
|
||||
['xsel', '-' + sel + io], **pipe)
|
||||
@@ -0,0 +1,90 @@
|
||||
# pylint: disable=W0611
|
||||
'''
|
||||
OpenGL
|
||||
======
|
||||
|
||||
Select and use the best OpenGL library available. Depending on your system, the
|
||||
core provider can select an OpenGL ES or a 'classic' desktop OpenGL library.
|
||||
'''
|
||||
|
||||
import sys
|
||||
from os import environ
|
||||
|
||||
MIN_REQUIRED_GL_VERSION = (2, 0)
|
||||
|
||||
|
||||
def msgbox(message):
|
||||
if sys.platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
ctypes.windll.user32.MessageBoxW(None, LPCWSTR(message),
|
||||
u"Kivy Fatal Error", 0)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if 'KIVY_DOC' not in environ:
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.graphics import gl_init_resources
|
||||
from kivy.graphics.opengl_utils import gl_get_version
|
||||
from kivy.graphics.opengl import (
|
||||
GL_VERSION,
|
||||
GL_VENDOR,
|
||||
GL_RENDERER,
|
||||
GL_MAX_TEXTURE_IMAGE_UNITS,
|
||||
GL_MAX_TEXTURE_SIZE,
|
||||
GL_SHADING_LANGUAGE_VERSION,
|
||||
glGetString,
|
||||
glGetIntegerv,
|
||||
gl_init_symbols,
|
||||
)
|
||||
from kivy.graphics.cgl import cgl_get_initialized_backend_name
|
||||
from kivy.utils import platform
|
||||
|
||||
def init_gl(allowed=[], ignored=[]):
|
||||
gl_init_symbols(allowed, ignored)
|
||||
print_gl_version()
|
||||
gl_init_resources()
|
||||
|
||||
def print_gl_version():
|
||||
backend = cgl_get_initialized_backend_name()
|
||||
Logger.info('GL: Backend used <{}>'.format(backend))
|
||||
version = glGetString(GL_VERSION)
|
||||
vendor = glGetString(GL_VENDOR)
|
||||
renderer = glGetString(GL_RENDERER)
|
||||
Logger.info('GL: OpenGL version <{0}>'.format(version))
|
||||
Logger.info('GL: OpenGL vendor <{0}>'.format(vendor))
|
||||
Logger.info('GL: OpenGL renderer <{0}>'.format(renderer))
|
||||
|
||||
# Let the user know if his graphics hardware/drivers are too old
|
||||
major, minor = gl_get_version()
|
||||
Logger.info('GL: OpenGL parsed version: %d, %d' % (major, minor))
|
||||
if ((major, minor) < MIN_REQUIRED_GL_VERSION and backend != "mock"):
|
||||
if hasattr(sys, "_kivy_opengl_required_func"):
|
||||
sys._kivy_opengl_required_func(major, minor, version, vendor,
|
||||
renderer)
|
||||
else:
|
||||
msg = (
|
||||
'GL: Minimum required OpenGL version (2.0) NOT found!\n\n'
|
||||
'OpenGL version detected: {0}.{1}\n\n'
|
||||
'Version: {2}\nVendor: {3}\nRenderer: {4}\n\n'
|
||||
'Try upgrading your graphics drivers and/or your '
|
||||
'graphics hardware in case of problems.\n\n'
|
||||
'The application will leave now.').format(
|
||||
major, minor, version, vendor, renderer)
|
||||
Logger.critical(msg)
|
||||
msgbox(msg)
|
||||
|
||||
if platform != 'android':
|
||||
# XXX in the android emulator (latest version at 22 march 2013),
|
||||
# this call was segfaulting the gl stack.
|
||||
Logger.info('GL: Shading version <{0}>'.format(glGetString(
|
||||
GL_SHADING_LANGUAGE_VERSION)))
|
||||
Logger.info('GL: Texture max size <{0}>'.format(glGetIntegerv(
|
||||
GL_MAX_TEXTURE_SIZE)[0]))
|
||||
Logger.info('GL: Texture max units <{0}>'.format(glGetIntegerv(
|
||||
GL_MAX_TEXTURE_IMAGE_UNITS)[0]))
|
||||
|
||||
# To be able to use our GL provider, we must have a window
|
||||
# Automatically import window auto to ensure the default window creation
|
||||
import kivy.core.window # NOQA
|
||||
@@ -0,0 +1,996 @@
|
||||
'''
|
||||
Image
|
||||
=====
|
||||
|
||||
Core classes for loading images and converting them to a
|
||||
:class:`~kivy.graphics.texture.Texture`. The raw image data can be keep in
|
||||
memory for further access.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
|
||||
Add support for argb and abgr image data
|
||||
|
||||
In-memory image loading
|
||||
-----------------------
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
Official support for in-memory loading. Not all the providers support it,
|
||||
but currently SDL2, pygame, pil and imageio work.
|
||||
|
||||
To load an image with a filename, you would usually do::
|
||||
|
||||
from kivy.core.image import Image as CoreImage
|
||||
im = CoreImage("image.png")
|
||||
|
||||
You can also load the image data directly from a memory block. Instead of
|
||||
passing the filename, you'll need to pass the data as a BytesIO object
|
||||
together with an "ext" parameter. Both are mandatory::
|
||||
|
||||
import io
|
||||
from kivy.core.image import Image as CoreImage
|
||||
data = io.BytesIO(open("image.png", "rb").read())
|
||||
im = CoreImage(data, ext="png")
|
||||
|
||||
By default, the image will not be cached as our internal cache requires a
|
||||
filename. If you want caching, add a filename that represents your file (it
|
||||
will be used only for caching)::
|
||||
|
||||
import io
|
||||
from kivy.core.image import Image as CoreImage
|
||||
data = io.BytesIO(open("image.png", "rb").read())
|
||||
im = CoreImage(data, ext="png", filename="image.png")
|
||||
|
||||
Saving an image
|
||||
---------------
|
||||
|
||||
A CoreImage can be saved to a file::
|
||||
|
||||
from kivy.core.image import Image as CoreImage
|
||||
image = CoreImage(...)
|
||||
image.save("/tmp/test.png")
|
||||
|
||||
Or you can get the bytes (new in `1.11.0`):
|
||||
|
||||
import io
|
||||
from kivy.core.image import Image as CoreImage
|
||||
data = io.BytesIO()
|
||||
image = CoreImage(...)
|
||||
image.save(data, fmt="png")
|
||||
png_bytes = data.read()
|
||||
|
||||
'''
|
||||
import re
|
||||
from base64 import b64decode
|
||||
from filetype import guess_extension
|
||||
|
||||
__all__ = ('Image', 'ImageLoader', 'ImageData')
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.core import core_register_libs
|
||||
from kivy.logger import Logger
|
||||
from kivy.cache import Cache
|
||||
from kivy.clock import Clock
|
||||
from kivy.atlas import Atlas
|
||||
from kivy.resources import resource_find
|
||||
from kivy.utils import platform
|
||||
from kivy.compat import string_types
|
||||
from kivy.setupconfig import USE_SDL2
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
|
||||
# late binding
|
||||
Texture = TextureRegion = None
|
||||
|
||||
# register image caching only for keep_data=True
|
||||
Cache.register('kv.image', timeout=60)
|
||||
Cache.register('kv.atlas')
|
||||
|
||||
|
||||
class ImageData(object):
|
||||
'''Container for images and mipmap images.
|
||||
The container will always have at least the mipmap level 0.
|
||||
'''
|
||||
|
||||
__slots__ = ('fmt', 'mipmaps', 'source', 'flip_vertical', 'source_image')
|
||||
_supported_fmts = ('rgb', 'bgr', 'rgba', 'bgra', 'argb', 'abgr',
|
||||
's3tc_dxt1', 's3tc_dxt3', 's3tc_dxt5', 'pvrtc_rgb2',
|
||||
'pvrtc_rgb4', 'pvrtc_rgba2', 'pvrtc_rgba4', 'etc1_rgb8')
|
||||
|
||||
def __init__(self, width, height, fmt, data, source=None,
|
||||
flip_vertical=True, source_image=None,
|
||||
rowlength=0):
|
||||
assert fmt in ImageData._supported_fmts
|
||||
|
||||
#: Decoded image format, one of a available texture format
|
||||
self.fmt = fmt
|
||||
|
||||
#: Data for each mipmap.
|
||||
self.mipmaps = {}
|
||||
self.add_mipmap(0, width, height, data, rowlength)
|
||||
|
||||
#: Image source, if available
|
||||
self.source = source
|
||||
|
||||
#: Indicate if the texture will need to be vertically flipped
|
||||
self.flip_vertical = flip_vertical
|
||||
|
||||
# the original image, which we might need to save if it is a memoryview
|
||||
self.source_image = source_image
|
||||
|
||||
def release_data(self):
|
||||
mm = self.mipmaps
|
||||
for item in mm.values():
|
||||
item[2] = None
|
||||
self.source_image = None
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
'''Image width in pixels.
|
||||
(If the image is mipmapped, it will use the level 0)
|
||||
'''
|
||||
return self.mipmaps[0][0]
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
'''Image height in pixels.
|
||||
(If the image is mipmapped, it will use the level 0)
|
||||
'''
|
||||
return self.mipmaps[0][1]
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
'''Image data.
|
||||
(If the image is mipmapped, it will use the level 0)
|
||||
'''
|
||||
return self.mipmaps[0][2]
|
||||
|
||||
@property
|
||||
def rowlength(self):
|
||||
'''Image rowlength.
|
||||
(If the image is mipmapped, it will use the level 0)
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
'''
|
||||
return self.mipmaps[0][3]
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
'''Image (width, height) in pixels.
|
||||
(If the image is mipmapped, it will use the level 0)
|
||||
'''
|
||||
mm = self.mipmaps[0]
|
||||
return mm[0], mm[1]
|
||||
|
||||
@property
|
||||
def have_mipmap(self):
|
||||
return len(self.mipmaps) > 1
|
||||
|
||||
def __repr__(self):
|
||||
return ('<ImageData width=%d height=%d fmt=%s '
|
||||
'source=%r with %d images>' % (
|
||||
self.width, self.height, self.fmt,
|
||||
self.source, len(self.mipmaps)))
|
||||
|
||||
def add_mipmap(self, level, width, height, data, rowlength):
|
||||
'''Add a image for a specific mipmap level.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
'''
|
||||
self.mipmaps[level] = [int(width), int(height), data, rowlength]
|
||||
|
||||
def get_mipmap(self, level):
|
||||
'''Get the mipmap image at a specific level if it exists
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
'''
|
||||
if level == 0:
|
||||
return self.width, self.height, self.data, self.rowlength
|
||||
assert level < len(self.mipmaps)
|
||||
return self.mipmaps[level]
|
||||
|
||||
def iterate_mipmaps(self):
|
||||
'''Iterate over all mipmap images available.
|
||||
|
||||
.. versionadded:: 1.0.7
|
||||
'''
|
||||
mm = self.mipmaps
|
||||
for x in range(len(mm)):
|
||||
item = mm.get(x, None)
|
||||
if item is None:
|
||||
raise Exception('Invalid mipmap level, found empty one')
|
||||
yield x, item[0], item[1], item[2], item[3]
|
||||
|
||||
|
||||
class ImageLoaderBase(object):
|
||||
'''Base to implement an image loader.'''
|
||||
|
||||
__slots__ = ('_texture', '_data', 'filename', 'keep_data',
|
||||
'_mipmap', '_nocache', '_ext', '_inline')
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
self._mipmap = kwargs.get('mipmap', False)
|
||||
self.keep_data = kwargs.get('keep_data', False)
|
||||
self._nocache = kwargs.get('nocache', False)
|
||||
self._ext = kwargs.get('ext')
|
||||
self._inline = kwargs.get('inline')
|
||||
self.filename = filename
|
||||
if self._inline:
|
||||
self._data = self.load(kwargs.get('rawdata'))
|
||||
else:
|
||||
self._data = self.load(filename)
|
||||
self._textures = None
|
||||
|
||||
def load(self, filename):
|
||||
'''Load an image'''
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def can_save(fmt, is_bytesio=False):
|
||||
'''Indicate if the loader can save the Image object
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
Parameter `fmt` and `is_bytesio` added
|
||||
'''
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def can_load_memory():
|
||||
'''Indicate if the loader can load an image by passing data
|
||||
'''
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def save(*largs, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
def populate(self):
|
||||
self._textures = []
|
||||
fname = self.filename
|
||||
if __debug__:
|
||||
Logger.trace('Image: %r, populate to textures (%d)' %
|
||||
(fname, len(self._data)))
|
||||
|
||||
for count in range(len(self._data)):
|
||||
|
||||
# first, check if a texture with the same name already exist in the
|
||||
# cache
|
||||
uid = f'{fname}|{self._mipmap:d}|{count:d}'
|
||||
texture = Cache.get('kv.texture', uid)
|
||||
|
||||
# if not create it and append to the cache
|
||||
if texture is None:
|
||||
imagedata = self._data[count]
|
||||
source = (f"{'zip|' if fname.endswith('.zip') else ''}"
|
||||
f"{self._nocache}|")
|
||||
imagedata.source = f'{source}{uid}'
|
||||
texture = Texture.create_from_data(
|
||||
imagedata, mipmap=self._mipmap)
|
||||
if not self._nocache:
|
||||
Cache.append('kv.texture', uid, texture)
|
||||
if imagedata.flip_vertical:
|
||||
texture.flip_vertical()
|
||||
|
||||
# set as our current texture
|
||||
self._textures.append(texture)
|
||||
|
||||
# release data if ask
|
||||
if not self.keep_data:
|
||||
self._data[count].release_data()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
'''Image width
|
||||
'''
|
||||
return self._data[0].width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
'''Image height
|
||||
'''
|
||||
return self._data[0].height
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
'''Image size (width, height)
|
||||
'''
|
||||
return self._data[0].width, self._data[0].height
|
||||
|
||||
@property
|
||||
def texture(self):
|
||||
'''Get the image texture (created on the first call)
|
||||
'''
|
||||
if self._textures is None:
|
||||
self.populate()
|
||||
if self._textures is None:
|
||||
return None
|
||||
return self._textures[0]
|
||||
|
||||
@property
|
||||
def textures(self):
|
||||
'''Get the textures list (for mipmapped image or animated image)
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
if self._textures is None:
|
||||
self.populate()
|
||||
return self._textures
|
||||
|
||||
@property
|
||||
def nocache(self):
|
||||
'''Indicate if the texture will not be stored in the cache
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
'''
|
||||
return self._nocache
|
||||
|
||||
|
||||
class ImageLoader(object):
|
||||
loaders = []
|
||||
|
||||
@staticmethod
|
||||
def zip_loader(filename, **kwargs):
|
||||
'''Read images from an zip file.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
Returns an Image with a list of type ImageData stored in Image._data
|
||||
'''
|
||||
# read zip in memory for faster access
|
||||
_file = BytesIO(open(filename, 'rb').read())
|
||||
# read all images inside the zip
|
||||
z = zipfile.ZipFile(_file)
|
||||
image_data = []
|
||||
# sort filename list
|
||||
znamelist = z.namelist()
|
||||
znamelist.sort()
|
||||
image = None
|
||||
for zfilename in znamelist:
|
||||
try:
|
||||
# read file and store it in mem with fileIO struct around it
|
||||
tmpfile = BytesIO(z.read(zfilename))
|
||||
ext = zfilename.split('.')[-1].lower()
|
||||
im = None
|
||||
for loader in ImageLoader.loaders:
|
||||
if (ext not in loader.extensions() or
|
||||
not loader.can_load_memory()):
|
||||
continue
|
||||
Logger.debug('Image%s: Load <%s> from <%s>' %
|
||||
(loader.__name__[11:], zfilename, filename))
|
||||
try:
|
||||
im = loader(zfilename, ext=ext, rawdata=tmpfile,
|
||||
inline=True, **kwargs)
|
||||
except:
|
||||
# Loader failed, continue trying.
|
||||
continue
|
||||
break
|
||||
if im is not None:
|
||||
# append ImageData to local variable before its
|
||||
# overwritten
|
||||
image_data.append(im._data[0])
|
||||
image = im
|
||||
# else: if not image file skip to next
|
||||
except:
|
||||
Logger.warning('Image: Unable to load image'
|
||||
'<%s> in zip <%s> trying to continue...'
|
||||
% (zfilename, filename))
|
||||
z.close()
|
||||
if len(image_data) == 0:
|
||||
raise Exception('no images in zip <%s>' % filename)
|
||||
# replace Image.Data with the array of all the images in the zip
|
||||
image._data = image_data
|
||||
image.filename = filename
|
||||
return image
|
||||
|
||||
@staticmethod
|
||||
def register(defcls):
|
||||
ImageLoader.loaders.append(defcls)
|
||||
|
||||
@staticmethod
|
||||
def load(filename, **kwargs):
|
||||
|
||||
# atlas ?
|
||||
if filename[:8] == 'atlas://':
|
||||
# remove the url
|
||||
rfn = filename[8:]
|
||||
# last field is the ID
|
||||
try:
|
||||
rfn, uid = rfn.rsplit('/', 1)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Image: Invalid %s name for atlas' % filename)
|
||||
|
||||
# search if we already got the atlas loaded
|
||||
atlas = Cache.get('kv.atlas', rfn)
|
||||
|
||||
# atlas already loaded, so reupload the missing texture in cache,
|
||||
# because when it's not in use, the texture can be removed from the
|
||||
# kv.texture cache.
|
||||
if atlas:
|
||||
texture = atlas[uid]
|
||||
fn = f'atlas://{rfn}/{uid}'
|
||||
cid = f'{fn}|0|0'
|
||||
Cache.append('kv.texture', cid, texture)
|
||||
return Image(texture)
|
||||
|
||||
# search with resource
|
||||
afn = rfn
|
||||
if not afn.endswith('.atlas'):
|
||||
afn += '.atlas'
|
||||
afn = resource_find(afn)
|
||||
if not afn:
|
||||
raise Exception('Unable to find %r atlas' % afn)
|
||||
atlas = Atlas(afn)
|
||||
Cache.append('kv.atlas', rfn, atlas)
|
||||
# first time, fill our texture cache.
|
||||
for nid, texture in atlas.textures.items():
|
||||
fn = f'atlas://{rfn}/{nid}'
|
||||
cid = f'{fn}|0|0'
|
||||
Cache.append('kv.texture', cid, texture)
|
||||
return Image(atlas[uid])
|
||||
|
||||
# extract extensions
|
||||
ext = filename.split('.')[-1].lower()
|
||||
|
||||
# prevent url querystrings
|
||||
if filename.startswith(('http://', 'https://')):
|
||||
ext = ext.split('?')[0]
|
||||
|
||||
filename = resource_find(filename)
|
||||
|
||||
# Get actual image format instead of extension if possible
|
||||
ext = guess_extension(filename) or ext
|
||||
|
||||
# special case. When we are trying to load a "zip" file with image, we
|
||||
# will use the special zip_loader in ImageLoader. This might return a
|
||||
# sequence of images contained in the zip.
|
||||
if ext == 'zip':
|
||||
return ImageLoader.zip_loader(filename)
|
||||
else:
|
||||
im = None
|
||||
for loader in ImageLoader.loaders:
|
||||
if ext not in loader.extensions():
|
||||
continue
|
||||
Logger.debug(f'Image{loader.__name__[11:]}: Load <{filename}>')
|
||||
im = loader(filename, **kwargs)
|
||||
break
|
||||
if im is None:
|
||||
raise Exception(f'Unknown <{ext}> type, no loader found.')
|
||||
return im
|
||||
|
||||
|
||||
class Image(EventDispatcher):
|
||||
'''Load an image and store the size and texture.
|
||||
|
||||
.. versionchanged:: 1.0.7
|
||||
|
||||
`mipmap` attribute has been added. The `texture_mipmap` and
|
||||
`texture_rectangle` have been deleted.
|
||||
|
||||
.. versionchanged:: 1.0.8
|
||||
|
||||
An Image widget can change its texture. A new event 'on_texture' has
|
||||
been introduced. New methods for handling sequenced animation have been
|
||||
added.
|
||||
|
||||
:Parameters:
|
||||
`arg`: can be a string (str), Texture, BytesIO or Image object
|
||||
A string path to the image file or data URI to be loaded; or a
|
||||
Texture object, which will be wrapped in an Image object; or a
|
||||
BytesIO object containing raw image data; or an already existing
|
||||
image object, in which case, a real copy of the given image object
|
||||
will be returned.
|
||||
`keep_data`: bool, defaults to False
|
||||
Keep the image data when the texture is created.
|
||||
`mipmap`: bool, defaults to False
|
||||
Create mipmap for the texture.
|
||||
`anim_delay`: float, defaults to .25
|
||||
Delay in seconds between each animation frame. Lower values means
|
||||
faster animation.
|
||||
`ext`: str, only with BytesIO `arg`
|
||||
File extension to use in determining how to load raw image data.
|
||||
`filename`: str, only with BytesIO `arg`
|
||||
Filename to use in the image cache for raw image data.
|
||||
'''
|
||||
|
||||
copy_attributes = ('_size', '_filename', '_texture', '_image',
|
||||
'_mipmap', '_nocache')
|
||||
|
||||
data_uri_re = re.compile(r'^data:image/([^;,]*)(;[^,]*)?,(.*)$')
|
||||
|
||||
_anim_ev = None
|
||||
|
||||
def __init__(self, arg, **kwargs):
|
||||
# this event should be fired on animation of sequenced img's
|
||||
self.register_event_type('on_texture')
|
||||
|
||||
super(Image, self).__init__()
|
||||
|
||||
self._mipmap = kwargs.get('mipmap', False)
|
||||
self._keep_data = kwargs.get('keep_data', False)
|
||||
self._nocache = kwargs.get('nocache', False)
|
||||
self._size = [0, 0]
|
||||
self._image = None
|
||||
self._filename = None
|
||||
self._texture = None
|
||||
self._anim_available = False
|
||||
self._anim_index = 0
|
||||
self._anim_delay = 0
|
||||
self.anim_delay = kwargs.get('anim_delay', .25)
|
||||
# indicator of images having been loded in cache
|
||||
self._iteration_done = False
|
||||
|
||||
if isinstance(arg, Image):
|
||||
for attr in Image.copy_attributes:
|
||||
self.__setattr__(attr, arg.__getattribute__(attr))
|
||||
elif type(arg) in (Texture, TextureRegion):
|
||||
if not hasattr(self, 'textures'):
|
||||
self.textures = []
|
||||
self.textures.append(arg)
|
||||
self._texture = arg
|
||||
self._size = self.texture.size
|
||||
elif isinstance(arg, ImageLoaderBase):
|
||||
self.image = arg
|
||||
elif isinstance(arg, BytesIO):
|
||||
ext = kwargs.get('ext', None)
|
||||
if not ext:
|
||||
raise Exception('Inline loading require "ext" parameter')
|
||||
filename = kwargs.get('filename')
|
||||
if not filename:
|
||||
self._nocache = True
|
||||
filename = '__inline__'
|
||||
self.load_memory(arg, ext, filename)
|
||||
elif isinstance(arg, string_types):
|
||||
groups = self.data_uri_re.findall(arg)
|
||||
if groups:
|
||||
self._nocache = True
|
||||
imtype, optstr, data = groups[0]
|
||||
options = [o for o in optstr.split(';') if o]
|
||||
ext = imtype
|
||||
isb64 = 'base64' in options
|
||||
if data:
|
||||
if isb64:
|
||||
data = b64decode(data)
|
||||
self.load_memory(BytesIO(data), ext)
|
||||
else:
|
||||
self.filename = arg
|
||||
else:
|
||||
raise Exception('Unable to load image type {0!r}'.format(arg))
|
||||
|
||||
def remove_from_cache(self):
|
||||
'''Remove the Image from cache. This facilitates re-loading of
|
||||
images from disk in case the image content has changed.
|
||||
|
||||
.. versionadded:: 1.3.0
|
||||
|
||||
Usage::
|
||||
|
||||
im = CoreImage('1.jpg')
|
||||
# -- do something --
|
||||
im.remove_from_cache()
|
||||
im = CoreImage('1.jpg')
|
||||
# this time image will be re-loaded from disk
|
||||
|
||||
'''
|
||||
count = 0
|
||||
uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
|
||||
Cache.remove("kv.image", uid)
|
||||
while Cache.get("kv.texture", uid):
|
||||
Cache.remove("kv.texture", uid)
|
||||
count += 1
|
||||
uid = f'{self.filename}|{self._mipmap:d}|{count:d}'
|
||||
|
||||
def _anim(self, *largs):
|
||||
if not self._image:
|
||||
return
|
||||
textures = self.image.textures
|
||||
if self._anim_index >= len(textures):
|
||||
self._anim_index = 0
|
||||
self._texture = self.image.textures[self._anim_index]
|
||||
self.dispatch('on_texture')
|
||||
self._anim_index += 1
|
||||
self._anim_index %= len(self._image.textures)
|
||||
|
||||
def anim_reset(self, allow_anim):
|
||||
'''Reset an animation if available.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
:Parameters:
|
||||
`allow_anim`: bool
|
||||
Indicate whether the animation should restart playing or not.
|
||||
|
||||
Usage::
|
||||
|
||||
# start/reset animation
|
||||
image.anim_reset(True)
|
||||
|
||||
# or stop the animation
|
||||
image.anim_reset(False)
|
||||
|
||||
You can change the animation speed whilst it is playing::
|
||||
|
||||
# Set to 20 FPS
|
||||
image.anim_delay = 1 / 20.
|
||||
|
||||
'''
|
||||
# stop animation
|
||||
if self._anim_ev is not None:
|
||||
self._anim_ev.cancel()
|
||||
self._anim_ev = None
|
||||
|
||||
if allow_anim and self._anim_available and self._anim_delay >= 0:
|
||||
self._anim_ev = Clock.schedule_interval(self._anim,
|
||||
self.anim_delay)
|
||||
self._anim()
|
||||
|
||||
def _get_anim_delay(self):
|
||||
return self._anim_delay
|
||||
|
||||
def _set_anim_delay(self, x):
|
||||
if self._anim_delay == x:
|
||||
return
|
||||
self._anim_delay = x
|
||||
if self._anim_available:
|
||||
if self._anim_ev is not None:
|
||||
self._anim_ev.cancel()
|
||||
self._anim_ev = None
|
||||
|
||||
if self._anim_delay >= 0:
|
||||
self._anim_ev = Clock.schedule_interval(self._anim,
|
||||
self._anim_delay)
|
||||
|
||||
anim_delay = property(_get_anim_delay, _set_anim_delay)
|
||||
'''Delay between each animation frame. A lower value means faster
|
||||
animation.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
|
||||
@property
|
||||
def anim_available(self):
|
||||
'''Return True if this Image instance has animation available.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
return self._anim_available
|
||||
|
||||
@property
|
||||
def anim_index(self):
|
||||
'''Return the index number of the image currently in the texture.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
return self._anim_index
|
||||
|
||||
def _img_iterate(self, *largs):
|
||||
if not self.image or self._iteration_done:
|
||||
return
|
||||
self._iteration_done = True
|
||||
imgcount = len(self.image.textures)
|
||||
if imgcount > 1:
|
||||
self._anim_available = True
|
||||
self.anim_reset(True)
|
||||
self._texture = self.image.textures[0]
|
||||
|
||||
def on_texture(self, *largs):
|
||||
'''This event is fired when the texture reference or content has
|
||||
changed. It is normally used for sequenced images.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
'''
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def load(filename, **kwargs):
|
||||
'''Load an image
|
||||
|
||||
:Parameters:
|
||||
`filename`: str
|
||||
Filename of the image.
|
||||
`keep_data`: bool, defaults to False
|
||||
Keep the image data when the texture is created.
|
||||
'''
|
||||
kwargs.setdefault('keep_data', False)
|
||||
return Image(filename, **kwargs)
|
||||
|
||||
def _get_image(self):
|
||||
return self._image
|
||||
|
||||
def _set_image(self, image):
|
||||
self._image = image
|
||||
if hasattr(image, 'filename'):
|
||||
self._filename = image.filename
|
||||
if image:
|
||||
self._size = (self.image.width, self.image.height)
|
||||
|
||||
image = property(_get_image, _set_image,
|
||||
doc='Get/set the data image object')
|
||||
|
||||
def _get_filename(self):
|
||||
return self._filename
|
||||
|
||||
def _set_filename(self, value):
|
||||
if value is None or value == self._filename:
|
||||
return
|
||||
self._filename = value
|
||||
|
||||
# construct uid as a key for Cache
|
||||
f = self.filename
|
||||
uid = f'{f}|{self._mipmap:d}|0'
|
||||
|
||||
# in case of Image have been asked with keep_data
|
||||
# check the kv.image cache instead of texture.
|
||||
image = Cache.get('kv.image', uid)
|
||||
if image:
|
||||
# we found an image, yeah ! but reset the texture now.
|
||||
self.image = image
|
||||
# if image.__class__ is core image then it's a texture
|
||||
# from atlas or other sources and has no data so skip
|
||||
if (image.__class__ != self.__class__ and
|
||||
not image.keep_data and self._keep_data):
|
||||
self.remove_from_cache()
|
||||
self._filename = ''
|
||||
self._set_filename(value)
|
||||
else:
|
||||
self._texture = None
|
||||
return
|
||||
else:
|
||||
# if we already got a texture, it will be automatically reloaded.
|
||||
_texture = Cache.get('kv.texture', uid)
|
||||
if _texture:
|
||||
self._texture = _texture
|
||||
return
|
||||
|
||||
# if image not already in cache then load
|
||||
tmpfilename = self._filename
|
||||
image = ImageLoader.load(
|
||||
self._filename, keep_data=self._keep_data,
|
||||
mipmap=self._mipmap, nocache=self._nocache)
|
||||
self._filename = tmpfilename
|
||||
# put the image into the cache if needed
|
||||
if isinstance(image, Texture):
|
||||
self._texture = image
|
||||
self._size = image.size
|
||||
else:
|
||||
self.image = image
|
||||
if not self._nocache:
|
||||
Cache.append('kv.image', uid, self.image)
|
||||
|
||||
filename = property(_get_filename, _set_filename,
|
||||
doc='Get/set the filename of image')
|
||||
|
||||
def load_memory(self, data, ext, filename='__inline__'):
|
||||
'''(internal) Method to load an image from raw data.
|
||||
'''
|
||||
self._filename = filename
|
||||
|
||||
# see if there is a available loader for it
|
||||
loaders = [loader for loader in ImageLoader.loaders if
|
||||
loader.can_load_memory() and
|
||||
ext in loader.extensions()]
|
||||
if not loaders:
|
||||
raise Exception(f'No inline loader found to load {ext}')
|
||||
image = loaders[0](filename, ext=ext, rawdata=data, inline=True,
|
||||
nocache=self._nocache, mipmap=self._mipmap,
|
||||
keep_data=self._keep_data)
|
||||
if isinstance(image, Texture):
|
||||
self._texture = image
|
||||
self._size = image.size
|
||||
else:
|
||||
self.image = image
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
'''Image size (width, height)
|
||||
'''
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
'''Image width
|
||||
'''
|
||||
return self._size[0]
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
'''Image height
|
||||
'''
|
||||
return self._size[1]
|
||||
|
||||
@property
|
||||
def texture(self):
|
||||
'''Texture of the image'''
|
||||
if self.image:
|
||||
if not self._iteration_done:
|
||||
self._img_iterate()
|
||||
return self._texture
|
||||
|
||||
@property
|
||||
def nocache(self):
|
||||
'''Indicate whether the texture will not be stored in the cache or not.
|
||||
|
||||
.. versionadded:: 1.6.0
|
||||
'''
|
||||
return self._nocache
|
||||
|
||||
def save(self, filename, flipped=False, fmt=None):
|
||||
'''Save image texture to file.
|
||||
|
||||
The filename should have the '.png' extension because the texture data
|
||||
read from the GPU is in the RGBA format. '.jpg' might work but has not
|
||||
been heavily tested so some providers might break when using it.
|
||||
Any other extensions are not officially supported.
|
||||
|
||||
The flipped parameter flips the saved image vertically, and
|
||||
defaults to False.
|
||||
|
||||
Example::
|
||||
|
||||
# Save an core image object
|
||||
from kivy.core.image import Image
|
||||
img = Image('hello.png')
|
||||
img.save('hello2.png')
|
||||
|
||||
# Save a texture
|
||||
texture = Texture.create(...)
|
||||
img = Image(texture)
|
||||
img.save('hello3.png')
|
||||
|
||||
.. versionadded:: 1.7.0
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
Parameter `flipped` added to flip the image before saving, default
|
||||
to False.
|
||||
|
||||
.. versionchanged:: 1.11.0
|
||||
Parameter `fmt` added to force the output format of the file
|
||||
Filename can now be a BytesIO object.
|
||||
|
||||
'''
|
||||
is_bytesio = False
|
||||
if isinstance(filename, BytesIO):
|
||||
is_bytesio = True
|
||||
if not fmt:
|
||||
raise Exception(
|
||||
"You must specify a format to save into a BytesIO object")
|
||||
elif fmt is None:
|
||||
fmt = self._find_format_from_filename(filename)
|
||||
|
||||
pixels = None
|
||||
size = None
|
||||
loaders = [
|
||||
x for x in ImageLoader.loaders
|
||||
if x.can_save(fmt, is_bytesio=is_bytesio)
|
||||
]
|
||||
if not loaders:
|
||||
return False
|
||||
loader = loaders[0]
|
||||
|
||||
if self.image:
|
||||
# we might have a ImageData object to use
|
||||
data = self.image._data[0]
|
||||
if data.data is not None:
|
||||
if data.fmt in ('rgba', 'rgb'):
|
||||
# fast path, use the "raw" data when keep_data is used
|
||||
size = data.width, data.height
|
||||
pixels = data.data
|
||||
|
||||
else:
|
||||
# the format is not rgba, we need to convert it.
|
||||
# use texture for that.
|
||||
self.populate()
|
||||
|
||||
if pixels is None and self._texture:
|
||||
# use the texture pixels
|
||||
size = self._texture.size
|
||||
pixels = self._texture.pixels
|
||||
|
||||
if pixels is None:
|
||||
return False
|
||||
|
||||
l_pixels = len(pixels)
|
||||
if l_pixels == size[0] * size[1] * 3:
|
||||
pixelfmt = 'rgb'
|
||||
elif l_pixels == size[0] * size[1] * 4:
|
||||
pixelfmt = 'rgba'
|
||||
else:
|
||||
raise Exception('Unable to determine the format of the pixels')
|
||||
return loader.save(
|
||||
filename, size[0], size[1], pixelfmt, pixels, flipped, fmt)
|
||||
|
||||
def _find_format_from_filename(self, filename):
|
||||
ext = filename.rsplit(".", 1)[-1].lower()
|
||||
if (ext in
|
||||
{'bmp', 'jpe', 'lbm', 'pcx', 'png', 'pnm', 'tga',
|
||||
'tiff', 'webp', 'xcf', 'xpm', 'xv'}):
|
||||
return ext
|
||||
elif ext in ('jpg', 'jpeg'):
|
||||
return 'jpg'
|
||||
elif ext in ('b64', 'base64'):
|
||||
return 'base64'
|
||||
return None
|
||||
|
||||
def read_pixel(self, x, y):
|
||||
'''For a given local x/y position, return the pixel color at that
|
||||
position.
|
||||
|
||||
.. warning::
|
||||
This function can only be used with images loaded with the
|
||||
keep_data=True keyword. For example::
|
||||
|
||||
m = Image.load('image.png', keep_data=True)
|
||||
color = m.read_pixel(150, 150)
|
||||
|
||||
:Parameters:
|
||||
`x`: int
|
||||
Local x coordinate of the pixel in question.
|
||||
`y`: int
|
||||
Local y coordinate of the pixel in question.
|
||||
'''
|
||||
data = self.image._data[0]
|
||||
|
||||
# can't use this function without ImageData
|
||||
if data.data is None:
|
||||
raise EOFError('Image data is missing, make sure that image is'
|
||||
'loaded with keep_data=True keyword.')
|
||||
|
||||
# check bounds
|
||||
x, y = int(x), int(y)
|
||||
if not (0 <= x < data.width and 0 <= y < data.height):
|
||||
raise IndexError(f'Position ({x:d}, {y:d}) is out of range.')
|
||||
|
||||
assert data.fmt in ImageData._supported_fmts
|
||||
size = 3 if data.fmt in ('rgb', 'bgr') else 4
|
||||
index = y * data.width * size + x * size
|
||||
raw = bytearray(data.data[index:index + size])
|
||||
color = [c / 255.0 for c in raw]
|
||||
|
||||
bgr_flag = False
|
||||
if data.fmt == 'argb':
|
||||
color.reverse() # bgra
|
||||
bgr_flag = True
|
||||
elif data.fmt == 'abgr':
|
||||
color.reverse() # rgba
|
||||
|
||||
# conversion for BGR->RGB, BGRA->RGBA format
|
||||
if bgr_flag or data.fmt in ('bgr', 'bgra'):
|
||||
color[0], color[2] = color[2], color[0]
|
||||
|
||||
return color
|
||||
|
||||
|
||||
def load(filename):
|
||||
'''Load an image'''
|
||||
return Image.load(filename)
|
||||
|
||||
|
||||
# load image loaders
|
||||
image_libs = []
|
||||
|
||||
if platform in ('macosx', 'ios'):
|
||||
image_libs += [('imageio', 'img_imageio')]
|
||||
|
||||
image_libs += [
|
||||
('tex', 'img_tex'),
|
||||
('dds', 'img_dds')]
|
||||
if USE_SDL2:
|
||||
image_libs += [('sdl2', 'img_sdl2')]
|
||||
else:
|
||||
image_libs += [('pygame', 'img_pygame')]
|
||||
image_libs += [
|
||||
('ffpy', 'img_ffpyplayer'),
|
||||
('pil', 'img_pil')]
|
||||
|
||||
libs_loaded = core_register_libs('image', image_libs)
|
||||
|
||||
from os import environ
|
||||
|
||||
if 'KIVY_DOC' not in environ and not libs_loaded:
|
||||
import sys
|
||||
|
||||
Logger.critical('App: Unable to get any Image provider, abort.')
|
||||
sys.exit(1)
|
||||
|
||||
# resolve binding.
|
||||
from kivy.graphics.texture import Texture, TextureRegion
|
||||
@@ -0,0 +1,40 @@
|
||||
'''
|
||||
DDS: DDS image loader
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderDDS', )
|
||||
|
||||
from kivy.lib.ddsfile import DDSFile
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
|
||||
|
||||
class ImageLoaderDDS(ImageLoaderBase):
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
return ('dds', )
|
||||
|
||||
def load(self, filename):
|
||||
try:
|
||||
dds = DDSFile(filename=filename)
|
||||
except:
|
||||
Logger.warning('Image: Unable to load image <%s>' % filename)
|
||||
raise
|
||||
|
||||
self.filename = filename
|
||||
width, height = dds.size
|
||||
im = ImageData(width, height, dds.dxt, dds.images[0], source=filename,
|
||||
flip_vertical=False)
|
||||
if len(dds.images) > 1:
|
||||
images = dds.images
|
||||
images_size = dds.images_size
|
||||
for index in range(1, len(dds.images)):
|
||||
w, h = images_size[index]
|
||||
data = images[index]
|
||||
im.add_mipmap(index, w, h, data)
|
||||
return [im]
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderDDS)
|
||||
@@ -0,0 +1,87 @@
|
||||
'''
|
||||
FFPyPlayer: FFmpeg based image loader
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderFFPy', )
|
||||
|
||||
import ffpyplayer
|
||||
from ffpyplayer.pic import ImageLoader as ffImageLoader, SWScale
|
||||
from ffpyplayer.tools import set_log_callback, get_log_callback
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
|
||||
|
||||
Logger.info('ImageLoaderFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
|
||||
|
||||
|
||||
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
|
||||
'fatal': Logger.critical, 'error': Logger.error,
|
||||
'warning': Logger.warning, 'info': Logger.info,
|
||||
'verbose': Logger.debug, 'debug': Logger.debug}
|
||||
|
||||
|
||||
def _log_callback(message, level):
|
||||
message = message.strip()
|
||||
if message:
|
||||
logger_func[level]('ffpyplayer: {}'.format(message))
|
||||
|
||||
|
||||
if not get_log_callback():
|
||||
set_log_callback(_log_callback)
|
||||
|
||||
|
||||
class ImageLoaderFFPy(ImageLoaderBase):
|
||||
'''Image loader based on the ffpyplayer library.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. note:
|
||||
This provider may support more formats than what is listed in
|
||||
:meth:`extensions`.
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
'''Return accepted extensions for this loader'''
|
||||
# See https://www.ffmpeg.org/general.html#Image-Formats
|
||||
return ('bmp', 'dpx', 'exr', 'gif', 'ico', 'jpeg', 'jpg2000', 'jpg',
|
||||
'jls', 'pam', 'pbm', 'pcx', 'pgm', 'pgmyuv', 'pic', 'png',
|
||||
'ppm', 'ptx', 'sgi', 'ras', 'tga', 'tiff', 'webp', 'xbm',
|
||||
'xface', 'xwd')
|
||||
|
||||
def load(self, filename):
|
||||
try:
|
||||
loader = ffImageLoader(filename)
|
||||
except:
|
||||
Logger.warning('Image: Unable to load image <%s>' % filename)
|
||||
raise
|
||||
|
||||
# update internals
|
||||
self.filename = filename
|
||||
images = []
|
||||
|
||||
while True:
|
||||
frame, t = loader.next_frame()
|
||||
if frame is None:
|
||||
break
|
||||
images.append(frame)
|
||||
if not len(images):
|
||||
raise Exception('No image found in {}'.format(filename))
|
||||
|
||||
w, h = images[0].get_size()
|
||||
ifmt = images[0].get_pixel_format()
|
||||
if ifmt != 'rgba' and ifmt != 'rgb24':
|
||||
fmt = 'rgba'
|
||||
sws = SWScale(w, h, ifmt, ofmt=fmt)
|
||||
for i, image in enumerate(images):
|
||||
images[i] = sws.scale(image)
|
||||
else:
|
||||
fmt = ifmt if ifmt == 'rgba' else 'rgb'
|
||||
|
||||
return [ImageData(w, h, fmt, img.to_memoryview()[0], source_image=img)
|
||||
for img in images]
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderFFPy)
|
||||
@@ -0,0 +1,123 @@
|
||||
'''
|
||||
PIL: PIL image loader
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderPIL', )
|
||||
|
||||
try:
|
||||
import Image as PILImage
|
||||
except ImportError:
|
||||
# for python3
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
|
||||
try:
|
||||
# Pillow
|
||||
PILImage.frombytes
|
||||
PILImage.Image.tobytes
|
||||
except AttributeError:
|
||||
# PIL
|
||||
# monkey patch frombytes and tobytes methods, refs:
|
||||
# https://github.com/kivy/kivy/issues/5460
|
||||
PILImage.frombytes = PILImage.frombuffer
|
||||
PILImage.Image.tobytes = PILImage.Image.tostring
|
||||
|
||||
|
||||
class ImageLoaderPIL(ImageLoaderBase):
|
||||
'''Image loader based on the PIL library.
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
Support for GIF animation added.
|
||||
|
||||
Gif animation has a lot of issues(transparency/color depths... etc).
|
||||
In order to keep it simple, what is implemented here is what is
|
||||
natively supported by the PIL library.
|
||||
|
||||
As a general rule, try to use gifs that have no transparency.
|
||||
Gif's with transparency will work but be prepared for some
|
||||
artifacts until transparency support is improved.
|
||||
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def can_save(fmt, is_bytesio):
|
||||
if is_bytesio:
|
||||
return False
|
||||
return fmt in ImageLoaderPIL.extensions()
|
||||
|
||||
@staticmethod
|
||||
def can_load_memory():
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
'''Return accepted extensions for this loader'''
|
||||
PILImage.init()
|
||||
return tuple((ext_with_dot[1:] for ext_with_dot in PILImage.EXTENSION))
|
||||
|
||||
def _img_correct(self, _img_tmp):
|
||||
'''Convert image to the correct format and orientation.
|
||||
'''
|
||||
# image loader work only with rgb/rgba image
|
||||
if _img_tmp.mode.lower() not in ('rgb', 'rgba'):
|
||||
try:
|
||||
imc = _img_tmp.convert('RGBA')
|
||||
except:
|
||||
Logger.warning(
|
||||
'Image: Unable to convert image to rgba (was %s)' %
|
||||
(_img_tmp.mode.lower()))
|
||||
raise
|
||||
_img_tmp = imc
|
||||
|
||||
return _img_tmp
|
||||
|
||||
def _img_read(self, im):
|
||||
'''Read images from an animated file.
|
||||
'''
|
||||
im.seek(0)
|
||||
|
||||
# Read all images inside
|
||||
try:
|
||||
img_ol = None
|
||||
while True:
|
||||
img_tmp = im
|
||||
img_tmp = self._img_correct(img_tmp)
|
||||
if img_ol and (hasattr(im, 'dispose') and not im.dispose):
|
||||
# paste new frame over old so as to handle
|
||||
# transparency properly
|
||||
img_ol.paste(img_tmp, (0, 0), img_tmp)
|
||||
img_tmp = img_ol
|
||||
img_ol = img_tmp
|
||||
yield ImageData(img_tmp.size[0], img_tmp.size[1],
|
||||
img_tmp.mode.lower(), img_tmp.tobytes())
|
||||
im.seek(im.tell() + 1)
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
def load(self, filename):
|
||||
try:
|
||||
im = PILImage.open(filename)
|
||||
except:
|
||||
Logger.warning('Image: Unable to load image <%s>' % filename)
|
||||
raise
|
||||
# update internals
|
||||
if not self._inline:
|
||||
self.filename = filename
|
||||
# returns an array of type ImageData len 1 if not a sequence image
|
||||
return list(self._img_read(im))
|
||||
|
||||
@staticmethod
|
||||
def save(filename, width, height, pixelfmt, pixels, flipped=False,
|
||||
imagefmt=None):
|
||||
image = PILImage.frombytes(pixelfmt.upper(), (width, height), pixels)
|
||||
if flipped:
|
||||
image = image.transpose(PILImage.FLIP_TOP_BOTTOM)
|
||||
image.save(filename)
|
||||
return True
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderPIL)
|
||||
@@ -0,0 +1,119 @@
|
||||
'''
|
||||
Pygame: Pygame image loader
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderPygame', )
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
from os.path import isfile
|
||||
from kivy.utils import deprecated
|
||||
|
||||
try:
|
||||
import pygame
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
class ImageLoaderPygame(ImageLoaderBase):
|
||||
'''Image loader based on the PIL library'''
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, *largs, **kwargs):
|
||||
super(ImageLoaderPygame, self).__init__(*largs, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
'''Return accepted extensions for this loader'''
|
||||
# under OS X, i got with "pygame.error: File is not a Windows BMP
|
||||
# file". documentation said: The image module is a required dependency
|
||||
# of Pygame, but it only optionally supports any extended file formats.
|
||||
# By default it can only load uncompressed BMP image
|
||||
if pygame.image.get_extended() == 0:
|
||||
return ('bmp', )
|
||||
return ('jpg', 'jpeg', 'jpe', 'png', 'bmp', 'pcx', 'tga', 'tiff',
|
||||
'tif', 'lbm', 'pbm', 'ppm', 'xpm')
|
||||
|
||||
@staticmethod
|
||||
def can_save(fmt, is_bytesio):
|
||||
if is_bytesio:
|
||||
return False
|
||||
return fmt in ('png', 'jpg')
|
||||
|
||||
@staticmethod
|
||||
def can_load_memory():
|
||||
return True
|
||||
|
||||
def load(self, filename):
|
||||
if not filename:
|
||||
import traceback
|
||||
traceback.print_stack()
|
||||
return
|
||||
try:
|
||||
im = None
|
||||
if self._inline:
|
||||
im = pygame.image.load(filename, 'x.{}'.format(self._ext))
|
||||
elif isfile(filename):
|
||||
with open(filename, 'rb') as fd:
|
||||
im = pygame.image.load(fd)
|
||||
elif isinstance(filename, bytes):
|
||||
try:
|
||||
fname = filename.decode()
|
||||
if isfile(fname):
|
||||
with open(fname, 'rb') as fd:
|
||||
im = pygame.image.load(fd)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if im is None:
|
||||
im = pygame.image.load(filename)
|
||||
except:
|
||||
# Logger.warning(type(filename)('Image: Unable to load image <%s>')
|
||||
# % filename)
|
||||
raise
|
||||
|
||||
fmt = ''
|
||||
if im.get_bytesize() == 3 and not im.get_colorkey():
|
||||
fmt = 'rgb'
|
||||
elif im.get_bytesize() == 4:
|
||||
fmt = 'rgba'
|
||||
|
||||
# image loader work only with rgb/rgba image
|
||||
if fmt not in ('rgb', 'rgba'):
|
||||
try:
|
||||
imc = im.convert(32)
|
||||
fmt = 'rgba'
|
||||
except:
|
||||
try:
|
||||
imc = im.convert_alpha()
|
||||
fmt = 'rgba'
|
||||
except:
|
||||
Logger.warning(
|
||||
'Image: Unable to convert image %r to rgba (was %r)' %
|
||||
(filename, im.fmt))
|
||||
raise
|
||||
im = imc
|
||||
|
||||
# update internals
|
||||
if not self._inline:
|
||||
self.filename = filename
|
||||
data = pygame.image.tostring(im, fmt.upper())
|
||||
return [ImageData(im.get_width(), im.get_height(),
|
||||
fmt, data, source=filename)]
|
||||
|
||||
@staticmethod
|
||||
def save(filename, width, height, pixelfmt, pixels, flipped,
|
||||
imagefmt=None):
|
||||
surface = pygame.image.fromstring(
|
||||
pixels, (width, height), pixelfmt.upper(), flipped)
|
||||
pygame.image.save(surface, filename)
|
||||
return True
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderPygame)
|
||||
@@ -0,0 +1,66 @@
|
||||
'''
|
||||
SDL2 image loader
|
||||
=================
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderSDL2', )
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
try:
|
||||
from kivy.core.image import _img_sdl2
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'image', 'sdl2', 'kivy.core.image._img_sdl2')
|
||||
raise
|
||||
|
||||
|
||||
class ImageLoaderSDL2(ImageLoaderBase):
|
||||
'''Image loader based on SDL2_image'''
|
||||
|
||||
def _ensure_ext(self):
|
||||
_img_sdl2.init()
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
'''Return accepted extensions for this loader'''
|
||||
return ('bmp', 'jpg', 'jpeg', 'jpe', 'lbm', 'pcx', 'png', 'pnm',
|
||||
'tga', 'tiff', 'webp', 'xcf', 'xpm', 'xv')
|
||||
|
||||
@staticmethod
|
||||
def can_save(fmt, is_bytesio):
|
||||
return fmt in ('jpg', 'png')
|
||||
|
||||
@staticmethod
|
||||
def can_load_memory():
|
||||
return True
|
||||
|
||||
def load(self, filename):
|
||||
if self._inline:
|
||||
data = filename.read()
|
||||
info = _img_sdl2.load_from_memory(data)
|
||||
else:
|
||||
info = _img_sdl2.load_from_filename(filename)
|
||||
if not info:
|
||||
Logger.warning('Image: Unable to load image <%s>' % filename)
|
||||
raise Exception('SDL2: Unable to load image')
|
||||
|
||||
w, h, fmt, pixels, rowlength = info
|
||||
|
||||
# update internals
|
||||
if not self._inline:
|
||||
self.filename = filename
|
||||
return [ImageData(
|
||||
w, h, fmt, pixels, source=filename,
|
||||
rowlength=rowlength)]
|
||||
|
||||
@staticmethod
|
||||
def save(filename, width, height, pixelfmt, pixels, flipped, imagefmt):
|
||||
_img_sdl2.save(filename, width, height, pixelfmt, pixels, flipped,
|
||||
imagefmt)
|
||||
return True
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderSDL2)
|
||||
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
Tex: Compressed texture
|
||||
'''
|
||||
|
||||
__all__ = ('ImageLoaderTex', )
|
||||
|
||||
import json
|
||||
from struct import unpack
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.image import ImageLoaderBase, ImageData, ImageLoader
|
||||
|
||||
|
||||
class ImageLoaderTex(ImageLoaderBase):
|
||||
|
||||
@staticmethod
|
||||
def extensions():
|
||||
return ('tex', )
|
||||
|
||||
def load(self, filename):
|
||||
try:
|
||||
fd = open(filename, 'rb')
|
||||
if fd.read(4) != 'KTEX':
|
||||
raise Exception('Invalid tex identifier')
|
||||
|
||||
headersize = unpack('I', fd.read(4))[0]
|
||||
header = fd.read(headersize)
|
||||
if len(header) != headersize:
|
||||
raise Exception('Truncated tex header')
|
||||
|
||||
info = json.loads(header)
|
||||
data = fd.read()
|
||||
if len(data) != info['datalen']:
|
||||
raise Exception('Truncated tex data')
|
||||
|
||||
except:
|
||||
Logger.warning('Image: Image <%s> is corrupted' % filename)
|
||||
raise
|
||||
|
||||
width, height = info['image_size']
|
||||
tw, th = info['texture_size']
|
||||
|
||||
images = [data]
|
||||
im = ImageData(width, height, str(info['format']), images[0],
|
||||
source=filename)
|
||||
'''
|
||||
if len(dds.images) > 1:
|
||||
images = dds.images
|
||||
images_size = dds.images_size
|
||||
for index in range(1, len(dds.images)):
|
||||
w, h = images_size[index]
|
||||
data = images[index]
|
||||
im.add_mipmap(index, w, h, data)
|
||||
'''
|
||||
return [im]
|
||||
|
||||
|
||||
# register
|
||||
ImageLoader.register(ImageLoaderTex)
|
||||
@@ -0,0 +1,135 @@
|
||||
'''
|
||||
Spelling
|
||||
========
|
||||
|
||||
Provides abstracted access to a range of spellchecking backends as well as
|
||||
word suggestions. The API is inspired by enchant but other backends can be
|
||||
added that implement the same API.
|
||||
|
||||
Spelling currently requires `python-enchant` for all platforms except
|
||||
OSX, where a native implementation exists.
|
||||
|
||||
::
|
||||
|
||||
>>> from kivy.core.spelling import Spelling
|
||||
>>> s = Spelling()
|
||||
>>> s.list_languages()
|
||||
['en', 'en_CA', 'en_GB', 'en_US']
|
||||
>>> s.select_language('en_US')
|
||||
>>> s.suggest('helo')
|
||||
[u'hole', u'help', u'helot', u'hello', u'halo', u'hero', u'hell', u'held',
|
||||
u'helm', u'he-lo']
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('Spelling', 'SpellingBase', 'NoSuchLangError',
|
||||
'NoLanguageSelectedError')
|
||||
|
||||
import sys
|
||||
from kivy.core import core_select_lib
|
||||
|
||||
|
||||
class NoSuchLangError(Exception):
|
||||
'''
|
||||
Exception to be raised when a specific language could not be found.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class NoLanguageSelectedError(Exception):
|
||||
'''
|
||||
Exception to be raised when a language-using method is called but no
|
||||
language was selected prior to the call.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class SpellingBase(object):
|
||||
'''
|
||||
Base class for all spelling providers.
|
||||
Supports some abstract methods for checking words and getting suggestions.
|
||||
'''
|
||||
|
||||
def __init__(self, language=None):
|
||||
'''
|
||||
If a `language` identifier (such as 'en_US') is provided and a matching
|
||||
language exists, it is selected. If an identifier is provided and no
|
||||
matching language exists, a NoSuchLangError exception is raised by
|
||||
self.select_language().
|
||||
If no `language` identifier is provided, we just fall back to the first
|
||||
one that is available.
|
||||
|
||||
:Parameters:
|
||||
`language`: str, defaults to None
|
||||
If provided, indicates the language to be used. This needs
|
||||
to be a language identifier understood by select_language(),
|
||||
i.e. one of the options returned by list_languages().
|
||||
If nothing is provided, the first available language is used.
|
||||
If no language is available, NoLanguageSelectedError is raised.
|
||||
'''
|
||||
langs = self.list_languages()
|
||||
try:
|
||||
# If no language was specified, we just use the first one
|
||||
# that is available.
|
||||
fallback_lang = langs[0]
|
||||
except IndexError:
|
||||
raise NoLanguageSelectedError("No languages available!")
|
||||
self.select_language(language or fallback_lang)
|
||||
|
||||
def select_language(self, language):
|
||||
'''
|
||||
From the set of registered languages, select the first language
|
||||
for `language`.
|
||||
|
||||
:Parameters:
|
||||
`language`: str
|
||||
Language identifier. Needs to be one of the options returned by
|
||||
list_languages(). Sets the language used for spell checking and
|
||||
word suggestions.
|
||||
'''
|
||||
raise NotImplementedError('select_language() method not implemented '
|
||||
'by abstract spelling base class!')
|
||||
|
||||
def list_languages(self):
|
||||
'''
|
||||
Return a list of all supported languages.
|
||||
E.g. ['en', 'en_GB', 'en_US', 'de', ...]
|
||||
'''
|
||||
raise NotImplementedError('list_languages() is not implemented '
|
||||
'by abstract spelling base class!')
|
||||
|
||||
def check(self, word):
|
||||
'''
|
||||
If `word` is a valid word in `self._language` (the currently active
|
||||
language), returns True. If the word shouldn't be checked, returns
|
||||
None (e.g. for ''). If it is not a valid word in `self._language`,
|
||||
return False.
|
||||
|
||||
:Parameters:
|
||||
`word`: str
|
||||
The word to check.
|
||||
'''
|
||||
raise NotImplementedError('check() not implemented by abstract ' +
|
||||
'spelling base class!')
|
||||
|
||||
def suggest(self, fragment):
|
||||
'''
|
||||
For a given `fragment` (i.e. part of a word or a word by itself),
|
||||
provide corrections (`fragment` may be misspelled) or completions
|
||||
as a list of strings.
|
||||
|
||||
:Parameters:
|
||||
`fragment`: str
|
||||
The word fragment to get suggestions/corrections for.
|
||||
E.g. 'foo' might become 'of', 'food' or 'foot'.
|
||||
|
||||
'''
|
||||
raise NotImplementedError('suggest() not implemented by abstract ' +
|
||||
'spelling base class!')
|
||||
|
||||
|
||||
_libs = (('enchant', 'spelling_enchant', 'SpellingEnchant'), )
|
||||
if sys.platform == 'darwin':
|
||||
_libs += (('osxappkit', 'spelling_osxappkit', 'SpellingOSXAppKit'), )
|
||||
|
||||
Spelling = core_select_lib('spelling', _libs)
|
||||
@@ -0,0 +1,50 @@
|
||||
'''
|
||||
Enchant Spelling
|
||||
================
|
||||
|
||||
Implementation spelling backend based on enchant.
|
||||
|
||||
.. warning:: pyenchant doesn't have dedicated build anymore for Windows/x64.
|
||||
See https://github.com/kivy/kivy/issues/5816 for more information
|
||||
'''
|
||||
|
||||
|
||||
import enchant
|
||||
|
||||
from kivy.core.spelling import SpellingBase, NoSuchLangError
|
||||
from kivy.compat import PY2
|
||||
|
||||
|
||||
class SpellingEnchant(SpellingBase):
|
||||
'''
|
||||
Spelling backend based on the enchant library.
|
||||
'''
|
||||
|
||||
def __init__(self, language=None):
|
||||
self._language = None
|
||||
super(SpellingEnchant, self).__init__(language)
|
||||
|
||||
def select_language(self, language):
|
||||
try:
|
||||
self._language = enchant.Dict(language)
|
||||
except enchant.DictNotFoundError:
|
||||
err = 'Enchant Backend: No language for "%s"' % (language, )
|
||||
raise NoSuchLangError(err)
|
||||
|
||||
def list_languages(self):
|
||||
# Note: We do NOT return enchant.list_dicts because that also returns
|
||||
# the enchant dict objects and not only the language identifiers.
|
||||
return enchant.list_languages()
|
||||
|
||||
def check(self, word):
|
||||
if not word:
|
||||
return None
|
||||
return self._language.check(word)
|
||||
|
||||
def suggest(self, fragment):
|
||||
suggestions = self._language.suggest(fragment)
|
||||
# Don't show suggestions that are invalid
|
||||
suggestions = [s for s in suggestions if self.check(s)]
|
||||
if PY2:
|
||||
suggestions = [s.decode('utf-8') for s in suggestions]
|
||||
return suggestions
|
||||
@@ -0,0 +1,64 @@
|
||||
'''
|
||||
AppKit Spelling: Implements spelling backend based on OSX's spellchecking
|
||||
features provided by the ApplicationKit.
|
||||
|
||||
NOTE:
|
||||
Requires pyobjc and setuptools to be installed!
|
||||
`sudo easy_install pyobjc setuptools`
|
||||
|
||||
Developers should read:
|
||||
http://developer.apple.com/mac/library/documentation/
|
||||
Cocoa/Conceptual/SpellCheck/SpellCheck.html
|
||||
http://developer.apple.com/cocoa/pyobjc.html
|
||||
'''
|
||||
|
||||
|
||||
from AppKit import NSSpellChecker, NSMakeRange
|
||||
|
||||
from kivy.core.spelling import SpellingBase, NoSuchLangError
|
||||
|
||||
|
||||
class SpellingOSXAppKit(SpellingBase):
|
||||
'''
|
||||
Spelling backend based on OSX's spelling features provided by AppKit.
|
||||
'''
|
||||
|
||||
def __init__(self, language=None):
|
||||
self._language = NSSpellChecker.alloc().init()
|
||||
super(SpellingOSXAppKit, self).__init__(language)
|
||||
|
||||
def select_language(self, language):
|
||||
success = self._language.setLanguage_(language)
|
||||
if not success:
|
||||
err = 'AppKit Backend: No language "%s" ' % (language, )
|
||||
raise NoSuchLangError(err)
|
||||
|
||||
def list_languages(self):
|
||||
return list(self._language.availableLanguages())
|
||||
|
||||
def check(self, word):
|
||||
# TODO Implement this!
|
||||
# NSSpellChecker provides several functions that look like what we
|
||||
# need, but they're a) slooow and b) return a strange result.
|
||||
# Might be a snow leopard bug. Have to test further.
|
||||
# See: http://paste.pocoo.org/show/217968/
|
||||
if not word:
|
||||
return None
|
||||
err = 'check() not currently supported by the OSX AppKit backend'
|
||||
raise NotImplementedError(err)
|
||||
|
||||
def suggest(self, fragment):
|
||||
l = self._language
|
||||
# XXX Both ways below work on OSX 10.6. It has not been tested on any
|
||||
# other version, but it should work.
|
||||
try:
|
||||
# This is deprecated as of OSX 10.6, hence the try-except
|
||||
return list(l.guessesForWord_(fragment))
|
||||
except AttributeError:
|
||||
# From 10.6 onwards you're supposed to do it like this:
|
||||
checkrange = NSMakeRange(0, len(fragment))
|
||||
g = l.\
|
||||
guessesForWordRange_inString_language_inSpellDocumentWithTag_(
|
||||
checkrange, fragment, l.language(), 0)
|
||||
# Right, this was much easier, Apple! :-)
|
||||
return list(g)
|
||||
@@ -0,0 +1,893 @@
|
||||
'''
|
||||
Text Markup
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.1.0
|
||||
|
||||
.. versionchanged:: 1.10.1
|
||||
|
||||
Added `font_context`, `font_features` and `text_language` (Pango only)
|
||||
|
||||
We provide a simple text-markup for inline text styling. The syntax look the
|
||||
same as the `BBCode <http://en.wikipedia.org/wiki/BBCode>`_.
|
||||
|
||||
A tag is defined as ``[tag]``, and should have a corresponding
|
||||
``[/tag]`` closing tag. For example::
|
||||
|
||||
[b]Hello [color=ff0000]world[/color][/b]
|
||||
|
||||
The following tags are available:
|
||||
|
||||
``[b][/b]``
|
||||
Activate bold text
|
||||
``[i][/i]``
|
||||
Activate italic text
|
||||
``[u][/u]``
|
||||
Underlined text
|
||||
``[s][/s]``
|
||||
Strikethrough text
|
||||
``[font=<str>][/font]``
|
||||
Change the font (note: this refers to a TTF file or registered alias)
|
||||
``[font_context=<str>][/font_context]``
|
||||
Change context for the font, use string value "none" for isolated context.
|
||||
``[font_family=<str>][/font_family]``
|
||||
Font family to request for drawing. This is only valid when using a
|
||||
font context, and takes precedence over `[font]`. See
|
||||
:class:`kivy.uix.label.Label` for details.
|
||||
``[font_features=<str>][/font_features]``
|
||||
OpenType font features, in CSS format, this is passed straight
|
||||
through to Pango. The effects of requesting a feature depends on loaded
|
||||
fonts, library versions, etc. Pango only, requires v1.38 or later.
|
||||
``[size=<size>][/size]``
|
||||
Change the font size. <size> should be an integer, optionally with a
|
||||
unit (i.e. ``16sp``)
|
||||
``[color=#<color>][/color]``
|
||||
Change the text color
|
||||
``[ref=<str>][/ref]``
|
||||
Add an interactive zone. The reference + all the word box inside the
|
||||
reference will be available in :attr:`MarkupLabel.refs`
|
||||
``[anchor=<str>]``
|
||||
Put an anchor in the text. You can get the position of your anchor within
|
||||
the text with :attr:`MarkupLabel.anchors`
|
||||
``[sub][/sub]``
|
||||
Display the text at a subscript position relative to the text before it.
|
||||
``[sup][/sup]``
|
||||
Display the text at a superscript position relative to the text before it.
|
||||
``[text_language=<str>][/text_language]``
|
||||
Language of the text, this is an RFC-3066 format language tag (as string),
|
||||
for example "en_US", "zh_CN", "fr" or "ja". This can impact font selection,
|
||||
metrics and rendering. For example, the same bytes of text can look
|
||||
different for `ur` and `ar` languages, though both use Arabic script.
|
||||
Use the string `'none'` to revert to locale detection. Pango only.
|
||||
|
||||
If you need to escape the markup from the current text, use
|
||||
:func:`kivy.utils.escape_markup`.
|
||||
'''
|
||||
|
||||
__all__ = ('MarkupLabel', )
|
||||
|
||||
import re
|
||||
from kivy.properties import dpi2px
|
||||
from kivy.parser import parse_color
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.text import Label, LabelBase
|
||||
from kivy.core.text.text_layout import layout_text, LayoutWord, LayoutLine
|
||||
from copy import copy
|
||||
from functools import partial
|
||||
|
||||
# We need to do this trick when documentation is generated
|
||||
MarkupLabelBase = Label
|
||||
if Label is None:
|
||||
MarkupLabelBase = LabelBase
|
||||
|
||||
|
||||
class MarkupLabel(MarkupLabelBase):
|
||||
'''Markup text label.
|
||||
|
||||
See module documentation for more information.
|
||||
'''
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
self._style_stack = {}
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
super(MarkupLabel, self).__init__(*largs, **kwargs)
|
||||
self._internal_size = 0, 0
|
||||
self._cached_lines = []
|
||||
|
||||
@property
|
||||
def refs(self):
|
||||
'''Get the bounding box of all the ``[ref=...]``::
|
||||
|
||||
{ 'refA': ((x1, y1, x2, y2), (x1, y1, x2, y2)), ... }
|
||||
'''
|
||||
return self._refs
|
||||
|
||||
@property
|
||||
def anchors(self):
|
||||
'''Get the position of all the ``[anchor=...]``::
|
||||
|
||||
{ 'anchorA': (x, y), 'anchorB': (x, y), ... }
|
||||
'''
|
||||
return self._anchors
|
||||
|
||||
@property
|
||||
def markup(self):
|
||||
'''Return the text with all the markup split::
|
||||
|
||||
>>> MarkupLabel('[b]Hello world[/b]').markup
|
||||
>>> ('[b]', 'Hello world', '[/b]')
|
||||
|
||||
'''
|
||||
s = re.split(r'(\[.*?\])', self.label)
|
||||
s = [x for x in s if x != '']
|
||||
return s
|
||||
|
||||
def _push_style(self, k):
|
||||
if k not in self._style_stack:
|
||||
self._style_stack[k] = []
|
||||
self._style_stack[k].append(self.options[k])
|
||||
|
||||
def _pop_style(self, k):
|
||||
if k not in self._style_stack or len(self._style_stack[k]) == 0:
|
||||
Logger.warning('Label: pop style stack without push')
|
||||
return
|
||||
v = self._style_stack[k].pop()
|
||||
self.options[k] = v
|
||||
|
||||
def render(self, real=False):
|
||||
options = copy(self.options)
|
||||
if not real:
|
||||
ret = self._pre_render()
|
||||
else:
|
||||
ret = self._render_real()
|
||||
self.options = options
|
||||
return ret
|
||||
|
||||
def _pre_render(self):
|
||||
# split markup, words, and lines
|
||||
# result: list of word with position and width/height
|
||||
# during the first pass, we don't care about h/valign
|
||||
self._cached_lines = lines = []
|
||||
self._refs = {}
|
||||
self._anchors = {}
|
||||
clipped = False
|
||||
w = h = 0
|
||||
uw, uh = self.text_size
|
||||
spush = self._push_style
|
||||
spop = self._pop_style
|
||||
options = self.options
|
||||
options['_ref'] = None
|
||||
options['_anchor'] = None
|
||||
options['script'] = 'normal'
|
||||
shorten = options['shorten']
|
||||
# if shorten, then don't split lines to fit uw, because it will be
|
||||
# flattened later when shortening and broken up lines if broken
|
||||
# mid-word will have space mid-word when lines are joined
|
||||
uw_temp = None if shorten else uw
|
||||
xpad = options['padding'][0] + options['padding'][2]
|
||||
uhh = (None if uh is not None and options['valign'] != 'top' or
|
||||
options['shorten'] else uh)
|
||||
options['strip'] = options['strip'] or options['halign'] == 'justify'
|
||||
find_base_dir = Label.find_base_direction
|
||||
base_dir = options['base_direction']
|
||||
self._resolved_base_dir = None
|
||||
for item in self.markup:
|
||||
if item == '[b]':
|
||||
spush('bold')
|
||||
options['bold'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/b]':
|
||||
spop('bold')
|
||||
self.resolve_font_name()
|
||||
elif item == '[i]':
|
||||
spush('italic')
|
||||
options['italic'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/i]':
|
||||
spop('italic')
|
||||
self.resolve_font_name()
|
||||
elif item == '[u]':
|
||||
spush('underline')
|
||||
options['underline'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/u]':
|
||||
spop('underline')
|
||||
self.resolve_font_name()
|
||||
elif item == '[s]':
|
||||
spush('strikethrough')
|
||||
options['strikethrough'] = True
|
||||
self.resolve_font_name()
|
||||
elif item == '[/s]':
|
||||
spop('strikethrough')
|
||||
self.resolve_font_name()
|
||||
elif item[:6] == '[size=':
|
||||
item = item[6:-1]
|
||||
try:
|
||||
if item[-2:] in ('px', 'pt', 'in', 'cm', 'mm', 'dp', 'sp'):
|
||||
size = dpi2px(item[:-2], item[-2:])
|
||||
else:
|
||||
size = int(item)
|
||||
except ValueError:
|
||||
raise
|
||||
size = options['font_size']
|
||||
spush('font_size')
|
||||
options['font_size'] = size
|
||||
elif item == '[/size]':
|
||||
spop('font_size')
|
||||
elif item[:7] == '[color=':
|
||||
color = parse_color(item[7:-1])
|
||||
spush('color')
|
||||
options['color'] = color
|
||||
elif item == '[/color]':
|
||||
spop('color')
|
||||
elif item[:6] == '[font=':
|
||||
fontname = item[6:-1]
|
||||
spush('font_name')
|
||||
options['font_name'] = fontname
|
||||
self.resolve_font_name()
|
||||
elif item == '[/font]':
|
||||
spop('font_name')
|
||||
self.resolve_font_name()
|
||||
elif item[:13] == '[font_family=':
|
||||
spush('font_family')
|
||||
options['font_family'] = item[13:-1]
|
||||
elif item == '[/font_family]':
|
||||
spop('font_family')
|
||||
elif item[:14] == '[font_context=':
|
||||
fctx = item[14:-1]
|
||||
if not fctx or fctx.lower() == 'none':
|
||||
fctx = None
|
||||
spush('font_context')
|
||||
options['font_context'] = fctx
|
||||
elif item == '[/font_context]':
|
||||
spop('font_context')
|
||||
elif item[:15] == '[font_features=':
|
||||
spush('font_features')
|
||||
options['font_features'] = item[15:-1]
|
||||
elif item == '[/font_features]':
|
||||
spop('font_features')
|
||||
elif item[:15] == '[text_language=':
|
||||
lang = item[15:-1]
|
||||
if not lang or lang.lower() == 'none':
|
||||
lang = None
|
||||
spush('text_language')
|
||||
options['text_language'] = lang
|
||||
elif item == '[/text_language]':
|
||||
spop('text_language')
|
||||
elif item[:5] == '[sub]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'subscript'
|
||||
elif item == '[/sub]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[sup]':
|
||||
spush('font_size')
|
||||
spush('script')
|
||||
options['font_size'] = options['font_size'] * .5
|
||||
options['script'] = 'superscript'
|
||||
elif item == '[/sup]':
|
||||
spop('font_size')
|
||||
spop('script')
|
||||
elif item[:5] == '[ref=':
|
||||
ref = item[5:-1]
|
||||
spush('_ref')
|
||||
options['_ref'] = ref
|
||||
elif item == '[/ref]':
|
||||
spop('_ref')
|
||||
elif not clipped and item[:8] == '[anchor=':
|
||||
options['_anchor'] = item[8:-1]
|
||||
elif not clipped:
|
||||
item = item.replace('&bl;', '[').replace(
|
||||
'&br;', ']').replace('&', '&')
|
||||
if not base_dir:
|
||||
base_dir = self._resolved_base_dir = find_base_dir(item)
|
||||
opts = copy(options)
|
||||
extents = self.get_cached_extents()
|
||||
opts['space_width'] = extents(' ')[0]
|
||||
w, h, clipped = layout_text(
|
||||
item, lines, (w, h), (uw_temp, uhh),
|
||||
opts, extents,
|
||||
append_down=True,
|
||||
complete=False
|
||||
)
|
||||
|
||||
if len(lines): # remove any trailing spaces from the last line
|
||||
old_opts = self.options
|
||||
self.options = copy(opts)
|
||||
w, h, clipped = layout_text(
|
||||
'', lines, (w, h), (uw_temp, uhh),
|
||||
self.options, self.get_cached_extents(),
|
||||
append_down=True,
|
||||
complete=True
|
||||
)
|
||||
self.options = old_opts
|
||||
|
||||
self.is_shortened = False
|
||||
if shorten:
|
||||
options['_ref'] = None # no refs for you!
|
||||
options['_anchor'] = None
|
||||
w, h, lines = self.shorten_post(lines, w, h)
|
||||
self._cached_lines = lines
|
||||
# when valign is not top, for markup we layout everything (text_size[1]
|
||||
# is temporarily set to None) and after layout cut to size if too tall
|
||||
elif uh != uhh and h > uh and len(lines) > 1:
|
||||
if options['valign'] == 'bottom':
|
||||
i = 0
|
||||
while i < len(lines) - 1 and h > uh:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
else: # middle
|
||||
i = 0
|
||||
top = int(h / 2. + uh / 2.) # remove extra top portion
|
||||
while i < len(lines) - 1 and h > top:
|
||||
h -= lines[i].h
|
||||
i += 1
|
||||
del lines[:i]
|
||||
i = len(lines) - 1 # remove remaining bottom portion
|
||||
while i and h > uh:
|
||||
h -= lines[i].h
|
||||
i -= 1
|
||||
del lines[i + 1:]
|
||||
|
||||
# now justify the text
|
||||
if options['halign'] == 'justify' and uw is not None:
|
||||
# XXX: update refs to justified pos
|
||||
# when justify, each line should've been stripped already
|
||||
split = partial(re.split, re.compile('( +)'))
|
||||
uww = uw - xpad
|
||||
chr = type(self.text)
|
||||
space = chr(' ')
|
||||
empty = chr('')
|
||||
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
words = line.words
|
||||
# if there's nothing to justify, we're done
|
||||
if (not line.w or int(uww - line.w) <= 0 or not len(words) or
|
||||
line.is_last_line):
|
||||
continue
|
||||
|
||||
done = False
|
||||
parts = [None, ] * len(words) # contains words split by space
|
||||
idxs = [None, ] * len(words) # indices of the space in parts
|
||||
# break each word into spaces and add spaces until it's full
|
||||
# do first round of split in case we don't need to split all
|
||||
for w in range(len(words)):
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w] = split(word.text)
|
||||
idxs[w] = [v for v in range(len(p)) if
|
||||
p[v].startswith(' ')]
|
||||
# now we have the indices of the spaces in split list
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# there's not a single space in the line?
|
||||
if not any(idxs):
|
||||
continue
|
||||
|
||||
# now keep adding spaces to already split words until done
|
||||
while not done:
|
||||
for w in range(len(words)):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
word = words[w]
|
||||
sw = word.options['space_width']
|
||||
p = parts[w]
|
||||
for k in idxs[w]:
|
||||
# try to add single space at each space
|
||||
if line.w + sw > uww:
|
||||
done = True
|
||||
break
|
||||
line.w += sw
|
||||
word.lw += sw
|
||||
p[k] += space
|
||||
if done:
|
||||
break
|
||||
|
||||
# if not completely full, push last words to right edge
|
||||
diff = int(uww - line.w)
|
||||
if diff > 0:
|
||||
# find the last word that had a space
|
||||
for w in range(len(words) - 1, -1, -1):
|
||||
if not idxs[w]:
|
||||
continue
|
||||
break
|
||||
old_opts = self.options
|
||||
self.options = word.options
|
||||
word = words[w]
|
||||
# split that word into left/right and push right till uww
|
||||
l_text = empty.join(parts[w][:idxs[w][-1]])
|
||||
r_text = empty.join(parts[w][idxs[w][-1]:])
|
||||
left = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(l_text)[0],
|
||||
word.lh,
|
||||
l_text
|
||||
)
|
||||
right = LayoutWord(
|
||||
word.options,
|
||||
self.get_extents(r_text)[0],
|
||||
word.lh,
|
||||
r_text
|
||||
)
|
||||
left.lw = max(left.lw, word.lw + diff - right.lw)
|
||||
self.options = old_opts
|
||||
|
||||
# now put words back together with right/left inserted
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
words[w] = right
|
||||
words.insert(w, left)
|
||||
else:
|
||||
for k in range(len(words)):
|
||||
if idxs[k]:
|
||||
words[k].text = empty.join(parts[k])
|
||||
line.w = uww
|
||||
w = max(w, uww)
|
||||
|
||||
self._internal_size = w, h
|
||||
if uw:
|
||||
w = uw
|
||||
if uh:
|
||||
h = uh
|
||||
if h > 1 and w < 2:
|
||||
w = 2
|
||||
if w < 1:
|
||||
w = 1
|
||||
if h < 1:
|
||||
h = 1
|
||||
return int(w), int(h)
|
||||
|
||||
def render_lines(self, lines, options, render_text, y, size):
|
||||
padding_left = options['padding'][0]
|
||||
padding_right = options['padding'][2]
|
||||
w = size[0]
|
||||
halign = options['halign']
|
||||
refs = self._refs
|
||||
anchors = self._anchors
|
||||
base_dir = options['base_direction'] or self._resolved_base_dir
|
||||
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
||||
|
||||
for layout_line in lines: # for plain label each line has only one str
|
||||
lw, lh = layout_line.w, layout_line.h
|
||||
x = padding_left
|
||||
if halign == 'center':
|
||||
x = min(
|
||||
int(w - lw),
|
||||
max(
|
||||
int(padding_left),
|
||||
int((w - lw + padding_left - padding_right) / 2.0)
|
||||
)
|
||||
)
|
||||
elif halign == 'right' or auto_halign_r:
|
||||
x = max(0, int(w - lw - padding_right))
|
||||
layout_line.x = x
|
||||
layout_line.y = y
|
||||
psp = pph = 0
|
||||
for word in layout_line.words:
|
||||
options = self.options = word.options
|
||||
# the word height is not scaled by line_height, only lh was
|
||||
wh = options['line_height'] * word.lh
|
||||
# calculate sub/super script pos
|
||||
if options['script'] == 'superscript':
|
||||
script_pos = max(0, psp if psp and (wh < pph)
|
||||
else self.get_descent())
|
||||
psp = script_pos
|
||||
pph = wh
|
||||
elif options['script'] == 'subscript':
|
||||
script_pos = min(lh - wh, ((psp + pph) - wh)
|
||||
if pph and (wh < pph) else (lh - wh))
|
||||
pph = wh
|
||||
psp = script_pos
|
||||
else:
|
||||
script_pos = (lh - wh) / 1.25
|
||||
psp = pph = 0
|
||||
if len(word.text):
|
||||
render_text(word.text, x, y + script_pos)
|
||||
|
||||
# should we record refs ?
|
||||
ref = options['_ref']
|
||||
if ref is not None:
|
||||
if ref not in refs:
|
||||
refs[ref] = []
|
||||
refs[ref].append((x, y, x + word.lw, y + wh))
|
||||
|
||||
# Should we record anchors?
|
||||
anchor = options['_anchor']
|
||||
if anchor is not None:
|
||||
if anchor not in anchors:
|
||||
anchors[anchor] = (x, y)
|
||||
x += word.lw
|
||||
y += lh
|
||||
return y
|
||||
|
||||
def shorten_post(self, lines, w, h, margin=2):
|
||||
''' Shortens the text to a single line according to the label options.
|
||||
|
||||
This function operates on a text that has already been laid out because
|
||||
for markup, parts of text can have different size and options.
|
||||
|
||||
If :attr:`text_size` [0] is None, the lines are returned unchanged.
|
||||
Otherwise, the lines are converted to a single line fitting within the
|
||||
constrained width, :attr:`text_size` [0].
|
||||
|
||||
:params:
|
||||
|
||||
`lines`: list of `LayoutLine` instances describing the text.
|
||||
`w`: int, the width of the text in lines, including padding.
|
||||
`h`: int, the height of the text in lines, including padding.
|
||||
`margin` int, the additional space left on the sides. This is in
|
||||
addition to :attr:`padding_x`.
|
||||
|
||||
:returns:
|
||||
3-tuple of (xw, h, lines), where w, and h is similar to the input
|
||||
and contains the resulting width / height of the text, including
|
||||
padding. lines, is a list containing a single `LayoutLine`, which
|
||||
contains the words for the line.
|
||||
'''
|
||||
|
||||
def n(line, c):
|
||||
''' A function similar to text.find, except it's an iterator that
|
||||
returns successive occurrences of string c in list line. line is
|
||||
not a string, but a list of LayoutWord instances that we walk
|
||||
from left to right returning the indices of c in the words as we
|
||||
encounter them. Note that the options can be different among the
|
||||
words.
|
||||
|
||||
:returns:
|
||||
3-tuple: the index of the word in line, the index of the
|
||||
occurrence in word, and the extents (width) of the combined
|
||||
words until this occurrence, not including the occurrence char.
|
||||
If no more are found it returns (-1, -1, total_w) where total_w
|
||||
is the full width of all the words.
|
||||
'''
|
||||
total_w = 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.find, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield w, i, total_w + self.get_extents(word.text[:i])[0]
|
||||
i = f(i + 1)
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def p(line, c):
|
||||
''' Similar to the `n` function, except it returns occurrences of c
|
||||
from right to left in the list, line, similar to rfind.
|
||||
'''
|
||||
total_w = 0
|
||||
offset = 0 if len(c) else 1
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
if not word.lw:
|
||||
continue
|
||||
f = partial(word.text.rfind, c)
|
||||
i = f()
|
||||
while i != -1:
|
||||
self.options = word.options
|
||||
yield (w, i, total_w +
|
||||
self.get_extents(word.text[i + 1:])[0])
|
||||
if i:
|
||||
i = f(0, i - offset)
|
||||
else:
|
||||
if not c:
|
||||
self.options = word.options
|
||||
yield (w, -1, total_w +
|
||||
self.get_extents(word.text)[0])
|
||||
break
|
||||
self.options = word.options
|
||||
total_w += self.get_extents(word.text)[0]
|
||||
yield -1, -1, total_w # this should never be reached, really
|
||||
|
||||
def n_restricted(line, uw, c):
|
||||
''' Similar to the function `n`, except it only returns the first
|
||||
occurrence and it's not an iterator. Furthermore, if the first
|
||||
occurrence doesn't fit within width uw, it returns the index of
|
||||
whatever amount of text will still fit in uw.
|
||||
|
||||
:returns:
|
||||
similar to the function `n`, except it's a 4-tuple, with the
|
||||
last element a boolean, indicating if we had to clip the text
|
||||
to fit in uw (True) or if the whole text until the first
|
||||
occurrence fitted in uw (False).
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line)):
|
||||
word = line[w]
|
||||
f = partial(word.text.find, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[:i])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
i = len(word.text)
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
e = 0
|
||||
while e != i and total_w + extents(word.text[:e])[0] <= uw:
|
||||
e += 1
|
||||
e = max(0, e - 1)
|
||||
return w, e, total_w + extents(word.text[:e])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
def p_restricted(line, uw, c):
|
||||
''' Similar to `n_restricted`, except it returns the first
|
||||
occurrence starting from the right, like `p`.
|
||||
'''
|
||||
total_w = 0
|
||||
if not len(line):
|
||||
return 0, 0, 0
|
||||
for w in range(len(line) - 1, -1, -1):
|
||||
word = line[w]
|
||||
f = partial(word.text.rfind, c)
|
||||
self.options = word.options
|
||||
extents = self.get_cached_extents()
|
||||
i = f()
|
||||
if i != -1:
|
||||
ww = extents(word.text[i + 1:])[0]
|
||||
|
||||
if i != -1 and total_w + ww <= uw: # found and it fits
|
||||
return w, i, total_w + ww, False
|
||||
elif i == -1:
|
||||
ww = extents(word.text)[0]
|
||||
if total_w + ww <= uw: # wasn't found and all fits
|
||||
total_w += ww
|
||||
continue
|
||||
|
||||
# now just find whatever amount of the word does fit
|
||||
s = len(word.text) - 1
|
||||
while s >= 0 and total_w + extents(word.text[s:])[0] <= uw:
|
||||
s -= 1
|
||||
return w, s, total_w + extents(word.text[s + 1:])[0], True
|
||||
|
||||
return -1, -1, total_w, False
|
||||
|
||||
textwidth = self.get_cached_extents()
|
||||
uw = self.text_size[0]
|
||||
if uw is None:
|
||||
return w, h, lines
|
||||
old_opts = copy(self.options)
|
||||
uw = max(
|
||||
0,
|
||||
int(uw - old_opts["padding"][0] - old_opts["padding"][2] - margin),
|
||||
)
|
||||
chr = type(self.text)
|
||||
ssize = textwidth(' ')
|
||||
c = old_opts['split_str']
|
||||
line_height = old_opts['line_height']
|
||||
xpad, ypad = (
|
||||
old_opts["padding"][0] + old_opts["padding"][2],
|
||||
old_opts["padding"][1] + old_opts["padding"][3],
|
||||
)
|
||||
dir = old_opts['shorten_from'][0]
|
||||
|
||||
# flatten lines into single line
|
||||
line = []
|
||||
last_w = 0
|
||||
for l in range(len(lines)):
|
||||
# concatenate (non-empty) inside lines with a space
|
||||
this_line = lines[l]
|
||||
if last_w and this_line.w and not this_line.line_wrap:
|
||||
line.append(LayoutWord(old_opts, ssize[0], ssize[1], chr(' ')))
|
||||
last_w = this_line.w or last_w
|
||||
for word in this_line.words:
|
||||
if word.lw:
|
||||
line.append(word)
|
||||
|
||||
# if that fits, just return the flattened line
|
||||
lw = sum([word.lw for word in line])
|
||||
if lw <= uw:
|
||||
lh = max([word.lh for word in line] + [0]) * line_height
|
||||
self.is_shortened = False
|
||||
return (
|
||||
lw + xpad,
|
||||
lh + ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line)]
|
||||
)
|
||||
|
||||
elps_opts = copy(old_opts)
|
||||
if 'ellipsis_options' in old_opts:
|
||||
elps_opts.update(old_opts['ellipsis_options'])
|
||||
|
||||
# Set new opts for ellipsis
|
||||
self.options = elps_opts
|
||||
# find the size of ellipsis that'll fit
|
||||
elps_s = textwidth('...')
|
||||
if elps_s[0] > uw: # even ellipsis didn't fit...
|
||||
self.is_shortened = True
|
||||
s = textwidth('..')
|
||||
if s[0] <= uw:
|
||||
return (
|
||||
s[0] + xpad,
|
||||
s[1] * line_height + ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '..')])]
|
||||
)
|
||||
|
||||
else:
|
||||
s = textwidth('.')
|
||||
return (
|
||||
s[0] + xpad,
|
||||
s[1] * line_height + ypad,
|
||||
[LayoutLine(
|
||||
0, 0, s[0], s[1], 1, 0,
|
||||
[LayoutWord(old_opts, s[0], s[1], '.')])]
|
||||
)
|
||||
|
||||
elps = LayoutWord(elps_opts, elps_s[0], elps_s[1], '...')
|
||||
uw -= elps_s[0]
|
||||
# Restore old opts
|
||||
self.options = old_opts
|
||||
|
||||
# now find the first left and right words that fit
|
||||
w1, e1, l1, clipped1 = n_restricted(line, uw, c)
|
||||
w2, s2, l2, clipped2 = p_restricted(line, uw, c)
|
||||
|
||||
if dir != 'l': # center or right
|
||||
line1 = None
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take first
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1],
|
||||
last_text))
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if line1:
|
||||
line1.append(elps)
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + xpad,
|
||||
lh + ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
if dir == 'r':
|
||||
f = n(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1) # first word should match
|
||||
ww1, ee1, l1 = next(f)
|
||||
while l2 + l1 <= uw:
|
||||
w1, e1 = ww1, ee1
|
||||
ww1, ee1, l1 = next(f)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # center
|
||||
f = n(line, c) # iterator
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f)[:-1] == (w1, e1)
|
||||
assert next(f_inv)[:-1] == (w2, s2)
|
||||
while True:
|
||||
if l1 <= l2:
|
||||
ww1, ee1, l1 = next(f) # hypothesize that next fit
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w1, e1 = ww1, ee1
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else:
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if l2 + l1 > uw:
|
||||
break
|
||||
w2, s2 = ww2, ss2
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
else: # left
|
||||
line1 = [elps]
|
||||
if clipped1 or clipped2 or l1 + l2 > uw:
|
||||
# if either was clipped or both don't fit, just take last
|
||||
if len(c):
|
||||
self.options = old_opts
|
||||
old_opts['split_str'] = ''
|
||||
res = self.shorten_post(lines, w, h, margin)
|
||||
self.options['split_str'] = c
|
||||
self.is_shortened = True
|
||||
return res
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
elif (w1, e1) == (-1, -1): # this shouldn't occur
|
||||
line1 = line
|
||||
if len(line1) != 1:
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + xpad,
|
||||
lh + ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
|
||||
# now we know that both the first and last word fit, and that
|
||||
# there's at least one instances of the split_str in the line
|
||||
if (w1, e1) != (w2, s2): # more than one split_str
|
||||
f_inv = p(line, c) # iterator
|
||||
assert next(f_inv)[:-1] == (w2, s2) # last word should match
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
while l2 + l1 <= uw:
|
||||
w2, s2 = ww2, ss2
|
||||
ww2, ss2, l2 = next(f_inv)
|
||||
if (w1, e1) == (w2, s2):
|
||||
break
|
||||
|
||||
# now add back the left half
|
||||
line1 = line[:w1]
|
||||
last_word = line[w1]
|
||||
last_text = last_word.text[:e1]
|
||||
self.options = last_word.options
|
||||
s = self.get_extents(last_text)
|
||||
if len(last_text):
|
||||
line1.append(LayoutWord(last_word.options, s[0], s[1], last_text))
|
||||
line1.append(elps)
|
||||
|
||||
# now add back the right half
|
||||
first_word = line[w2]
|
||||
first_text = first_word.text[s2 + 1:]
|
||||
self.options = first_word.options
|
||||
s = self.get_extents(first_text)
|
||||
if len(first_text):
|
||||
line1.append(LayoutWord(first_word.options, s[0], s[1],
|
||||
first_text))
|
||||
line1.extend(line[w2 + 1:])
|
||||
|
||||
lw = sum([word.lw for word in line1])
|
||||
lh = max([word.lh for word in line1]) * line_height
|
||||
self.options = old_opts
|
||||
if uw < lw:
|
||||
self.is_shortened = True
|
||||
return (
|
||||
lw + xpad,
|
||||
lh + ypad,
|
||||
[LayoutLine(0, 0, lw, lh, 1, 0, line1)]
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
cdef class LayoutWord:
|
||||
cdef public object text
|
||||
cdef public int lw, lh
|
||||
cdef public dict options
|
||||
|
||||
|
||||
cdef class LayoutLine:
|
||||
cdef public int x, y, w, h
|
||||
cdef public int line_wrap # whether this line wraps from last line
|
||||
cdef public int is_last_line # in a paragraph
|
||||
cdef public list words
|
||||
@@ -0,0 +1,145 @@
|
||||
'''
|
||||
Pango text provider
|
||||
===================
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
|
||||
.. warning::
|
||||
The low-level Pango API is experimental, and subject to change without
|
||||
notice for as long as this warning is present.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
1. Install pangoft2 (`apt install libfreetype6-dev libpango1.0-dev
|
||||
libpangoft2-1.0-0`) or ensure it is available in pkg-config
|
||||
2. Recompile kivy. Check that pangoft2 is found `use_pangoft2 = 1`
|
||||
3. Test it! Enforce the text core renderer to pango using environment variable:
|
||||
`export KIVY_TEXT=pango`
|
||||
|
||||
This has been tested on OSX and Linux, Python 3.6.
|
||||
|
||||
|
||||
Font context types for FontConfig+FreeType2 backend
|
||||
---------------------------------------------------
|
||||
|
||||
* `system://` - `FcInitLoadConfigAndFonts()`
|
||||
* `systemconfig://` - `FcInitLoadConfig()`
|
||||
* `directory://<PATH>` - `FcInitLoadConfig()` + `FcAppFontAddDir()`
|
||||
* `fontconfig://<PATH>` - `FcConfigCreate()` + `FcConfigParseAndLoad()`
|
||||
* Any other context name - `FcConfigCreate()`
|
||||
|
||||
|
||||
Low-level Pango access
|
||||
----------------------
|
||||
|
||||
Since Kivy currently does its own text layout, the Label and TextInput widgets
|
||||
do not take full advantage of Pango. For example, line breaks do not take
|
||||
language/script into account, and switching alignment per paragraph (for bi-
|
||||
directional text) is not supported. For advanced i18n requirements, we provide
|
||||
a simple wrapper around PangoLayout that you can use to render text.
|
||||
|
||||
* https://developer.gnome.org/pango/1.40/pango-Layout-Objects.html
|
||||
* https://developer.gnome.org/pango/1.40/PangoMarkupFormat.html
|
||||
* See the `kivy/core/text/_text_pango.pyx` file @ `cdef class KivyPangoLayout`
|
||||
for more information. Not all features of PangoLayout are implemented.
|
||||
|
||||
.. python::
|
||||
from kivy.core.window import Window # OpenGL must be initialized
|
||||
from kivy.core.text._text_pango import KivyPangoLayout
|
||||
layout = KivyPangoLayout('system://')
|
||||
layout.set_markup('<span font="20">Hello <b>World!</b></span>')
|
||||
tex = layout.render_as_Texture()
|
||||
|
||||
|
||||
Known limitations
|
||||
-----------------
|
||||
|
||||
* Pango versions older than v1.38 has not been tested. It may work on
|
||||
some systems with older pango and newer FontConfig/FreeType2 versions.
|
||||
* Kivy's text layout is used, not Pango. This means we do not use Pango's
|
||||
line-breaking feature (which is superior to Kivy's), and we can't use
|
||||
Pango's bidirectional cursor helpers in TextInput.
|
||||
* Font family collisions can happen. For example, if you use a `system://`
|
||||
context and add a custom `Arial.ttf`, using `arial` as the `font_family`
|
||||
may or may not draw with your custom font (depending on whether or not
|
||||
there is already a system-wide "arial" font installed)
|
||||
* Rendering is inefficient; the normal way to integrate Pango would be
|
||||
using a dedicated PangoLayout per widget. This is not currently practical
|
||||
due to missing abstractions in Kivy core (in the current implementation,
|
||||
we have a dedicated PangoLayout *per font context,* which is rendered
|
||||
once for each LayoutWord)
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPango', )
|
||||
|
||||
from types import MethodType
|
||||
from os.path import isfile
|
||||
from kivy.resources import resource_find
|
||||
from kivy.core.text import LabelBase, FontContextManagerBase
|
||||
from kivy.core.text._text_pango import (
|
||||
KivyPangoRenderer,
|
||||
kpango_get_extents,
|
||||
kpango_get_ascent,
|
||||
kpango_get_descent,
|
||||
kpango_find_base_dir,
|
||||
kpango_font_context_exists,
|
||||
kpango_font_context_create,
|
||||
kpango_font_context_destroy,
|
||||
kpango_font_context_add_font,
|
||||
kpango_font_context_list,
|
||||
kpango_font_context_list_custom,
|
||||
kpango_font_context_list_families)
|
||||
|
||||
|
||||
class LabelPango(LabelBase):
|
||||
|
||||
_font_family_support = True
|
||||
|
||||
def __init__(self, *largs, **kwargs):
|
||||
self.get_extents = MethodType(kpango_get_extents, self)
|
||||
self.get_ascent = MethodType(kpango_get_ascent, self)
|
||||
self.get_descent = MethodType(kpango_get_descent, self)
|
||||
super(LabelPango, self).__init__(*largs, **kwargs)
|
||||
|
||||
find_base_direction = staticmethod(kpango_find_base_dir)
|
||||
|
||||
def _render_begin(self):
|
||||
self._rdr = KivyPangoRenderer(*self._size)
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
self._rdr.render(self, text, x, y)
|
||||
|
||||
def _render_end(self):
|
||||
imgdata = self._rdr.get_ImageData()
|
||||
del self._rdr
|
||||
return imgdata
|
||||
|
||||
|
||||
class PangoFontContextManager(FontContextManagerBase):
|
||||
create = staticmethod(kpango_font_context_create)
|
||||
exists = staticmethod(kpango_font_context_exists)
|
||||
destroy = staticmethod(kpango_font_context_destroy)
|
||||
list = staticmethod(kpango_font_context_list)
|
||||
list_families = staticmethod(kpango_font_context_list_families)
|
||||
list_custom = staticmethod(kpango_font_context_list_custom)
|
||||
|
||||
@staticmethod
|
||||
def add_font(font_context, filename, autocreate=True, family=None):
|
||||
if not autocreate and not PangoFontContextManager.exists(font_context):
|
||||
raise Exception("FontContextManager: Attempt to add font file "
|
||||
"'{}' to non-existing context '{}' without "
|
||||
"autocreate.".format(filename, font_context))
|
||||
if not filename:
|
||||
raise Exception("FontContextManager: Cannot add empty font file")
|
||||
if not isfile(filename):
|
||||
filename = resource_find(filename)
|
||||
if not isfile(filename):
|
||||
if not filename.endswith('.ttf'):
|
||||
filename = resource_find('{}.ttf'.format(filename))
|
||||
if filename and isfile(filename):
|
||||
return kpango_font_context_add_font(font_context, filename)
|
||||
raise Exception("FontContextManager: Attempt to add non-existent "
|
||||
"font file: '{}' to context '{}'"
|
||||
.format(filename, font_context))
|
||||
@@ -0,0 +1,84 @@
|
||||
'''
|
||||
Text PIL: Draw text with PIL
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPIL', )
|
||||
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
||||
|
||||
from kivy.compat import text_type
|
||||
from kivy.core.text import LabelBase
|
||||
from kivy.core.image import ImageData
|
||||
|
||||
# used for fetching extends before creature image surface
|
||||
default_font = ImageFont.load_default()
|
||||
|
||||
|
||||
class LabelPIL(LabelBase):
|
||||
_cache = {}
|
||||
|
||||
def _select_font(self):
|
||||
if self.options['font_size'] < 1:
|
||||
return None
|
||||
|
||||
fontsize = int(self.options['font_size'])
|
||||
fontname = self.options['font_name_r']
|
||||
try:
|
||||
id = '%s.%s' % (text_type(fontname), text_type(fontsize))
|
||||
except UnicodeDecodeError:
|
||||
id = '%s.%s' % (fontname, fontsize)
|
||||
|
||||
if id not in self._cache:
|
||||
font = ImageFont.truetype(fontname, fontsize)
|
||||
self._cache[id] = font
|
||||
|
||||
return self._cache[id]
|
||||
|
||||
def get_extents(self, text):
|
||||
font = self._select_font()
|
||||
if not font:
|
||||
return 0, 0
|
||||
|
||||
left, top, right, bottom = font.getbbox(text)
|
||||
ascent, descent = font.getmetrics()
|
||||
|
||||
if self.options['limit_render_to_text_bbox']:
|
||||
h = bottom - top
|
||||
else:
|
||||
h = ascent + descent
|
||||
w = right - left
|
||||
|
||||
return w, h
|
||||
|
||||
def get_cached_extents(self):
|
||||
return self.get_extents
|
||||
|
||||
def _render_begin(self):
|
||||
# create a surface, context, font...
|
||||
self._pil_im = Image.new('RGBA', self._size, color=(255, 255, 255, 0))
|
||||
self._pil_draw = ImageDraw.Draw(self._pil_im)
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
font = self._select_font()
|
||||
if not font:
|
||||
return
|
||||
|
||||
color = tuple([int(c * 255) for c in self.options['color']])
|
||||
|
||||
# Adjust x and y position to avoid text cutoff
|
||||
if self.options['limit_render_to_text_bbox']:
|
||||
bbox = font.getbbox(text)
|
||||
x -= bbox[0]
|
||||
y -= bbox[1]
|
||||
|
||||
self._pil_draw.text((int(x), int(y)), text, font=font, fill=color)
|
||||
|
||||
def _render_end(self):
|
||||
data = ImageData(self._size[0], self._size[1],
|
||||
self._pil_im.mode.lower(), self._pil_im.tobytes())
|
||||
|
||||
del self._pil_im
|
||||
del self._pil_draw
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,117 @@
|
||||
'''
|
||||
Text Pygame: Draw text with pygame
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('LabelPygame', )
|
||||
|
||||
from kivy.compat import PY2
|
||||
from kivy.core.text import LabelBase
|
||||
from kivy.core.image import ImageData
|
||||
from kivy.utils import deprecated
|
||||
|
||||
try:
|
||||
import pygame
|
||||
except:
|
||||
raise
|
||||
|
||||
pygame_cache = {}
|
||||
pygame_font_handles = {}
|
||||
pygame_cache_order = []
|
||||
|
||||
# init pygame font
|
||||
try:
|
||||
pygame.ftfont.init()
|
||||
except:
|
||||
pygame.font.init()
|
||||
|
||||
|
||||
class LabelPygame(LabelBase):
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, *largs, **kwargs):
|
||||
super(LabelPygame, self).__init__(*largs, **kwargs)
|
||||
|
||||
def _get_font_id(self):
|
||||
return '|'.join([str(self.options[x]) for x in
|
||||
('font_size', 'font_name_r', 'bold', 'italic')])
|
||||
|
||||
def _get_font(self):
|
||||
fontid = self._get_font_id()
|
||||
if fontid not in pygame_cache:
|
||||
# try first the file if it's a filename
|
||||
font_handle = fontobject = None
|
||||
fontname = self.options['font_name_r']
|
||||
ext = fontname.rsplit('.', 1)
|
||||
if len(ext) == 2:
|
||||
# try to open the font if it has an extension
|
||||
font_handle = open(fontname, 'rb')
|
||||
fontobject = pygame.font.Font(font_handle,
|
||||
int(self.options['font_size']))
|
||||
|
||||
# fallback to search a system font
|
||||
if fontobject is None:
|
||||
# try to search the font
|
||||
font = pygame.font.match_font(
|
||||
self.options['font_name_r'].replace(' ', ''),
|
||||
bold=self.options['bold'],
|
||||
italic=self.options['italic'])
|
||||
|
||||
# fontobject
|
||||
fontobject = pygame.font.Font(font,
|
||||
int(self.options['font_size']))
|
||||
pygame_cache[fontid] = fontobject
|
||||
pygame_font_handles[fontid] = font_handle
|
||||
pygame_cache_order.append(fontid)
|
||||
|
||||
# to prevent too much file open, limit the number of opened fonts to 64
|
||||
while len(pygame_cache_order) > 64:
|
||||
popid = pygame_cache_order.pop(0)
|
||||
del pygame_cache[popid]
|
||||
font_handle = pygame_font_handles.pop(popid)
|
||||
if font_handle is not None:
|
||||
font_handle.close()
|
||||
|
||||
return pygame_cache[fontid]
|
||||
|
||||
def get_ascent(self):
|
||||
return self._get_font().get_ascent()
|
||||
|
||||
def get_descent(self):
|
||||
return self._get_font().get_descent()
|
||||
|
||||
def get_extents(self, text):
|
||||
return self._get_font().size(text)
|
||||
|
||||
def get_cached_extents(self):
|
||||
return self._get_font().size
|
||||
|
||||
def _render_begin(self):
|
||||
self._pygame_surface = pygame.Surface(self._size, pygame.SRCALPHA, 32)
|
||||
self._pygame_surface.fill((0, 0, 0, 0))
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
font = self._get_font()
|
||||
color = [c * 255 for c in self.options['color']]
|
||||
color[0], color[2] = color[2], color[0]
|
||||
try:
|
||||
text = font.render(text, True, color)
|
||||
text.set_colorkey(color)
|
||||
self._pygame_surface.blit(text, (x, y), None,
|
||||
pygame.BLEND_RGBA_ADD)
|
||||
except pygame.error:
|
||||
pass
|
||||
|
||||
def _render_end(self):
|
||||
w, h = self._size
|
||||
data = ImageData(w, h,
|
||||
'rgba', self._pygame_surface.get_buffer().raw)
|
||||
|
||||
del self._pygame_surface
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,50 @@
|
||||
'''
|
||||
SDL2 text provider
|
||||
==================
|
||||
|
||||
Based on SDL2 + SDL2_ttf
|
||||
'''
|
||||
|
||||
__all__ = ('LabelSDL2', )
|
||||
|
||||
from kivy.compat import PY2
|
||||
from kivy.core.text import LabelBase
|
||||
try:
|
||||
from kivy.core.text._text_sdl2 import (_SurfaceContainer, _get_extents,
|
||||
_get_fontdescent, _get_fontascent)
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'text', 'sdl2', 'kivy.core.text._text_sdl2')
|
||||
raise
|
||||
|
||||
|
||||
class LabelSDL2(LabelBase):
|
||||
|
||||
def _get_font_id(self):
|
||||
return '|'.join([str(self.options[x]) for x
|
||||
in ('font_size', 'font_name_r', 'bold',
|
||||
'italic', 'underline', 'strikethrough')])
|
||||
|
||||
def get_extents(self, text):
|
||||
try:
|
||||
if PY2:
|
||||
text = text.encode('UTF-8')
|
||||
except:
|
||||
pass
|
||||
return _get_extents(self, text)
|
||||
|
||||
def get_descent(self):
|
||||
return _get_fontdescent(self)
|
||||
|
||||
def get_ascent(self):
|
||||
return _get_fontascent(self)
|
||||
|
||||
def _render_begin(self):
|
||||
self._surface = _SurfaceContainer(self._size[0], self._size[1])
|
||||
|
||||
def _render_text(self, text, x, y):
|
||||
self._surface.render(self, text, x, y)
|
||||
|
||||
def _render_end(self):
|
||||
return self._surface.get_data()
|
||||
@@ -0,0 +1,219 @@
|
||||
'''
|
||||
Video
|
||||
=====
|
||||
|
||||
Core class for reading video files and managing the video
|
||||
:class:`~kivy.graphics.texture.Texture`.
|
||||
|
||||
.. versionchanged:: 1.10.0
|
||||
The pyglet, pygst and gi providers have been removed.
|
||||
|
||||
.. versionchanged:: 1.8.0
|
||||
There are now 2 distinct Gstreamer implementations: one using Gi/Gst
|
||||
working for both Python 2+3 with Gstreamer 1.0, and one using PyGST
|
||||
working only for Python 2 + Gstreamer 0.10.
|
||||
|
||||
.. note::
|
||||
|
||||
Recording is not supported.
|
||||
'''
|
||||
|
||||
__all__ = ('VideoBase', 'Video')
|
||||
|
||||
from kivy.clock import Clock
|
||||
from kivy.core import core_select_lib
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.logger import Logger
|
||||
from kivy.compat import PY2
|
||||
|
||||
|
||||
class VideoBase(EventDispatcher):
|
||||
'''VideoBase, a class used to implement a video reader.
|
||||
|
||||
:Parameters:
|
||||
`filename`: str
|
||||
Filename of the video. Can be a file or an URI.
|
||||
`eos`: str, defaults to 'pause'
|
||||
Action to take when EOS is hit. Can be one of 'pause', 'stop' or
|
||||
'loop'.
|
||||
|
||||
.. versionchanged:: 1.4.0
|
||||
added 'pause'
|
||||
|
||||
`async`: bool, defaults to True
|
||||
Load the video asynchronously (may be not supported by all
|
||||
providers).
|
||||
`autoplay`: bool, defaults to False
|
||||
Auto play the video on init.
|
||||
|
||||
:Events:
|
||||
`on_eos`
|
||||
Fired when EOS is hit.
|
||||
`on_load`
|
||||
Fired when the video is loaded and the texture is available.
|
||||
`on_frame`
|
||||
Fired when a new frame is written to the texture.
|
||||
'''
|
||||
|
||||
__slots__ = ('_wantplay', '_buffer', '_filename', '_texture',
|
||||
'_volume', 'eos', '_state', '_async', '_autoplay')
|
||||
|
||||
__events__ = ('on_eos', 'on_load', 'on_frame')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('filename', None)
|
||||
kwargs.setdefault('eos', 'stop')
|
||||
kwargs.setdefault('async', True)
|
||||
kwargs.setdefault('autoplay', False)
|
||||
|
||||
super(VideoBase, self).__init__()
|
||||
|
||||
self._wantplay = False
|
||||
self._buffer = None
|
||||
self._filename = None
|
||||
self._texture = None
|
||||
self._volume = 1.
|
||||
self._state = ''
|
||||
|
||||
self._autoplay = kwargs.get('autoplay')
|
||||
self._async = kwargs.get('async')
|
||||
self.eos = kwargs.get('eos')
|
||||
if self.eos == 'pause':
|
||||
Logger.warning("'pause' is deprecated. Use 'stop' instead.")
|
||||
self.eos = 'stop'
|
||||
self.filename = kwargs.get('filename')
|
||||
|
||||
Clock.schedule_interval(self._update, 1 / 30.)
|
||||
|
||||
if self._autoplay:
|
||||
self.play()
|
||||
|
||||
def __del__(self):
|
||||
self.unload()
|
||||
|
||||
def on_eos(self):
|
||||
pass
|
||||
|
||||
def on_load(self):
|
||||
pass
|
||||
|
||||
def on_frame(self):
|
||||
pass
|
||||
|
||||
def _get_filename(self):
|
||||
return self._filename
|
||||
|
||||
def _set_filename(self, filename):
|
||||
if filename == self._filename:
|
||||
return
|
||||
self.unload()
|
||||
self._filename = filename
|
||||
if self._filename is None:
|
||||
return
|
||||
self.load()
|
||||
|
||||
filename = property(lambda self: self._get_filename(),
|
||||
lambda self, x: self._set_filename(x),
|
||||
doc='Get/set the filename/uri of the current video')
|
||||
|
||||
def _get_position(self):
|
||||
return 0
|
||||
|
||||
def _set_position(self, pos):
|
||||
self.seek(pos)
|
||||
|
||||
position = property(lambda self: self._get_position(),
|
||||
lambda self, x: self._set_position(x),
|
||||
doc='Get/set the position in the video (in seconds)')
|
||||
|
||||
def _get_volume(self):
|
||||
return self._volume
|
||||
|
||||
def _set_volume(self, volume):
|
||||
self._volume = volume
|
||||
|
||||
volume = property(lambda self: self._get_volume(),
|
||||
lambda self, x: self._set_volume(x),
|
||||
doc='Get/set the volume in the video (1.0 = 100%)')
|
||||
|
||||
def _get_duration(self):
|
||||
return 0
|
||||
|
||||
duration = property(lambda self: self._get_duration(),
|
||||
doc='Get the video duration (in seconds)')
|
||||
|
||||
def _get_texture(self):
|
||||
return self._texture
|
||||
|
||||
texture = property(lambda self: self._get_texture(),
|
||||
doc='Get the video texture')
|
||||
|
||||
def _get_state(self):
|
||||
return self._state
|
||||
|
||||
state = property(lambda self: self._get_state(),
|
||||
doc='Get the video playing status')
|
||||
|
||||
def _do_eos(self, *args):
|
||||
'''
|
||||
.. versionchanged:: 1.4.0
|
||||
Now dispatches the `on_eos` event.
|
||||
'''
|
||||
if self.eos == 'pause':
|
||||
self.pause()
|
||||
elif self.eos == 'stop':
|
||||
self.stop()
|
||||
elif self.eos == 'loop':
|
||||
self.position = 0
|
||||
self.play()
|
||||
|
||||
self.dispatch('on_eos')
|
||||
|
||||
def _update(self, dt):
|
||||
'''Update the video content to texture.
|
||||
'''
|
||||
pass
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
'''Move to position as percentage (strictly, a proportion from
|
||||
0 - 1) of the duration'''
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
'''Stop the video playing'''
|
||||
self._state = ''
|
||||
|
||||
def pause(self):
|
||||
'''Pause the video
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
'''
|
||||
self._state = 'paused'
|
||||
|
||||
def play(self):
|
||||
'''Play the video'''
|
||||
self._state = 'playing'
|
||||
|
||||
def load(self):
|
||||
'''Load the video from the current filename'''
|
||||
pass
|
||||
|
||||
def unload(self):
|
||||
'''Unload the actual video'''
|
||||
self._state = ''
|
||||
|
||||
|
||||
# Load the appropriate provider
|
||||
video_providers = []
|
||||
try:
|
||||
from kivy.lib.gstplayer import GstPlayer # NOQA
|
||||
video_providers += [('gstplayer', 'video_gstplayer', 'VideoGstplayer')]
|
||||
except ImportError:
|
||||
pass
|
||||
video_providers += [
|
||||
('ffmpeg', 'video_ffmpeg', 'VideoFFMpeg'),
|
||||
('ffpyplayer', 'video_ffpyplayer', 'VideoFFPy'),
|
||||
('null', 'video_null', 'VideoNull')]
|
||||
|
||||
|
||||
Video = core_select_lib('video', video_providers)
|
||||
@@ -0,0 +1,106 @@
|
||||
'''
|
||||
FFmpeg video abstraction
|
||||
========================
|
||||
|
||||
.. versionadded:: 1.0.8
|
||||
|
||||
This abstraction requires ffmpeg python extensions. We have made a special
|
||||
extension that is used for the android platform but can also be used on x86
|
||||
platforms. The project is available at::
|
||||
|
||||
http://github.com/tito/ffmpeg-android
|
||||
|
||||
The extension is designed for implementing a video player.
|
||||
Refer to the documentation of the ffmpeg-android project for more information
|
||||
about the requirements.
|
||||
'''
|
||||
|
||||
try:
|
||||
import ffmpeg
|
||||
except:
|
||||
raise
|
||||
|
||||
from kivy.core.video import VideoBase
|
||||
from kivy.graphics.texture import Texture
|
||||
|
||||
|
||||
class VideoFFMpeg(VideoBase):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._do_load = False
|
||||
self._player = None
|
||||
super(VideoFFMpeg, self).__init__(**kwargs)
|
||||
|
||||
def unload(self):
|
||||
if self._player:
|
||||
self._player.stop()
|
||||
self._player = None
|
||||
self._state = ''
|
||||
self._do_load = False
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
|
||||
def play(self):
|
||||
if self._player:
|
||||
self.unload()
|
||||
self._player = ffmpeg.FFVideo(self._filename)
|
||||
self._player.set_volume(self._volume)
|
||||
self._do_load = True
|
||||
|
||||
def stop(self):
|
||||
self.unload()
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
if self._player is None:
|
||||
return
|
||||
self._player.seek(percent)
|
||||
|
||||
def _do_eos(self):
|
||||
self.unload()
|
||||
self.dispatch('on_eos')
|
||||
super(VideoFFMpeg, self)._do_eos()
|
||||
|
||||
def _update(self, dt):
|
||||
if self._do_load:
|
||||
self._player.open()
|
||||
self._do_load = False
|
||||
return
|
||||
|
||||
player = self._player
|
||||
if player is None:
|
||||
return
|
||||
if not player.is_open:
|
||||
self._do_eos()
|
||||
return
|
||||
|
||||
frame = player.get_next_frame()
|
||||
if frame is None:
|
||||
return
|
||||
|
||||
# first time we got a frame, we know that video is read now.
|
||||
if self._texture is None:
|
||||
self._texture = Texture.create(size=(
|
||||
player.get_width(), player.get_height()),
|
||||
colorfmt='rgb')
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
if self._texture:
|
||||
self._texture.blit_buffer(frame)
|
||||
self.dispatch('on_frame')
|
||||
|
||||
def _get_duration(self):
|
||||
if self._player is None:
|
||||
return 0
|
||||
return self._player.get_duration()
|
||||
|
||||
def _get_position(self):
|
||||
if self._player is None:
|
||||
return 0
|
||||
return self._player.get_position()
|
||||
|
||||
def _set_volume(self, value):
|
||||
self._volume = value
|
||||
if self._player:
|
||||
self._player.set_volume(self._volume)
|
||||
@@ -0,0 +1,449 @@
|
||||
'''
|
||||
FFmpeg based video abstraction
|
||||
==============================
|
||||
|
||||
To use, you need to install ffpyplayer and have a compiled ffmpeg shared
|
||||
library.
|
||||
|
||||
https://github.com/matham/ffpyplayer
|
||||
|
||||
The docs there describe how to set this up. But briefly, first you need to
|
||||
compile ffmpeg using the shared flags while disabling the static flags (you'll
|
||||
probably have to set the fPIC flag, e.g. CFLAGS=-fPIC). Here are some
|
||||
instructions: https://trac.ffmpeg.org/wiki/CompilationGuide. For Windows, you
|
||||
can download compiled GPL binaries from http://ffmpeg.zeranoe.com/builds/.
|
||||
Similarly, you should download SDL2.
|
||||
|
||||
Now, you should have ffmpeg and sdl directories. In each, you should have an
|
||||
'include', 'bin' and 'lib' directory, where e.g. for Windows, 'lib' contains
|
||||
the .dll.a files, while 'bin' contains the actual dlls. The 'include' directory
|
||||
holds the headers. The 'bin' directory is only needed if the shared libraries
|
||||
are not already in the path. In the environment, define FFMPEG_ROOT and
|
||||
SDL_ROOT, each pointing to the ffmpeg and SDL directories respectively. (If
|
||||
you're using SDL2, the 'include' directory will contain an 'SDL2' directory,
|
||||
which then holds the headers).
|
||||
|
||||
Once defined, download the ffpyplayer git repo and run
|
||||
|
||||
python setup.py build_ext --inplace
|
||||
|
||||
Finally, before running you need to ensure that ffpyplayer is in python's path.
|
||||
|
||||
..Note::
|
||||
|
||||
When kivy exits by closing the window while the video is playing,
|
||||
it appears that the __del__method of VideoFFPy
|
||||
is not called. Because of this, the VideoFFPy object is not
|
||||
properly deleted when kivy exits. The consequence is that because
|
||||
MediaPlayer creates internal threads which do not have their daemon
|
||||
flag set, when the main threads exists, it'll hang and wait for the other
|
||||
MediaPlayer threads to exit. But since __del__ is not called to delete the
|
||||
MediaPlayer object, those threads will remain alive, hanging kivy. What
|
||||
this means is that you have to be sure to delete the MediaPlayer object
|
||||
before kivy exits by setting it to None.
|
||||
'''
|
||||
|
||||
__all__ = ('VideoFFPy', )
|
||||
|
||||
try:
|
||||
import ffpyplayer
|
||||
from ffpyplayer.player import MediaPlayer
|
||||
from ffpyplayer.tools import set_log_callback, get_log_callback
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
from threading import Thread
|
||||
from queue import Queue, Empty, Full
|
||||
from kivy.clock import Clock, mainthread
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.video import VideoBase
|
||||
from kivy.graphics import Rectangle, BindTexture
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.graphics.fbo import Fbo
|
||||
from kivy.weakmethod import WeakMethod
|
||||
import time
|
||||
|
||||
Logger.info('VideoFFPy: Using ffpyplayer {}'.format(ffpyplayer.version))
|
||||
|
||||
|
||||
logger_func = {'quiet': Logger.critical, 'panic': Logger.critical,
|
||||
'fatal': Logger.critical, 'error': Logger.error,
|
||||
'warning': Logger.warning, 'info': Logger.info,
|
||||
'verbose': Logger.debug, 'debug': Logger.debug}
|
||||
|
||||
|
||||
def _log_callback(message, level):
|
||||
message = message.strip()
|
||||
if message:
|
||||
logger_func[level]('ffpyplayer: {}'.format(message))
|
||||
|
||||
|
||||
if not get_log_callback():
|
||||
set_log_callback(_log_callback)
|
||||
|
||||
|
||||
class VideoFFPy(VideoBase):
|
||||
|
||||
YUV_RGB_FS = """
|
||||
$HEADER$
|
||||
uniform sampler2D tex_y;
|
||||
uniform sampler2D tex_u;
|
||||
uniform sampler2D tex_v;
|
||||
|
||||
void main(void) {
|
||||
float y = texture2D(tex_y, tex_coord0).r;
|
||||
float u = texture2D(tex_u, tex_coord0).r - 0.5;
|
||||
float v = texture2D(tex_v, tex_coord0).r - 0.5;
|
||||
float r = y + 1.402 * v;
|
||||
float g = y - 0.344 * u - 0.714 * v;
|
||||
float b = y + 1.772 * u;
|
||||
gl_FragColor = vec4(r, g, b, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
_trigger = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._ffplayer = None
|
||||
self._thread = None
|
||||
self._next_frame = None
|
||||
self._seek_queue = []
|
||||
self._ffplayer_need_quit = False
|
||||
self._wakeup_queue = Queue(maxsize=1)
|
||||
self._trigger = Clock.create_trigger(self._redraw)
|
||||
|
||||
super(VideoFFPy, self).__init__(**kwargs)
|
||||
|
||||
@property
|
||||
def _is_stream(self):
|
||||
# This is only used when building ff_opts, to prevent starting
|
||||
# player paused and can probably be removed as soon as the 'eof'
|
||||
# receiving issue is solved.
|
||||
# See https://github.com/matham/ffpyplayer/issues/142
|
||||
return self.filename.startswith('rtsp://')
|
||||
|
||||
def __del__(self):
|
||||
self.unload()
|
||||
|
||||
def _wakeup_thread(self):
|
||||
try:
|
||||
self._wakeup_queue.put(None, False)
|
||||
except Full:
|
||||
pass
|
||||
|
||||
def _wait_for_wakeup(self, timeout):
|
||||
try:
|
||||
self._wakeup_queue.get(True, timeout)
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def _player_callback(self, selector, value):
|
||||
if self._ffplayer is None:
|
||||
return
|
||||
if selector == 'quit':
|
||||
def close(*args):
|
||||
self.unload()
|
||||
Clock.schedule_once(close, 0)
|
||||
|
||||
def _get_position(self):
|
||||
if self._ffplayer is not None:
|
||||
return self._ffplayer.get_pts()
|
||||
return 0
|
||||
|
||||
def _set_position(self, pos):
|
||||
self.seek(pos)
|
||||
|
||||
def _set_volume(self, volume):
|
||||
self._volume = volume
|
||||
if self._ffplayer is not None:
|
||||
self._ffplayer.set_volume(self._volume)
|
||||
|
||||
def _get_duration(self):
|
||||
if self._ffplayer is None:
|
||||
return 0
|
||||
return self._ffplayer.get_metadata()['duration']
|
||||
|
||||
@mainthread
|
||||
def _do_eos(self):
|
||||
if self.eos == 'pause':
|
||||
self.pause()
|
||||
elif self.eos == 'stop':
|
||||
self.stop()
|
||||
elif self.eos == 'loop':
|
||||
# this causes a seek to zero
|
||||
self.position = 0
|
||||
|
||||
self.dispatch('on_eos')
|
||||
|
||||
@mainthread
|
||||
def _finish_setup(self):
|
||||
# once setup is done, we make sure player state matches what user
|
||||
# could have requested while player was being setup and it was in limbo
|
||||
# also, thread starts player in internal paused mode, so this unpauses
|
||||
# it if user didn't request pause meanwhile
|
||||
if self._ffplayer is not None:
|
||||
self._ffplayer.set_volume(self._volume)
|
||||
self._ffplayer.set_pause(self._state == 'paused')
|
||||
self._wakeup_thread()
|
||||
|
||||
def _redraw(self, *args):
|
||||
if not self._ffplayer:
|
||||
return
|
||||
next_frame = self._next_frame
|
||||
if not next_frame:
|
||||
return
|
||||
|
||||
img, pts = next_frame
|
||||
if img.get_size() != self._size or self._texture is None:
|
||||
self._size = w, h = img.get_size()
|
||||
|
||||
if self._out_fmt == 'yuv420p':
|
||||
w2 = int(w / 2)
|
||||
h2 = int(h / 2)
|
||||
self._tex_y = Texture.create(
|
||||
size=(w, h), colorfmt='luminance')
|
||||
self._tex_u = Texture.create(
|
||||
size=(w2, h2), colorfmt='luminance')
|
||||
self._tex_v = Texture.create(
|
||||
size=(w2, h2), colorfmt='luminance')
|
||||
self._fbo = fbo = Fbo(size=self._size)
|
||||
with fbo:
|
||||
BindTexture(texture=self._tex_u, index=1)
|
||||
BindTexture(texture=self._tex_v, index=2)
|
||||
Rectangle(size=fbo.size, texture=self._tex_y)
|
||||
fbo.shader.fs = VideoFFPy.YUV_RGB_FS
|
||||
fbo['tex_y'] = 0
|
||||
fbo['tex_u'] = 1
|
||||
fbo['tex_v'] = 2
|
||||
self._texture = fbo.texture
|
||||
else:
|
||||
self._texture = Texture.create(size=self._size,
|
||||
colorfmt='rgba')
|
||||
|
||||
# XXX FIXME
|
||||
# self.texture.add_reload_observer(self.reload_buffer)
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
if self._texture:
|
||||
if self._out_fmt == 'yuv420p':
|
||||
dy, du, dv, _ = img.to_memoryview()
|
||||
if dy and du and dv:
|
||||
self._tex_y.blit_buffer(dy, colorfmt='luminance')
|
||||
self._tex_u.blit_buffer(du, colorfmt='luminance')
|
||||
self._tex_v.blit_buffer(dv, colorfmt='luminance')
|
||||
self._fbo.ask_update()
|
||||
self._fbo.draw()
|
||||
else:
|
||||
self._texture.blit_buffer(
|
||||
img.to_memoryview()[0], colorfmt='rgba')
|
||||
|
||||
self.dispatch('on_frame')
|
||||
|
||||
def _next_frame_run(self, ffplayer):
|
||||
sleep = time.sleep
|
||||
trigger = self._trigger
|
||||
did_dispatch_eof = False
|
||||
wait_for_wakeup = self._wait_for_wakeup
|
||||
seek_queue = self._seek_queue
|
||||
# video starts in internal paused state
|
||||
|
||||
# fast path, if the source video is yuv420p, we'll use a glsl shader
|
||||
# for buffer conversion to rgba
|
||||
# wait until we get frame metadata
|
||||
while not self._ffplayer_need_quit:
|
||||
src_pix_fmt = ffplayer.get_metadata().get('src_pix_fmt')
|
||||
if not src_pix_fmt:
|
||||
wait_for_wakeup(0.005)
|
||||
continue
|
||||
|
||||
# ffpyplayer reports src_pix_fmt as bytes. this may or may not
|
||||
# change in future, so we check for both bytes and str
|
||||
if src_pix_fmt in (b'yuv420p', 'yuv420p'):
|
||||
self._out_fmt = 'yuv420p'
|
||||
ffplayer.set_output_pix_fmt(self._out_fmt)
|
||||
break
|
||||
|
||||
if self._ffplayer_need_quit:
|
||||
ffplayer.close_player()
|
||||
return
|
||||
|
||||
self._ffplayer = ffplayer
|
||||
self._finish_setup()
|
||||
# now, we'll be in internal paused state and loop will wait until
|
||||
# mainthread unpauses us when finishing setup
|
||||
|
||||
while not self._ffplayer_need_quit:
|
||||
seek_happened = False
|
||||
if seek_queue:
|
||||
vals = seek_queue[:]
|
||||
del seek_queue[:len(vals)]
|
||||
percent, precise = vals[-1]
|
||||
ffplayer.seek(
|
||||
percent * ffplayer.get_metadata()['duration'],
|
||||
relative=False,
|
||||
accurate=precise
|
||||
)
|
||||
seek_happened = True
|
||||
did_dispatch_eof = False
|
||||
self._next_frame = None
|
||||
|
||||
# Get next frame if paused:
|
||||
if seek_happened and ffplayer.get_pause():
|
||||
ffplayer.set_volume(0.0) # Try to do it silently.
|
||||
ffplayer.set_pause(False)
|
||||
try:
|
||||
# We don't know concrete number of frames to skip,
|
||||
# this number worked fine on couple of tested videos:
|
||||
to_skip = 6
|
||||
while True:
|
||||
frame, val = ffplayer.get_frame(show=False)
|
||||
# Exit loop on invalid val:
|
||||
if val in ('paused', 'eof'):
|
||||
break
|
||||
# Exit loop on seek_queue updated:
|
||||
if seek_queue:
|
||||
break
|
||||
# Wait for next frame:
|
||||
if frame is None:
|
||||
sleep(0.005)
|
||||
continue
|
||||
# Wait until we skipped enough frames:
|
||||
to_skip -= 1
|
||||
if to_skip == 0:
|
||||
break
|
||||
# Assuming last frame is actual, just get it:
|
||||
frame, val = ffplayer.get_frame(force_refresh=True)
|
||||
finally:
|
||||
ffplayer.set_pause(bool(self._state == 'paused'))
|
||||
# todo: this is not safe because user could have updated
|
||||
# volume between us reading it and setting it
|
||||
ffplayer.set_volume(self._volume)
|
||||
# Get next frame regular:
|
||||
else:
|
||||
frame, val = ffplayer.get_frame()
|
||||
|
||||
if val == 'eof':
|
||||
if not did_dispatch_eof:
|
||||
self._do_eos()
|
||||
did_dispatch_eof = True
|
||||
wait_for_wakeup(None)
|
||||
elif val == 'paused':
|
||||
did_dispatch_eof = False
|
||||
wait_for_wakeup(None)
|
||||
else:
|
||||
did_dispatch_eof = False
|
||||
if frame:
|
||||
self._next_frame = frame
|
||||
trigger()
|
||||
else:
|
||||
val = val if val else (1 / 30.)
|
||||
wait_for_wakeup(val)
|
||||
|
||||
ffplayer.close_player()
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
# still save seek while thread is setting up
|
||||
self._seek_queue.append((percent, precise,))
|
||||
self._wakeup_thread()
|
||||
|
||||
def stop(self):
|
||||
self.unload()
|
||||
|
||||
def pause(self):
|
||||
# if state hasn't been set (empty), there's no player. If it's
|
||||
# paused, nothing to do so just handle playing
|
||||
if self._state == 'playing':
|
||||
# we could be in limbo while player is setting up so check. Player
|
||||
# will pause when finishing setting up
|
||||
if self._ffplayer is not None:
|
||||
self._ffplayer.set_pause(True)
|
||||
# even in limbo, indicate to start in paused state
|
||||
self._state = 'paused'
|
||||
self._wakeup_thread()
|
||||
|
||||
def play(self):
|
||||
# _state starts empty and is empty again after unloading
|
||||
if self._ffplayer:
|
||||
# player is already setup, just handle unpausing
|
||||
assert self._state in ('paused', 'playing')
|
||||
if self._state == 'paused':
|
||||
self._ffplayer.set_pause(False)
|
||||
self._state = 'playing'
|
||||
self._wakeup_thread()
|
||||
return
|
||||
|
||||
# we're now either in limbo state waiting for thread to setup,
|
||||
# or no thread has been started
|
||||
if self._state == 'playing':
|
||||
# in limbo, just wait for thread to setup player
|
||||
return
|
||||
elif self._state == 'paused':
|
||||
# in limbo, still unpause for when player becomes ready
|
||||
self._state = 'playing'
|
||||
self._wakeup_thread()
|
||||
return
|
||||
|
||||
# load first unloads
|
||||
self.load()
|
||||
self._out_fmt = 'rgba'
|
||||
# if no stream, it starts internally paused, but unpauses itself
|
||||
# if stream and we start paused, we sometimes receive eof after a
|
||||
# few frames, depending on the stream producer.
|
||||
# XXX: This probably needs to be figured out in ffpyplayer, using
|
||||
# ffplay directly works.
|
||||
ff_opts = {
|
||||
'paused': not self._is_stream,
|
||||
'out_fmt': self._out_fmt,
|
||||
'sn': True,
|
||||
'volume': self._volume,
|
||||
}
|
||||
ffplayer = MediaPlayer(
|
||||
self._filename, callback=self._player_callback,
|
||||
thread_lib='SDL',
|
||||
loglevel='info', ff_opts=ff_opts
|
||||
)
|
||||
|
||||
# Disabled as an attempt to fix kivy issue #6210
|
||||
# self._ffplayer.set_volume(self._volume)
|
||||
|
||||
self._thread = Thread(
|
||||
target=self._next_frame_run,
|
||||
name='Next frame',
|
||||
args=(ffplayer, )
|
||||
)
|
||||
# todo: remove
|
||||
self._thread.daemon = True
|
||||
|
||||
# start in playing mode, but _ffplayer isn't set until ready. We're
|
||||
# now in a limbo state
|
||||
self._state = 'playing'
|
||||
self._thread.start()
|
||||
|
||||
def load(self):
|
||||
self.unload()
|
||||
|
||||
def unload(self):
|
||||
# no need to call self._trigger.cancel() because _ffplayer is set
|
||||
# to None below, and it's not safe to call clock stuff from __del__
|
||||
|
||||
# if thread is still alive, set it to exit and wake it
|
||||
self._wakeup_thread()
|
||||
self._ffplayer_need_quit = True
|
||||
# wait until it exits
|
||||
if self._thread:
|
||||
# TODO: use callback, don't block here
|
||||
self._thread.join()
|
||||
self._thread = None
|
||||
|
||||
if self._ffplayer:
|
||||
self._ffplayer = None
|
||||
self._next_frame = None
|
||||
self._size = (0, 0)
|
||||
self._state = ''
|
||||
self._seek_queue = []
|
||||
|
||||
# reset for next load since thread is dead for sure
|
||||
self._ffplayer_need_quit = False
|
||||
self._wakeup_queue = Queue(maxsize=1)
|
||||
@@ -0,0 +1,140 @@
|
||||
'''
|
||||
Video Gstplayer
|
||||
===============
|
||||
|
||||
.. versionadded:: 1.8.0
|
||||
|
||||
Implementation of a VideoBase with Kivy :class:`~kivy.lib.gstplayer.GstPlayer`
|
||||
This player is the preferred player, using Gstreamer 1.0, working on both
|
||||
Python 2 and 3.
|
||||
'''
|
||||
|
||||
try:
|
||||
from kivy.lib.gstplayer import GstPlayer, get_gst_version
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'VideoGstplayer', 'gst', 'kivy.lib.gstplayer._gstplayer')
|
||||
raise
|
||||
from kivy.graphics.texture import Texture
|
||||
from kivy.core.video import VideoBase
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
from kivy.compat import PY2
|
||||
from threading import Lock
|
||||
from functools import partial
|
||||
from os.path import realpath
|
||||
from weakref import ref
|
||||
|
||||
if PY2:
|
||||
from urllib import pathname2url
|
||||
else:
|
||||
from urllib.request import pathname2url
|
||||
|
||||
Logger.info('VideoGstplayer: Using Gstreamer {}'.format(
|
||||
'.'.join(map(str, get_gst_version()))))
|
||||
|
||||
|
||||
def _on_gstplayer_buffer(video, width, height, data):
|
||||
video = video()
|
||||
# if we still receive the video but no more player, remove it.
|
||||
if not video:
|
||||
return
|
||||
with video._buffer_lock:
|
||||
video._buffer = (width, height, data)
|
||||
|
||||
|
||||
def _on_gstplayer_message(mtype, message):
|
||||
if mtype == 'error':
|
||||
Logger.error('VideoGstplayer: {}'.format(message))
|
||||
elif mtype == 'warning':
|
||||
Logger.warning('VideoGstplayer: {}'.format(message))
|
||||
elif mtype == 'info':
|
||||
Logger.info('VideoGstplayer: {}'.format(message))
|
||||
|
||||
|
||||
class VideoGstplayer(VideoBase):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.player = None
|
||||
self._buffer = None
|
||||
self._buffer_lock = Lock()
|
||||
super(VideoGstplayer, self).__init__(**kwargs)
|
||||
|
||||
def _on_gst_eos_sync(self):
|
||||
Clock.schedule_once(self._do_eos, 0)
|
||||
|
||||
def load(self):
|
||||
Logger.debug('VideoGstplayer: Load <{}>'.format(self._filename))
|
||||
uri = self._get_uri()
|
||||
wk_self = ref(self)
|
||||
self.player_callback = partial(_on_gstplayer_buffer, wk_self)
|
||||
self.player = GstPlayer(uri, self.player_callback,
|
||||
self._on_gst_eos_sync, _on_gstplayer_message)
|
||||
self.player.load()
|
||||
|
||||
def unload(self):
|
||||
if self.player:
|
||||
self.player.unload()
|
||||
self.player = None
|
||||
with self._buffer_lock:
|
||||
self._buffer = None
|
||||
self._texture = None
|
||||
|
||||
def stop(self):
|
||||
super(VideoGstplayer, self).stop()
|
||||
self.player.stop()
|
||||
|
||||
def pause(self):
|
||||
super(VideoGstplayer, self).pause()
|
||||
self.player.pause()
|
||||
|
||||
def play(self):
|
||||
super(VideoGstplayer, self).play()
|
||||
self.player.set_volume(self.volume)
|
||||
self.player.play()
|
||||
|
||||
def seek(self, percent, precise=True):
|
||||
self.player.seek(percent)
|
||||
|
||||
def _get_position(self):
|
||||
return self.player.get_position()
|
||||
|
||||
def _get_duration(self):
|
||||
return self.player.get_duration()
|
||||
|
||||
def _set_volume(self, value):
|
||||
self._volume = value
|
||||
if self.player:
|
||||
self.player.set_volume(self._volume)
|
||||
|
||||
def _update(self, dt):
|
||||
buf = None
|
||||
with self._buffer_lock:
|
||||
buf = self._buffer
|
||||
self._buffer = None
|
||||
if buf is not None:
|
||||
self._update_texture(buf)
|
||||
self.dispatch('on_frame')
|
||||
|
||||
def _update_texture(self, buf):
|
||||
width, height, data = buf
|
||||
|
||||
# texture is not allocated yet, create it first
|
||||
if not self._texture:
|
||||
self._texture = Texture.create(size=(width, height),
|
||||
colorfmt='rgb')
|
||||
self._texture.flip_vertical()
|
||||
self.dispatch('on_load')
|
||||
|
||||
if self._texture:
|
||||
self._texture.blit_buffer(
|
||||
data, size=(width, height), colorfmt='rgb')
|
||||
|
||||
def _get_uri(self):
|
||||
uri = self.filename
|
||||
if not uri:
|
||||
return
|
||||
if '://' not in uri:
|
||||
uri = 'file:' + pathname2url(realpath(uri))
|
||||
return uri
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
'''
|
||||
VideoNull: empty implementation of VideoBase for the no provider case
|
||||
'''
|
||||
|
||||
from kivy.core.video import VideoBase
|
||||
|
||||
|
||||
class VideoNull(VideoBase):
|
||||
'''VideoBase implementation when there is no provider.
|
||||
'''
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
include "../../include/config.pxi"
|
||||
|
||||
IF USE_WAYLAND:
|
||||
cdef extern from "wayland-client-protocol.h":
|
||||
cdef struct wl_display:
|
||||
pass
|
||||
|
||||
cdef struct wl_surface:
|
||||
pass
|
||||
|
||||
cdef struct wl_shell_surface:
|
||||
pass
|
||||
|
||||
IF USE_X11:
|
||||
cdef extern from "X11/Xlib.h":
|
||||
cdef struct _XDisplay:
|
||||
pass
|
||||
|
||||
ctypedef _XDisplay Display
|
||||
|
||||
ctypedef int XID
|
||||
ctypedef XID Window
|
||||
|
||||
IF UNAME_SYSNAME == 'Windows':
|
||||
cdef extern from "windows.h":
|
||||
ctypedef void *HANDLE
|
||||
|
||||
ctypedef HANDLE HWND
|
||||
ctypedef HANDLE HDC
|
||||
ctypedef HANDLE HINSTANCE
|
||||
@@ -0,0 +1,86 @@
|
||||
'''
|
||||
EGL Rpi Window: EGL Window provider, specialized for the Pi
|
||||
|
||||
Inspired by: rpi_vid_core + JF002 rpi kivy repo
|
||||
'''
|
||||
|
||||
__all__ = ('WindowEglRpi', )
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.core.window import WindowBase
|
||||
from kivy.base import EventLoop, ExceptionManager, stopTouchApp
|
||||
from kivy.lib.vidcore_lite import bcm, egl
|
||||
from os import environ
|
||||
|
||||
# Default display IDs.
|
||||
(DISPMANX_ID_MAIN_LCD,
|
||||
DISPMANX_ID_AUX_LCD,
|
||||
DISPMANX_ID_HDMI,
|
||||
DISPMANX_ID_SDTV,
|
||||
DISPMANX_ID_FORCE_LCD,
|
||||
DISPMANX_ID_FORCE_TV,
|
||||
DISPMANX_ID_FORCE_OTHER) = range(7)
|
||||
|
||||
|
||||
class WindowEglRpi(WindowBase):
|
||||
|
||||
_rpi_dispmanx_id = int(environ.get("KIVY_BCM_DISPMANX_ID", "0"))
|
||||
_rpi_dispmanx_layer = int(environ.get("KIVY_BCM_DISPMANX_LAYER", "0"))
|
||||
|
||||
gl_backends_ignored = ['sdl2']
|
||||
|
||||
def create_window(self):
|
||||
bcm.host_init()
|
||||
|
||||
w, h = bcm.graphics_get_display_size(self._rpi_dispmanx_id)
|
||||
Logger.debug('Window: Actual display size: {}x{}'.format(
|
||||
w, h))
|
||||
self._size = w, h
|
||||
self._create_window(w, h)
|
||||
self._create_egl_context(self.win, 0)
|
||||
super(WindowEglRpi, self).create_window()
|
||||
|
||||
def _create_window(self, w, h):
|
||||
dst = bcm.Rect(0, 0, w, h)
|
||||
src = bcm.Rect(0, 0, w << 16, h << 16)
|
||||
display = egl.bcm_display_open(self._rpi_dispmanx_id)
|
||||
update = egl.bcm_update_start(0)
|
||||
element = egl.bcm_element_add(
|
||||
update, display, self._rpi_dispmanx_layer, dst, src)
|
||||
self.win = egl.NativeWindow(element, w, h)
|
||||
egl.bcm_update_submit_sync(update)
|
||||
|
||||
def _create_egl_context(self, win, flags):
|
||||
api = egl._constants.EGL_OPENGL_ES_API
|
||||
c = egl._constants
|
||||
|
||||
attribs = [
|
||||
c.EGL_RED_SIZE, 8,
|
||||
c.EGL_GREEN_SIZE, 8,
|
||||
c.EGL_BLUE_SIZE, 8,
|
||||
c.EGL_ALPHA_SIZE, 8,
|
||||
c.EGL_DEPTH_SIZE, 16,
|
||||
c.EGL_STENCIL_SIZE, 8,
|
||||
c.EGL_SURFACE_TYPE, c.EGL_WINDOW_BIT,
|
||||
c.EGL_NONE]
|
||||
|
||||
attribs_context = [c.EGL_CONTEXT_CLIENT_VERSION, 2, c.EGL_NONE]
|
||||
|
||||
display = egl.GetDisplay(c.EGL_DEFAULT_DISPLAY)
|
||||
egl.Initialise(display)
|
||||
egl.BindAPI(c.EGL_OPENGL_ES_API)
|
||||
egl.GetConfigs(display)
|
||||
config = egl.ChooseConfig(display, attribs, 1)[0]
|
||||
surface = egl.CreateWindowSurface(display, config, win)
|
||||
context = egl.CreateContext(display, config, None, attribs_context)
|
||||
egl.MakeCurrent(display, surface, surface, context)
|
||||
|
||||
self.egl_info = (display, surface, context)
|
||||
egl.MakeCurrent(display, surface, surface, context)
|
||||
|
||||
def close(self):
|
||||
egl.Terminate(self.egl_info[0])
|
||||
|
||||
def flip(self):
|
||||
if not EventLoop.quit:
|
||||
egl.SwapBuffers(self.egl_info[0], self.egl_info[1])
|
||||
@@ -0,0 +1,19 @@
|
||||
include "window_attrs.pxi"
|
||||
|
||||
from libc.stdint cimport uintptr_t
|
||||
|
||||
IF USE_WAYLAND:
|
||||
cdef class WindowInfoWayland:
|
||||
cdef wl_display *display
|
||||
cdef wl_surface *surface
|
||||
cdef wl_shell_surface *shell_surface
|
||||
|
||||
IF USE_X11:
|
||||
cdef class WindowInfoX11:
|
||||
cdef Display *display
|
||||
cdef Window window
|
||||
|
||||
IF UNAME_SYSNAME == 'Windows':
|
||||
cdef class WindowInfoWindows:
|
||||
cdef HWND window
|
||||
cdef HDC hdc
|
||||
@@ -0,0 +1,441 @@
|
||||
'''
|
||||
Window Pygame: windowing provider based on Pygame
|
||||
|
||||
.. warning::
|
||||
|
||||
Pygame has been deprecated and will be removed in the release after Kivy
|
||||
1.11.0.
|
||||
'''
|
||||
|
||||
__all__ = ('WindowPygame', )
|
||||
|
||||
# fail early if possible
|
||||
import pygame
|
||||
|
||||
from kivy.compat import PY2
|
||||
from kivy.core.window import WindowBase
|
||||
from kivy.core import CoreCriticalException
|
||||
from os import environ
|
||||
from os.path import exists, join
|
||||
from kivy.config import Config
|
||||
from kivy import kivy_data_dir
|
||||
from kivy.base import ExceptionManager
|
||||
from kivy.logger import Logger
|
||||
from kivy.base import stopTouchApp, EventLoop
|
||||
from kivy.utils import platform, deprecated
|
||||
from kivy.resources import resource_find
|
||||
|
||||
try:
|
||||
android = None
|
||||
if platform == 'android':
|
||||
import android
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# late binding
|
||||
glReadPixels = GL_RGBA = GL_UNSIGNED_BYTE = None
|
||||
|
||||
|
||||
class WindowPygame(WindowBase):
|
||||
|
||||
@deprecated(
|
||||
msg='Pygame has been deprecated and will be removed after 1.11.0')
|
||||
def __init__(self, *largs, **kwargs):
|
||||
super(WindowPygame, self).__init__(*largs, **kwargs)
|
||||
|
||||
def create_window(self, *largs):
|
||||
# ensure the mouse is still not up after window creation, otherwise, we
|
||||
# have some weird bugs
|
||||
self.dispatch('on_mouse_up', 0, 0, 'all', [])
|
||||
|
||||
# force display to show (available only for fullscreen)
|
||||
displayidx = Config.getint('graphics', 'display')
|
||||
if 'SDL_VIDEO_FULLSCREEN_HEAD' not in environ and displayidx != -1:
|
||||
environ['SDL_VIDEO_FULLSCREEN_HEAD'] = '%d' % displayidx
|
||||
|
||||
# init some opengl, same as before.
|
||||
self.flags = pygame.HWSURFACE | pygame.OPENGL | pygame.DOUBLEBUF
|
||||
|
||||
# right now, activate resizable window only on linux.
|
||||
# on window / macosx, the opengl context is lost, and we need to
|
||||
# reconstruct everything. Check #168 for a state of the work.
|
||||
if platform in ('linux', 'macosx', 'win') and \
|
||||
Config.getboolean('graphics', 'resizable'):
|
||||
self.flags |= pygame.RESIZABLE
|
||||
|
||||
try:
|
||||
pygame.display.init()
|
||||
except pygame.error as e:
|
||||
raise CoreCriticalException(e.message)
|
||||
|
||||
multisamples = Config.getint('graphics', 'multisamples')
|
||||
|
||||
if multisamples > 0:
|
||||
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
|
||||
pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES,
|
||||
multisamples)
|
||||
pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 16)
|
||||
pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 1)
|
||||
pygame.display.set_caption(self.title)
|
||||
|
||||
if self.position == 'auto':
|
||||
self._pos = None
|
||||
elif self.position == 'custom':
|
||||
self._pos = self.left, self.top
|
||||
else:
|
||||
raise ValueError('position token in configuration accept only '
|
||||
'"auto" or "custom"')
|
||||
|
||||
if self._fake_fullscreen:
|
||||
if not self.borderless:
|
||||
self.fullscreen = self._fake_fullscreen = False
|
||||
elif not self.fullscreen or self.fullscreen == 'auto':
|
||||
self.borderless = self._fake_fullscreen = False
|
||||
|
||||
if self.fullscreen == 'fake':
|
||||
self.borderless = self._fake_fullscreen = True
|
||||
Logger.warning("The 'fake' fullscreen option has been "
|
||||
"deprecated, use Window.borderless or the "
|
||||
"borderless Config option instead.")
|
||||
|
||||
if self.fullscreen == 'fake' or self.borderless:
|
||||
Logger.debug('WinPygame: Set window to borderless mode.')
|
||||
|
||||
self.flags |= pygame.NOFRAME
|
||||
# If no position set in borderless mode, we always need
|
||||
# to set the position. So use 0, 0.
|
||||
if self._pos is None:
|
||||
self._pos = (0, 0)
|
||||
environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos
|
||||
|
||||
elif self.fullscreen in ('auto', True):
|
||||
Logger.debug('WinPygame: Set window to fullscreen mode')
|
||||
self.flags |= pygame.FULLSCREEN
|
||||
|
||||
elif self._pos is not None:
|
||||
environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos
|
||||
|
||||
# never stay with a None pos, application using w.center will be fired.
|
||||
self._pos = (0, 0)
|
||||
|
||||
# prepare keyboard
|
||||
repeat_delay = int(Config.get('kivy', 'keyboard_repeat_delay'))
|
||||
repeat_rate = float(Config.get('kivy', 'keyboard_repeat_rate'))
|
||||
pygame.key.set_repeat(repeat_delay, int(1000. / repeat_rate))
|
||||
|
||||
# set window icon before calling set_mode
|
||||
try:
|
||||
filename_icon = self.icon or Config.get('kivy', 'window_icon')
|
||||
if filename_icon == '':
|
||||
logo_size = 32
|
||||
if platform == 'macosx':
|
||||
logo_size = 512
|
||||
elif platform == 'win':
|
||||
logo_size = 64
|
||||
filename_icon = 'kivy-icon-{}.png'.format(logo_size)
|
||||
filename_icon = resource_find(
|
||||
join(kivy_data_dir, 'logo', filename_icon))
|
||||
self.set_icon(filename_icon)
|
||||
except:
|
||||
Logger.exception('Window: cannot set icon')
|
||||
|
||||
# try to use mode with multisamples
|
||||
try:
|
||||
self._pygame_set_mode()
|
||||
except pygame.error as e:
|
||||
if multisamples:
|
||||
Logger.warning('WinPygame: Video: failed (multisamples=%d)' %
|
||||
multisamples)
|
||||
Logger.warning('WinPygame: trying without antialiasing')
|
||||
pygame.display.gl_set_attribute(
|
||||
pygame.GL_MULTISAMPLEBUFFERS, 0)
|
||||
pygame.display.gl_set_attribute(
|
||||
pygame.GL_MULTISAMPLESAMPLES, 0)
|
||||
multisamples = 0
|
||||
try:
|
||||
self._pygame_set_mode()
|
||||
except pygame.error as e:
|
||||
raise CoreCriticalException(e.message)
|
||||
else:
|
||||
raise CoreCriticalException(e.message)
|
||||
|
||||
if pygame.RESIZABLE & self.flags:
|
||||
self._pygame_set_mode()
|
||||
|
||||
info = pygame.display.Info()
|
||||
self._size = (info.current_w, info.current_h)
|
||||
# self.dispatch('on_resize', *self._size)
|
||||
|
||||
# in order to debug futur issue with pygame/display, let's show
|
||||
# more debug output.
|
||||
Logger.debug('Window: Display driver ' + pygame.display.get_driver())
|
||||
Logger.debug('Window: Actual window size: %dx%d',
|
||||
info.current_w, info.current_h)
|
||||
if platform != 'android':
|
||||
# unsupported platform, such as android that doesn't support
|
||||
# gl_get_attribute.
|
||||
Logger.debug(
|
||||
'Window: Actual color bits r%d g%d b%d a%d',
|
||||
pygame.display.gl_get_attribute(pygame.GL_RED_SIZE),
|
||||
pygame.display.gl_get_attribute(pygame.GL_GREEN_SIZE),
|
||||
pygame.display.gl_get_attribute(pygame.GL_BLUE_SIZE),
|
||||
pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE))
|
||||
Logger.debug(
|
||||
'Window: Actual depth bits: %d',
|
||||
pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE))
|
||||
Logger.debug(
|
||||
'Window: Actual stencil bits: %d',
|
||||
pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE))
|
||||
Logger.debug(
|
||||
'Window: Actual multisampling samples: %d',
|
||||
pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES))
|
||||
super(WindowPygame, self).create_window()
|
||||
|
||||
# set mouse visibility
|
||||
self._set_cursor_state(self.show_cursor)
|
||||
|
||||
# if we are on android platform, automatically create hooks
|
||||
if android:
|
||||
from kivy.support import install_android
|
||||
install_android()
|
||||
|
||||
def close(self):
|
||||
pygame.display.quit()
|
||||
super(WindowPygame, self).close()
|
||||
|
||||
def on_title(self, instance, value):
|
||||
if self.initialized:
|
||||
pygame.display.set_caption(self.title)
|
||||
|
||||
def set_icon(self, filename):
|
||||
if not exists(filename):
|
||||
return False
|
||||
try:
|
||||
if platform == 'win':
|
||||
try:
|
||||
if self._set_icon_win(filename):
|
||||
return True
|
||||
except:
|
||||
# fallback on standard loading then.
|
||||
pass
|
||||
|
||||
# for all others platform, or if the ico is not available, use the
|
||||
# default way to set it.
|
||||
self._set_icon_standard(filename)
|
||||
super(WindowPygame, self).set_icon(filename)
|
||||
except:
|
||||
Logger.exception('WinPygame: unable to set icon')
|
||||
|
||||
def _set_icon_standard(self, filename):
|
||||
if PY2:
|
||||
try:
|
||||
im = pygame.image.load(filename)
|
||||
except UnicodeEncodeError:
|
||||
im = pygame.image.load(filename.encode('utf8'))
|
||||
else:
|
||||
im = pygame.image.load(filename)
|
||||
if im is None:
|
||||
raise Exception('Unable to load window icon (not found)')
|
||||
pygame.display.set_icon(im)
|
||||
|
||||
def _set_icon_win(self, filename):
|
||||
# ensure the window ico is ended by ico
|
||||
if not filename.endswith('.ico'):
|
||||
filename = '{}.ico'.format(filename.rsplit('.', 1)[0])
|
||||
if not exists(filename):
|
||||
return False
|
||||
|
||||
import win32api
|
||||
import win32gui
|
||||
import win32con
|
||||
hwnd = pygame.display.get_wm_info()['window']
|
||||
icon_big = win32gui.LoadImage(
|
||||
None, filename, win32con.IMAGE_ICON,
|
||||
48, 48, win32con.LR_LOADFROMFILE)
|
||||
icon_small = win32gui.LoadImage(
|
||||
None, filename, win32con.IMAGE_ICON,
|
||||
16, 16, win32con.LR_LOADFROMFILE)
|
||||
win32api.SendMessage(
|
||||
hwnd, win32con.WM_SETICON, win32con.ICON_SMALL, icon_small)
|
||||
win32api.SendMessage(
|
||||
hwnd, win32con.WM_SETICON, win32con.ICON_BIG, icon_big)
|
||||
return True
|
||||
|
||||
def _set_cursor_state(self, value):
|
||||
pygame.mouse.set_visible(value)
|
||||
|
||||
def screenshot(self, *largs, **kwargs):
|
||||
global glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE
|
||||
filename = super(WindowPygame, self).screenshot(*largs, **kwargs)
|
||||
if filename is None:
|
||||
return None
|
||||
if glReadPixels is None:
|
||||
from kivy.graphics.opengl import (glReadPixels, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE)
|
||||
width, height = self.system_size
|
||||
data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
|
||||
data = bytes(bytearray(data))
|
||||
surface = pygame.image.fromstring(data, (width, height), 'RGBA', True)
|
||||
pygame.image.save(surface, filename)
|
||||
Logger.debug('Window: Screenshot saved at <%s>' % filename)
|
||||
return filename
|
||||
|
||||
def flip(self):
|
||||
pygame.display.flip()
|
||||
super(WindowPygame, self).flip()
|
||||
|
||||
def mainloop(self):
|
||||
for event in pygame.event.get():
|
||||
|
||||
# kill application (SIG_TERM)
|
||||
if event.type == pygame.QUIT:
|
||||
if self.dispatch('on_request_close'):
|
||||
continue
|
||||
EventLoop.quit = True
|
||||
self.close()
|
||||
|
||||
# mouse move
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
x, y = event.pos
|
||||
self.mouse_pos = x, self.system_size[1] - y
|
||||
# don't dispatch motion if no button are pressed
|
||||
if event.buttons == (0, 0, 0):
|
||||
continue
|
||||
self._mouse_x = x
|
||||
self._mouse_y = y
|
||||
self._mouse_meta = self.modifiers
|
||||
self.dispatch('on_mouse_move', x, y, self.modifiers)
|
||||
|
||||
# mouse action
|
||||
elif event.type in (pygame.MOUSEBUTTONDOWN,
|
||||
pygame.MOUSEBUTTONUP):
|
||||
self._pygame_update_modifiers()
|
||||
x, y = event.pos
|
||||
btn = 'left'
|
||||
if event.button == 3:
|
||||
btn = 'right'
|
||||
elif event.button == 2:
|
||||
btn = 'middle'
|
||||
elif event.button == 4:
|
||||
btn = 'scrolldown'
|
||||
elif event.button == 5:
|
||||
btn = 'scrollup'
|
||||
elif event.button == 6:
|
||||
btn = 'scrollright'
|
||||
elif event.button == 7:
|
||||
btn = 'scrollleft'
|
||||
eventname = 'on_mouse_down'
|
||||
if event.type == pygame.MOUSEBUTTONUP:
|
||||
eventname = 'on_mouse_up'
|
||||
self._mouse_x = x
|
||||
self._mouse_y = y
|
||||
self._mouse_meta = self.modifiers
|
||||
self._mouse_btn = btn
|
||||
self._mouse_down = eventname == 'on_mouse_down'
|
||||
self.dispatch(eventname, x, y, btn, self.modifiers)
|
||||
|
||||
# joystick action
|
||||
elif event.type == pygame.JOYAXISMOTION:
|
||||
self.dispatch('on_joy_axis', event.joy, event.axis,
|
||||
event.value)
|
||||
|
||||
elif event.type == pygame.JOYHATMOTION:
|
||||
self.dispatch('on_joy_hat', event.joy, event.hat, event.value)
|
||||
|
||||
elif event.type == pygame.JOYBALLMOTION:
|
||||
self.dispatch('on_joy_ball', event.joy, event.ballid,
|
||||
event.rel[0], event.rel[1])
|
||||
|
||||
elif event.type == pygame.JOYBUTTONDOWN:
|
||||
self.dispatch('on_joy_button_down', event.joy, event.button)
|
||||
|
||||
elif event.type == pygame.JOYBUTTONUP:
|
||||
self.dispatch('on_joy_button_up', event.joy, event.button)
|
||||
|
||||
# keyboard action
|
||||
elif event.type in (pygame.KEYDOWN, pygame.KEYUP):
|
||||
self._pygame_update_modifiers(event.mod)
|
||||
# atm, don't handle keyup
|
||||
if event.type == pygame.KEYUP:
|
||||
self.dispatch('on_key_up', event.key,
|
||||
event.scancode)
|
||||
continue
|
||||
|
||||
# don't dispatch more key if down event is accepted
|
||||
if self.dispatch('on_key_down', event.key,
|
||||
event.scancode, event.unicode,
|
||||
self.modifiers):
|
||||
continue
|
||||
self.dispatch('on_keyboard', event.key,
|
||||
event.scancode, event.unicode,
|
||||
self.modifiers)
|
||||
|
||||
# video resize
|
||||
elif event.type == pygame.VIDEORESIZE:
|
||||
self._size = event.size
|
||||
self.update_viewport()
|
||||
|
||||
elif event.type == pygame.VIDEOEXPOSE:
|
||||
self.canvas.ask_update()
|
||||
|
||||
# ignored event
|
||||
elif event.type == pygame.ACTIVEEVENT:
|
||||
pass
|
||||
|
||||
# drop file (pygame patch needed)
|
||||
elif event.type == pygame.USEREVENT and \
|
||||
hasattr(pygame, 'USEREVENT_DROPFILE') and \
|
||||
event.code == pygame.USEREVENT_DROPFILE:
|
||||
drop_x, drop_y = pygame.mouse.get_pos()
|
||||
self.dispatch('on_drop_file', event.filename, drop_x, drop_y)
|
||||
|
||||
'''
|
||||
# unhandled event !
|
||||
else:
|
||||
Logger.debug('WinPygame: Unhandled event %s' % str(event))
|
||||
'''
|
||||
if not pygame.display.get_active():
|
||||
pygame.time.wait(100)
|
||||
|
||||
#
|
||||
# Pygame wrapper
|
||||
#
|
||||
def _pygame_set_mode(self, size=None):
|
||||
if size is None:
|
||||
size = self.size
|
||||
if self.fullscreen == 'auto':
|
||||
pygame.display.set_mode((0, 0), self.flags)
|
||||
else:
|
||||
pygame.display.set_mode(size, self.flags)
|
||||
|
||||
def _pygame_update_modifiers(self, mods=None):
|
||||
# Available mod, from dir(pygame)
|
||||
# 'KMOD_ALT', 'KMOD_CAPS', 'KMOD_CTRL', 'KMOD_LALT',
|
||||
# 'KMOD_LCTRL', 'KMOD_LMETA', 'KMOD_LSHIFT', 'KMOD_META',
|
||||
# 'KMOD_MODE', 'KMOD_NONE'
|
||||
if mods is None:
|
||||
mods = pygame.key.get_mods()
|
||||
self._modifiers = []
|
||||
if mods & (pygame.KMOD_SHIFT | pygame.KMOD_LSHIFT):
|
||||
self._modifiers.append('shift')
|
||||
if mods & (pygame.KMOD_ALT | pygame.KMOD_LALT):
|
||||
self._modifiers.append('alt')
|
||||
if mods & (pygame.KMOD_CTRL | pygame.KMOD_LCTRL):
|
||||
self._modifiers.append('ctrl')
|
||||
if mods & (pygame.KMOD_META | pygame.KMOD_LMETA):
|
||||
self._modifiers.append('meta')
|
||||
|
||||
def request_keyboard(
|
||||
self, callback, target, input_type='text', keyboard_suggestions=True
|
||||
):
|
||||
keyboard = super(WindowPygame, self).request_keyboard(
|
||||
callback, target, input_type, keyboard_suggestions)
|
||||
if android and not self.allow_vkeyboard:
|
||||
android.show_keyboard(target, input_type)
|
||||
return keyboard
|
||||
|
||||
def release_keyboard(self, *largs):
|
||||
super(WindowPygame, self).release_keyboard(*largs)
|
||||
if android:
|
||||
android.hide_keyboard()
|
||||
return True
|
||||
@@ -0,0 +1,999 @@
|
||||
# found a way to include it more easily.
|
||||
'''
|
||||
SDL2 Window
|
||||
===========
|
||||
|
||||
Windowing provider directly based on our own wrapped version of SDL.
|
||||
|
||||
TODO:
|
||||
- fix keys
|
||||
- support scrolling
|
||||
- clean code
|
||||
- manage correctly all sdl events
|
||||
|
||||
'''
|
||||
|
||||
__all__ = ('WindowSDL', )
|
||||
|
||||
from os.path import join
|
||||
import sys
|
||||
from typing import Optional
|
||||
from kivy import kivy_data_dir
|
||||
from kivy.logger import Logger
|
||||
from kivy.base import EventLoop
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.core.window import WindowBase
|
||||
try:
|
||||
from kivy.core.window._window_sdl2 import _WindowSDL2Storage
|
||||
except ImportError:
|
||||
from kivy.core import handle_win_lib_import_error
|
||||
handle_win_lib_import_error(
|
||||
'window', 'sdl2', 'kivy.core.window._window_sdl2')
|
||||
raise
|
||||
from kivy.input.provider import MotionEventProvider
|
||||
from kivy.input.motionevent import MotionEvent
|
||||
from kivy.resources import resource_find
|
||||
from kivy.utils import platform, deprecated
|
||||
from kivy.compat import unichr
|
||||
from collections import deque
|
||||
|
||||
|
||||
# SDL_keycode.h, https://wiki.libsdl.org/SDL_Keymod
|
||||
KMOD_NONE = 0x0000
|
||||
KMOD_LSHIFT = 0x0001
|
||||
KMOD_RSHIFT = 0x0002
|
||||
KMOD_LCTRL = 0x0040
|
||||
KMOD_RCTRL = 0x0080
|
||||
KMOD_LALT = 0x0100
|
||||
KMOD_RALT = 0x0200
|
||||
KMOD_LGUI = 0x0400
|
||||
KMOD_RGUI = 0x0800
|
||||
KMOD_NUM = 0x1000
|
||||
KMOD_CAPS = 0x2000
|
||||
KMOD_MODE = 0x4000
|
||||
|
||||
SDLK_SHIFTL = 1073742049
|
||||
SDLK_SHIFTR = 1073742053
|
||||
SDLK_LCTRL = 1073742048
|
||||
SDLK_RCTRL = 1073742052
|
||||
SDLK_LALT = 1073742050
|
||||
SDLK_RALT = 1073742054
|
||||
SDLK_LEFT = 1073741904
|
||||
SDLK_RIGHT = 1073741903
|
||||
SDLK_UP = 1073741906
|
||||
SDLK_DOWN = 1073741905
|
||||
SDLK_HOME = 1073741898
|
||||
SDLK_END = 1073741901
|
||||
SDLK_PAGEUP = 1073741899
|
||||
SDLK_PAGEDOWN = 1073741902
|
||||
SDLK_SUPER = 1073742051
|
||||
SDLK_CAPS = 1073741881
|
||||
SDLK_INSERT = 1073741897
|
||||
SDLK_KEYPADNUM = 1073741907
|
||||
SDLK_KP_DIVIDE = 1073741908
|
||||
SDLK_KP_MULTIPLY = 1073741909
|
||||
SDLK_KP_MINUS = 1073741910
|
||||
SDLK_KP_PLUS = 1073741911
|
||||
SDLK_KP_ENTER = 1073741912
|
||||
SDLK_KP_1 = 1073741913
|
||||
SDLK_KP_2 = 1073741914
|
||||
SDLK_KP_3 = 1073741915
|
||||
SDLK_KP_4 = 1073741916
|
||||
SDLK_KP_5 = 1073741917
|
||||
SDLK_KP_6 = 1073741918
|
||||
SDLK_KP_7 = 1073741919
|
||||
SDLK_KP_8 = 1073741920
|
||||
SDLK_KP_9 = 1073741921
|
||||
SDLK_KP_0 = 1073741922
|
||||
SDLK_KP_DOT = 1073741923
|
||||
SDLK_F1 = 1073741882
|
||||
SDLK_F2 = 1073741883
|
||||
SDLK_F3 = 1073741884
|
||||
SDLK_F4 = 1073741885
|
||||
SDLK_F5 = 1073741886
|
||||
SDLK_F6 = 1073741887
|
||||
SDLK_F7 = 1073741888
|
||||
SDLK_F8 = 1073741889
|
||||
SDLK_F9 = 1073741890
|
||||
SDLK_F10 = 1073741891
|
||||
SDLK_F11 = 1073741892
|
||||
SDLK_F12 = 1073741893
|
||||
SDLK_F13 = 1073741894
|
||||
SDLK_F14 = 1073741895
|
||||
SDLK_F15 = 1073741896
|
||||
|
||||
|
||||
class SDL2MotionEvent(MotionEvent):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('is_touch', True)
|
||||
kwargs.setdefault('type_id', 'touch')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.profile = ('pos', 'pressure')
|
||||
|
||||
def depack(self, args):
|
||||
self.sx, self.sy, self.pressure = args
|
||||
super().depack(args)
|
||||
|
||||
|
||||
class SDL2MotionEventProvider(MotionEventProvider):
|
||||
win = None
|
||||
q = deque()
|
||||
touchmap = {}
|
||||
|
||||
def update(self, dispatch_fn):
|
||||
touchmap = self.touchmap
|
||||
while True:
|
||||
try:
|
||||
value = self.q.pop()
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
action, fid, x, y, pressure = value
|
||||
y = 1 - y
|
||||
if fid not in touchmap:
|
||||
touchmap[fid] = me = SDL2MotionEvent(
|
||||
'sdl', fid, (x, y, pressure)
|
||||
)
|
||||
else:
|
||||
me = touchmap[fid]
|
||||
me.move((x, y, pressure))
|
||||
if action == 'fingerdown':
|
||||
dispatch_fn('begin', me)
|
||||
elif action == 'fingerup':
|
||||
me.update_time_end()
|
||||
dispatch_fn('end', me)
|
||||
del touchmap[fid]
|
||||
else:
|
||||
dispatch_fn('update', me)
|
||||
|
||||
|
||||
class WindowSDL(WindowBase):
|
||||
|
||||
_win_dpi_watch: Optional['_WindowsSysDPIWatch'] = None
|
||||
|
||||
_do_resize_ev = None
|
||||
|
||||
managed_textinput = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._pause_loop = False
|
||||
self._cursor_entered = False
|
||||
self._drop_pos = None
|
||||
self._win = _WindowSDL2Storage()
|
||||
super(WindowSDL, self).__init__()
|
||||
self.titlebar_widget = None
|
||||
self._mouse_x = self._mouse_y = -1
|
||||
self._meta_keys = (
|
||||
KMOD_LCTRL, KMOD_RCTRL, KMOD_RSHIFT,
|
||||
KMOD_LSHIFT, KMOD_RALT, KMOD_LALT, KMOD_LGUI,
|
||||
KMOD_RGUI, KMOD_NUM, KMOD_CAPS, KMOD_MODE)
|
||||
self.command_keys = {
|
||||
27: 'escape',
|
||||
9: 'tab',
|
||||
8: 'backspace',
|
||||
13: 'enter',
|
||||
127: 'del',
|
||||
271: 'enter',
|
||||
273: 'up',
|
||||
274: 'down',
|
||||
275: 'right',
|
||||
276: 'left',
|
||||
278: 'home',
|
||||
279: 'end',
|
||||
280: 'pgup',
|
||||
281: 'pgdown'}
|
||||
self._mouse_buttons_down = set()
|
||||
self.key_map = {SDLK_LEFT: 276, SDLK_RIGHT: 275, SDLK_UP: 273,
|
||||
SDLK_DOWN: 274, SDLK_HOME: 278, SDLK_END: 279,
|
||||
SDLK_PAGEDOWN: 281, SDLK_PAGEUP: 280, SDLK_SHIFTR: 303,
|
||||
SDLK_SHIFTL: 304, SDLK_SUPER: 309, SDLK_LCTRL: 305,
|
||||
SDLK_RCTRL: 306, SDLK_LALT: 308, SDLK_RALT: 307,
|
||||
SDLK_CAPS: 301, SDLK_INSERT: 277, SDLK_F1: 282,
|
||||
SDLK_F2: 283, SDLK_F3: 284, SDLK_F4: 285, SDLK_F5: 286,
|
||||
SDLK_F6: 287, SDLK_F7: 288, SDLK_F8: 289, SDLK_F9: 290,
|
||||
SDLK_F10: 291, SDLK_F11: 292, SDLK_F12: 293,
|
||||
SDLK_F13: 294, SDLK_F14: 295, SDLK_F15: 296,
|
||||
SDLK_KEYPADNUM: 300, SDLK_KP_DIVIDE: 267,
|
||||
SDLK_KP_MULTIPLY: 268, SDLK_KP_MINUS: 269,
|
||||
SDLK_KP_PLUS: 270, SDLK_KP_ENTER: 271,
|
||||
SDLK_KP_DOT: 266, SDLK_KP_0: 256, SDLK_KP_1: 257,
|
||||
SDLK_KP_2: 258, SDLK_KP_3: 259, SDLK_KP_4: 260,
|
||||
SDLK_KP_5: 261, SDLK_KP_6: 262, SDLK_KP_7: 263,
|
||||
SDLK_KP_8: 264, SDLK_KP_9: 265}
|
||||
if platform == 'ios':
|
||||
# XXX ios keyboard suck, when backspace is hit, the delete
|
||||
# keycode is sent. fix it.
|
||||
self.key_map[127] = 8
|
||||
elif platform == 'android':
|
||||
# map android back button to escape
|
||||
self.key_map[1073742094] = 27
|
||||
|
||||
self.bind(minimum_width=self._set_minimum_size,
|
||||
minimum_height=self._set_minimum_size)
|
||||
|
||||
self.bind(allow_screensaver=self._set_allow_screensaver)
|
||||
self.bind(always_on_top=self._set_always_on_top)
|
||||
|
||||
def get_window_info(self):
|
||||
return self._win.get_window_info()
|
||||
|
||||
def _set_minimum_size(self, *args):
|
||||
minimum_width = self.minimum_width
|
||||
minimum_height = self.minimum_height
|
||||
if minimum_width and minimum_height:
|
||||
self._win.set_minimum_size(minimum_width, minimum_height)
|
||||
elif minimum_width or minimum_height:
|
||||
Logger.warning(
|
||||
'Both Window.minimum_width and Window.minimum_height must be '
|
||||
'bigger than 0 for the size restriction to take effect.')
|
||||
|
||||
def _set_always_on_top(self, *args):
|
||||
self._win.set_always_on_top(self.always_on_top)
|
||||
|
||||
def _set_allow_screensaver(self, *args):
|
||||
self._win.set_allow_screensaver(self.allow_screensaver)
|
||||
|
||||
def _event_filter(self, action, *largs):
|
||||
from kivy.app import App
|
||||
if action == 'app_terminating':
|
||||
EventLoop.quit = True
|
||||
|
||||
elif action == 'app_lowmemory':
|
||||
self.dispatch('on_memorywarning')
|
||||
|
||||
elif action == 'app_willenterbackground':
|
||||
from kivy.base import stopTouchApp
|
||||
app = App.get_running_app()
|
||||
if not app:
|
||||
Logger.info('WindowSDL: No running App found, pause.')
|
||||
|
||||
elif not app.dispatch('on_pause'):
|
||||
if platform == 'android':
|
||||
Logger.info(
|
||||
'WindowSDL: App stopped, on_pause() returned False.')
|
||||
from android import mActivity
|
||||
mActivity.finishAndRemoveTask()
|
||||
else:
|
||||
Logger.info(
|
||||
'WindowSDL: App doesn\'t support pause mode, stop.')
|
||||
stopTouchApp()
|
||||
return 0
|
||||
|
||||
self._pause_loop = True
|
||||
|
||||
elif action == 'app_didenterforeground':
|
||||
# on iOS, the did enter foreground is launched at the start
|
||||
# of the application. in our case, we want it only when the app
|
||||
# is resumed
|
||||
if self._pause_loop:
|
||||
self._pause_loop = False
|
||||
app = App.get_running_app()
|
||||
if app:
|
||||
app.dispatch('on_resume')
|
||||
|
||||
elif action == 'windowresized':
|
||||
self._size = largs
|
||||
self._win.resize_window(*self._size)
|
||||
# Force kivy to render the frame now, so that the canvas is drawn.
|
||||
EventLoop.idle()
|
||||
|
||||
return 0
|
||||
|
||||
def create_window(self, *largs):
|
||||
if self._fake_fullscreen:
|
||||
if not self.borderless:
|
||||
self.fullscreen = self._fake_fullscreen = False
|
||||
elif not self.fullscreen or self.fullscreen == 'auto':
|
||||
self.custom_titlebar = \
|
||||
self.borderless = self._fake_fullscreen = False
|
||||
elif self.custom_titlebar:
|
||||
if platform == 'win':
|
||||
# use custom behavior
|
||||
# To handle aero snapping and rounded corners
|
||||
self.borderless = False
|
||||
if self.fullscreen == 'fake':
|
||||
self.borderless = self._fake_fullscreen = True
|
||||
Logger.warning("The 'fake' fullscreen option has been "
|
||||
"deprecated, use Window.borderless or the "
|
||||
"borderless Config option instead.")
|
||||
|
||||
if not self.initialized:
|
||||
if self.position == 'auto':
|
||||
pos = None, None
|
||||
elif self.position == 'custom':
|
||||
pos = self.left, self.top
|
||||
|
||||
# ensure we have an event filter
|
||||
self._win.set_event_filter(self._event_filter)
|
||||
|
||||
# setup window
|
||||
w, h = self.system_size
|
||||
resizable = Config.getboolean('graphics', 'resizable')
|
||||
state = (Config.get('graphics', 'window_state')
|
||||
if self._is_desktop else None)
|
||||
self.system_size = self._win.setup_window(
|
||||
pos[0], pos[1], w, h, self.borderless,
|
||||
self.fullscreen, resizable, state,
|
||||
self.get_gl_backend_name())
|
||||
|
||||
# We don't have a density or dpi yet set, so let's ask for an update
|
||||
self._update_density_and_dpi()
|
||||
|
||||
# never stay with a None pos, application using w.center
|
||||
# will be fired.
|
||||
self._pos = (0, 0)
|
||||
self._set_minimum_size()
|
||||
self._set_allow_screensaver()
|
||||
self._set_always_on_top()
|
||||
|
||||
if state == 'hidden':
|
||||
self._focus = False
|
||||
else:
|
||||
w, h = self.system_size
|
||||
self._win.resize_window(w, h)
|
||||
if platform == 'win':
|
||||
if self.custom_titlebar:
|
||||
# check dragging+resize or just dragging
|
||||
if Config.getboolean('graphics', 'resizable'):
|
||||
import win32con
|
||||
import ctypes
|
||||
self._win.set_border_state(False)
|
||||
# make windows dispatch,
|
||||
# WM_NCCALCSIZE explicitly
|
||||
ctypes.windll.user32.SetWindowPos(
|
||||
self._win.get_window_info().window,
|
||||
win32con.HWND_TOP,
|
||||
*self._win.get_window_pos(),
|
||||
*self.system_size,
|
||||
win32con.SWP_FRAMECHANGED
|
||||
)
|
||||
else:
|
||||
self._win.set_border_state(True)
|
||||
else:
|
||||
self._win.set_border_state(self.borderless)
|
||||
else:
|
||||
self._win.set_border_state(self.borderless
|
||||
or self.custom_titlebar)
|
||||
self._win.set_fullscreen_mode(self.fullscreen)
|
||||
|
||||
super(WindowSDL, self).create_window()
|
||||
# set mouse visibility
|
||||
self._set_cursor_state(self.show_cursor)
|
||||
|
||||
if self.initialized:
|
||||
return
|
||||
|
||||
# auto add input provider
|
||||
Logger.info('Window: auto add sdl2 input provider')
|
||||
SDL2MotionEventProvider.win = self
|
||||
EventLoop.add_input_provider(SDL2MotionEventProvider('sdl', ''))
|
||||
|
||||
# set window icon before calling set_mode
|
||||
try:
|
||||
filename_icon = self.icon or Config.get('kivy', 'window_icon')
|
||||
if filename_icon == '':
|
||||
logo_size = 32
|
||||
if platform == 'macosx':
|
||||
logo_size = 512
|
||||
elif platform == 'win':
|
||||
logo_size = 64
|
||||
filename_icon = 'kivy-icon-{}.png'.format(logo_size)
|
||||
filename_icon = resource_find(
|
||||
join(kivy_data_dir, 'logo', filename_icon))
|
||||
self.set_icon(filename_icon)
|
||||
except:
|
||||
Logger.exception('Window: cannot set icon')
|
||||
|
||||
if platform == 'win' and self._win_dpi_watch is None:
|
||||
self._win_dpi_watch = _WindowsSysDPIWatch(window=self)
|
||||
self._win_dpi_watch.start()
|
||||
|
||||
def _update_density_and_dpi(self):
|
||||
if platform == 'win':
|
||||
from ctypes import windll
|
||||
self._density = 1.
|
||||
try:
|
||||
hwnd = windll.user32.GetActiveWindow()
|
||||
self.dpi = float(windll.user32.GetDpiForWindow(hwnd))
|
||||
self._density = self.dpi / 96
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._density = self._win._get_gl_size()[0] / self._size[0]
|
||||
if self._is_desktop:
|
||||
self.dpi = self._density * 96.
|
||||
|
||||
def close(self):
|
||||
self._win.teardown_window()
|
||||
super(WindowSDL, self).close()
|
||||
if self._win_dpi_watch is not None:
|
||||
self._win_dpi_watch.stop()
|
||||
self._win_dpi_watch = None
|
||||
|
||||
self.initialized = False
|
||||
|
||||
def maximize(self):
|
||||
if self._is_desktop:
|
||||
self._win.maximize_window()
|
||||
else:
|
||||
Logger.warning('Window: maximize() is used only on desktop OSes.')
|
||||
|
||||
def minimize(self):
|
||||
if self._is_desktop:
|
||||
self._win.minimize_window()
|
||||
else:
|
||||
Logger.warning('Window: minimize() is used only on desktop OSes.')
|
||||
|
||||
def restore(self):
|
||||
if self._is_desktop:
|
||||
self._win.restore_window()
|
||||
else:
|
||||
Logger.warning('Window: restore() is used only on desktop OSes.')
|
||||
|
||||
def hide(self):
|
||||
if self._is_desktop:
|
||||
self._win.hide_window()
|
||||
else:
|
||||
Logger.warning('Window: hide() is used only on desktop OSes.')
|
||||
|
||||
def show(self):
|
||||
if self._is_desktop:
|
||||
self._win.show_window()
|
||||
else:
|
||||
Logger.warning('Window: show() is used only on desktop OSes.')
|
||||
|
||||
def raise_window(self):
|
||||
if self._is_desktop:
|
||||
self._win.raise_window()
|
||||
else:
|
||||
Logger.warning('Window: show() is used only on desktop OSes.')
|
||||
|
||||
def set_title(self, title):
|
||||
self._win.set_window_title(title)
|
||||
|
||||
def set_icon(self, filename):
|
||||
self._win.set_window_icon(str(filename))
|
||||
|
||||
def screenshot(self, *largs, **kwargs):
|
||||
filename = super(WindowSDL, self).screenshot(*largs, **kwargs)
|
||||
if filename is None:
|
||||
return
|
||||
|
||||
from kivy.graphics.opengl import glReadPixels, GL_RGB, GL_UNSIGNED_BYTE
|
||||
width, height = self.size
|
||||
data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
|
||||
self._win.save_bytes_in_png(filename, data, width, height)
|
||||
Logger.debug('Window: Screenshot saved at <%s>' % filename)
|
||||
return filename
|
||||
|
||||
def flip(self):
|
||||
self._win.flip()
|
||||
super(WindowSDL, self).flip()
|
||||
|
||||
def set_system_cursor(self, cursor_name):
|
||||
result = self._win.set_system_cursor(cursor_name)
|
||||
return result
|
||||
|
||||
def _get_window_pos(self):
|
||||
return self._win.get_window_pos()
|
||||
|
||||
def _set_window_pos(self, x, y):
|
||||
self._win.set_window_pos(x, y)
|
||||
|
||||
def _get_window_opacity(self):
|
||||
return self._win.get_window_opacity()
|
||||
|
||||
def _set_window_opacity(self, opacity):
|
||||
if self.opacity != opacity:
|
||||
return self._win.set_window_opacity(opacity)
|
||||
|
||||
# Transparent Window background
|
||||
def _is_shaped(self):
|
||||
return self._win.is_window_shaped()
|
||||
|
||||
def _set_shape(self, shape_image, mode='default',
|
||||
cutoff=False, color_key=None):
|
||||
modes = ('default', 'binalpha', 'reversebinalpha', 'colorkey')
|
||||
color_key = color_key or (0, 0, 0, 1)
|
||||
if mode not in modes:
|
||||
Logger.warning(
|
||||
'Window: shape mode can be only '
|
||||
'{}'.format(', '.join(modes))
|
||||
)
|
||||
return
|
||||
if not isinstance(color_key, (tuple, list)):
|
||||
return
|
||||
if len(color_key) not in (3, 4):
|
||||
return
|
||||
if len(color_key) == 3:
|
||||
color_key = (color_key[0], color_key[1], color_key[2], 1)
|
||||
Logger.warning(
|
||||
'Window: Shape color_key must be only tuple or list'
|
||||
)
|
||||
return
|
||||
color_key = (
|
||||
color_key[0] * 255,
|
||||
color_key[1] * 255,
|
||||
color_key[2] * 255,
|
||||
color_key[3] * 255
|
||||
)
|
||||
|
||||
assert cutoff in (1, 0)
|
||||
shape_image = shape_image or Config.get('kivy', 'window_shape')
|
||||
shape_image = resource_find(shape_image) or shape_image
|
||||
self._win.set_shape(shape_image, mode, cutoff, color_key)
|
||||
|
||||
def _get_shaped_mode(self):
|
||||
return self._win.get_shaped_mode()
|
||||
|
||||
def _set_shaped_mode(self, value):
|
||||
self._set_shape(
|
||||
shape_image=self.shape_image,
|
||||
mode=value, cutoff=self.shape_cutoff,
|
||||
color_key=self.shape_color_key
|
||||
)
|
||||
return self._win.get_shaped_mode()
|
||||
# twb end
|
||||
|
||||
def _set_cursor_state(self, value):
|
||||
self._win._set_cursor_state(value)
|
||||
|
||||
def _fix_mouse_pos(self, x, y):
|
||||
self.mouse_pos = (
|
||||
x * self._density,
|
||||
(self.system_size[1] - 1 - y) * self._density
|
||||
)
|
||||
return x, y
|
||||
|
||||
def mainloop(self):
|
||||
# for android/iOS, we don't want to have any event nor executing our
|
||||
# main loop while the pause is going on. This loop wait any event (not
|
||||
# handled by the event filter), and remove them from the queue.
|
||||
# Nothing happen during the pause on iOS, except gyroscope value sent
|
||||
# over joystick. So it's safe.
|
||||
while self._pause_loop:
|
||||
self._win.wait_event()
|
||||
if not self._pause_loop:
|
||||
break
|
||||
event = self._win.poll()
|
||||
if event is None:
|
||||
continue
|
||||
# A drop is send while the app is still in pause.loop
|
||||
# we need to dispatch it
|
||||
action, args = event[0], event[1:]
|
||||
if action.startswith('drop'):
|
||||
self._dispatch_drop_event(action, args)
|
||||
# app_terminating event might be received while the app is paused
|
||||
# in this case EventLoop.quit will be set at _event_filter
|
||||
elif EventLoop.quit:
|
||||
return
|
||||
|
||||
while True:
|
||||
event = self._win.poll()
|
||||
if event is False:
|
||||
break
|
||||
if event is None:
|
||||
continue
|
||||
|
||||
action, args = event[0], event[1:]
|
||||
if action == 'quit':
|
||||
if self.dispatch('on_request_close'):
|
||||
continue
|
||||
EventLoop.quit = True
|
||||
break
|
||||
|
||||
elif action in ('fingermotion', 'fingerdown', 'fingerup'):
|
||||
# for finger, pass the raw event to SDL motion event provider
|
||||
# XXX this is problematic. On OSX, it generates touches with 0,
|
||||
# 0 coordinates, at the same times as mouse. But it works.
|
||||
# We have a conflict of using either the mouse or the finger.
|
||||
# Right now, we have no mechanism that we could use to know
|
||||
# which is the preferred one for the application.
|
||||
if platform in ('ios', 'android'):
|
||||
SDL2MotionEventProvider.q.appendleft(event)
|
||||
pass
|
||||
|
||||
elif action == 'mousemotion':
|
||||
x, y = args
|
||||
x, y = self._fix_mouse_pos(x, y)
|
||||
self._mouse_x = x
|
||||
self._mouse_y = y
|
||||
if not self._cursor_entered:
|
||||
self._cursor_entered = True
|
||||
self.dispatch('on_cursor_enter')
|
||||
# don't dispatch motion if no button are pressed
|
||||
if len(self._mouse_buttons_down) == 0:
|
||||
continue
|
||||
self._mouse_meta = self.modifiers
|
||||
self.dispatch('on_mouse_move', x, y, self.modifiers)
|
||||
|
||||
elif action in ('mousebuttondown', 'mousebuttonup'):
|
||||
x, y, button = args
|
||||
x, y = self._fix_mouse_pos(x, y)
|
||||
self._mouse_x = x
|
||||
self._mouse_y = y
|
||||
if not self._cursor_entered:
|
||||
self._cursor_entered = True
|
||||
self.dispatch('on_cursor_enter')
|
||||
btn = 'left'
|
||||
if button == 3:
|
||||
btn = 'right'
|
||||
elif button == 2:
|
||||
btn = 'middle'
|
||||
elif button == 4:
|
||||
btn = "mouse4"
|
||||
elif button == 5:
|
||||
btn = "mouse5"
|
||||
eventname = 'on_mouse_down'
|
||||
self._mouse_buttons_down.add(button)
|
||||
if action == 'mousebuttonup':
|
||||
eventname = 'on_mouse_up'
|
||||
self._mouse_buttons_down.remove(button)
|
||||
self.dispatch(eventname, x, y, btn, self.modifiers)
|
||||
elif action.startswith('mousewheel'):
|
||||
x, y = self._win.get_relative_mouse_pos()
|
||||
if not self._collide_and_dispatch_cursor_enter(x, y):
|
||||
# Ignore if the cursor position is on the window title bar
|
||||
# or on its edges
|
||||
continue
|
||||
self._update_modifiers()
|
||||
x, y, button = args
|
||||
btn = 'scrolldown'
|
||||
if action.endswith('up'):
|
||||
btn = 'scrollup'
|
||||
elif action.endswith('right'):
|
||||
btn = 'scrollright'
|
||||
elif action.endswith('left'):
|
||||
btn = 'scrollleft'
|
||||
|
||||
self._mouse_meta = self.modifiers
|
||||
self._mouse_btn = btn
|
||||
# times = x if y == 0 else y
|
||||
# times = min(abs(times), 100)
|
||||
# for k in range(times):
|
||||
self._mouse_down = True
|
||||
self.dispatch('on_mouse_down',
|
||||
self._mouse_x, self._mouse_y, btn, self.modifiers)
|
||||
self._mouse_down = False
|
||||
self.dispatch('on_mouse_up',
|
||||
self._mouse_x, self._mouse_y, btn, self.modifiers)
|
||||
|
||||
elif action.startswith('drop'):
|
||||
self._dispatch_drop_event(action, args)
|
||||
# video resize
|
||||
elif action == 'windowresized':
|
||||
self._size = self._win.window_size
|
||||
# don't use trigger here, we want to delay the resize event
|
||||
ev = self._do_resize_ev
|
||||
if ev is None:
|
||||
ev = Clock.schedule_once(self._do_resize, .1)
|
||||
self._do_resize_ev = ev
|
||||
else:
|
||||
ev()
|
||||
elif action == 'windowdisplaychanged':
|
||||
Logger.info(f"WindowSDL: Window is now on display {args[0]}")
|
||||
|
||||
# The display has changed, so the density and dpi
|
||||
# may have changed too.
|
||||
self._update_density_and_dpi()
|
||||
|
||||
elif action == 'windowmoved':
|
||||
self.dispatch('on_move')
|
||||
|
||||
elif action == 'windowrestored':
|
||||
self.dispatch('on_restore')
|
||||
self.canvas.ask_update()
|
||||
|
||||
elif action == 'windowexposed':
|
||||
self.canvas.ask_update()
|
||||
|
||||
elif action == 'windowminimized':
|
||||
self.dispatch('on_minimize')
|
||||
if Config.getboolean('kivy', 'pause_on_minimize'):
|
||||
self.do_pause()
|
||||
|
||||
elif action == 'windowmaximized':
|
||||
self.dispatch('on_maximize')
|
||||
|
||||
elif action == 'windowhidden':
|
||||
self.dispatch('on_hide')
|
||||
|
||||
elif action == 'windowshown':
|
||||
self.dispatch('on_show')
|
||||
|
||||
elif action == 'windowfocusgained':
|
||||
self._focus = True
|
||||
|
||||
elif action == 'windowfocuslost':
|
||||
self._focus = False
|
||||
|
||||
elif action == 'windowenter':
|
||||
x, y = self._win.get_relative_mouse_pos()
|
||||
self._collide_and_dispatch_cursor_enter(x, y)
|
||||
|
||||
elif action == 'windowleave':
|
||||
self._cursor_entered = False
|
||||
self.dispatch('on_cursor_leave')
|
||||
|
||||
elif action == 'joyaxismotion':
|
||||
stickid, axisid, value = args
|
||||
self.dispatch('on_joy_axis', stickid, axisid, value)
|
||||
elif action == 'joyhatmotion':
|
||||
stickid, hatid, value = args
|
||||
self.dispatch('on_joy_hat', stickid, hatid, value)
|
||||
elif action == 'joyballmotion':
|
||||
stickid, ballid, xrel, yrel = args
|
||||
self.dispatch('on_joy_ball', stickid, ballid, xrel, yrel)
|
||||
elif action == 'joybuttondown':
|
||||
stickid, buttonid = args
|
||||
self.dispatch('on_joy_button_down', stickid, buttonid)
|
||||
elif action == 'joybuttonup':
|
||||
stickid, buttonid = args
|
||||
self.dispatch('on_joy_button_up', stickid, buttonid)
|
||||
|
||||
elif action in ('keydown', 'keyup'):
|
||||
mod, key, scancode, kstr = args
|
||||
|
||||
try:
|
||||
key = self.key_map[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if action == 'keydown':
|
||||
self._update_modifiers(mod, key)
|
||||
else:
|
||||
# ignore the key, it has been released
|
||||
self._update_modifiers(mod)
|
||||
|
||||
# if mod in self._meta_keys:
|
||||
if (key not in self._modifiers and
|
||||
key not in self.command_keys.keys()):
|
||||
try:
|
||||
kstr_chr = unichr(key)
|
||||
try:
|
||||
# On android, there is no 'encoding' attribute.
|
||||
# On other platforms, if stdout is redirected,
|
||||
# 'encoding' may be None
|
||||
encoding = getattr(sys.stdout, 'encoding',
|
||||
'utf8') or 'utf8'
|
||||
kstr_chr.encode(encoding)
|
||||
kstr = kstr_chr
|
||||
except UnicodeError:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
# if 'shift' in self._modifiers and key\
|
||||
# not in self.command_keys.keys():
|
||||
# return
|
||||
|
||||
if action == 'keyup':
|
||||
self.dispatch('on_key_up', key, scancode)
|
||||
continue
|
||||
|
||||
# don't dispatch more key if down event is accepted
|
||||
if self.dispatch('on_key_down', key,
|
||||
scancode, kstr,
|
||||
self.modifiers):
|
||||
continue
|
||||
self.dispatch('on_keyboard', key,
|
||||
scancode, kstr,
|
||||
self.modifiers)
|
||||
|
||||
elif action == 'textinput':
|
||||
text = args[0]
|
||||
self.dispatch('on_textinput', text)
|
||||
|
||||
elif action == 'textedit':
|
||||
text = args[0]
|
||||
self.dispatch('on_textedit', text)
|
||||
|
||||
# unhandled event !
|
||||
else:
|
||||
Logger.trace('WindowSDL: Unhandled event %s' % str(event))
|
||||
|
||||
def _dispatch_drop_event(self, action, args):
|
||||
x, y = (0, 0) if self._drop_pos is None else self._drop_pos
|
||||
if action == 'dropfile':
|
||||
self.dispatch('on_drop_file', args[0], x, y)
|
||||
elif action == 'droptext':
|
||||
self.dispatch('on_drop_text', args[0], x, y)
|
||||
elif action == 'dropbegin':
|
||||
self._drop_pos = x, y = self._win.get_relative_mouse_pos()
|
||||
self._collide_and_dispatch_cursor_enter(x, y)
|
||||
self.dispatch('on_drop_begin', x, y)
|
||||
elif action == 'dropend':
|
||||
self._drop_pos = None
|
||||
self.dispatch('on_drop_end', x, y)
|
||||
|
||||
def _collide_and_dispatch_cursor_enter(self, x, y):
|
||||
# x, y are relative to window left/top position
|
||||
w, h = self._win.window_size
|
||||
if 0 <= x < w and 0 <= y < h:
|
||||
self._mouse_x, self._mouse_y = self._fix_mouse_pos(x, y)
|
||||
if not self._cursor_entered:
|
||||
self._cursor_entered = True
|
||||
self.dispatch('on_cursor_enter')
|
||||
return True
|
||||
|
||||
def _do_resize(self, dt):
|
||||
Logger.debug('Window: Resize window to %s' % str(self.size))
|
||||
self._win.resize_window(*self._size)
|
||||
self.dispatch('on_pre_resize', *self.size)
|
||||
|
||||
def do_pause(self):
|
||||
# should go to app pause mode (desktop style)
|
||||
from kivy.app import App
|
||||
from kivy.base import stopTouchApp
|
||||
app = App.get_running_app()
|
||||
if not app:
|
||||
Logger.info('WindowSDL: No running App found, pause.')
|
||||
elif not app.dispatch('on_pause'):
|
||||
Logger.info('WindowSDL: App doesn\'t support pause mode, stop.')
|
||||
stopTouchApp()
|
||||
return
|
||||
|
||||
# XXX FIXME wait for sdl resume
|
||||
while True:
|
||||
event = self._win.poll()
|
||||
if event is False:
|
||||
continue
|
||||
if event is None:
|
||||
continue
|
||||
|
||||
action, args = event[0], event[1:]
|
||||
if action == 'quit':
|
||||
EventLoop.quit = True
|
||||
break
|
||||
elif action == 'app_willenterforeground':
|
||||
break
|
||||
elif action == 'windowrestored':
|
||||
break
|
||||
|
||||
if app:
|
||||
app.dispatch('on_resume')
|
||||
|
||||
def _update_modifiers(self, mods=None, key=None):
|
||||
if mods is None and key is None:
|
||||
return
|
||||
modifiers = set()
|
||||
|
||||
if mods is not None:
|
||||
if mods & (KMOD_RSHIFT | KMOD_LSHIFT):
|
||||
modifiers.add('shift')
|
||||
if mods & (KMOD_RALT | KMOD_LALT | KMOD_MODE):
|
||||
modifiers.add('alt')
|
||||
if mods & (KMOD_RCTRL | KMOD_LCTRL):
|
||||
modifiers.add('ctrl')
|
||||
if mods & (KMOD_RGUI | KMOD_LGUI):
|
||||
modifiers.add('meta')
|
||||
if mods & KMOD_NUM:
|
||||
modifiers.add('numlock')
|
||||
if mods & KMOD_CAPS:
|
||||
modifiers.add('capslock')
|
||||
|
||||
if key is not None:
|
||||
if key in (KMOD_RSHIFT, KMOD_LSHIFT):
|
||||
modifiers.add('shift')
|
||||
if key in (KMOD_RALT, KMOD_LALT, KMOD_MODE):
|
||||
modifiers.add('alt')
|
||||
if key in (KMOD_RCTRL, KMOD_LCTRL):
|
||||
modifiers.add('ctrl')
|
||||
if key in (KMOD_RGUI, KMOD_LGUI):
|
||||
modifiers.add('meta')
|
||||
if key == KMOD_NUM:
|
||||
modifiers.add('numlock')
|
||||
if key == KMOD_CAPS:
|
||||
modifiers.add('capslock')
|
||||
|
||||
self._modifiers = list(modifiers)
|
||||
return
|
||||
|
||||
def request_keyboard(
|
||||
self, callback, target, input_type='text', keyboard_suggestions=True
|
||||
):
|
||||
self._sdl_keyboard = super(WindowSDL, self).\
|
||||
request_keyboard(
|
||||
callback, target, input_type, keyboard_suggestions
|
||||
)
|
||||
self._win.show_keyboard(
|
||||
self._system_keyboard,
|
||||
self.softinput_mode,
|
||||
input_type,
|
||||
keyboard_suggestions,
|
||||
)
|
||||
Clock.schedule_interval(self._check_keyboard_shown, 1 / 5.)
|
||||
return self._sdl_keyboard
|
||||
|
||||
def release_keyboard(self, *largs):
|
||||
super(WindowSDL, self).release_keyboard(*largs)
|
||||
self._win.hide_keyboard()
|
||||
self._sdl_keyboard = None
|
||||
return True
|
||||
|
||||
def _check_keyboard_shown(self, dt):
|
||||
if self._sdl_keyboard is None:
|
||||
return False
|
||||
if not self._win.is_keyboard_shown():
|
||||
self._sdl_keyboard.release()
|
||||
|
||||
def map_key(self, original_key, new_key):
|
||||
self.key_map[original_key] = new_key
|
||||
|
||||
def unmap_key(self, key):
|
||||
if key in self.key_map:
|
||||
del self.key_map[key]
|
||||
|
||||
def grab_mouse(self):
|
||||
self._win.grab_mouse(True)
|
||||
|
||||
def ungrab_mouse(self):
|
||||
self._win.grab_mouse(False)
|
||||
|
||||
def set_custom_titlebar(self, titlebar_widget):
|
||||
if not self.custom_titlebar:
|
||||
Logger.warning("Window: Window.custom_titlebar not set to True… "
|
||||
"can't set custom titlebar")
|
||||
return
|
||||
self.titlebar_widget = titlebar_widget
|
||||
return self._win.set_custom_titlebar(self.titlebar_widget) == 0
|
||||
|
||||
|
||||
class _WindowsSysDPIWatch:
|
||||
|
||||
hwnd = None
|
||||
|
||||
new_windProc = None
|
||||
|
||||
old_windProc = None
|
||||
|
||||
window: WindowBase = None
|
||||
|
||||
def __init__(self, window: WindowBase):
|
||||
self.window = window
|
||||
|
||||
def start(self):
|
||||
from kivy.input.providers.wm_common import WNDPROC, \
|
||||
SetWindowLong_WndProc_wrapper
|
||||
from ctypes import windll
|
||||
|
||||
self.hwnd = windll.user32.GetActiveWindow()
|
||||
|
||||
# inject our own handler to handle messages before window manager
|
||||
self.new_windProc = WNDPROC(self._wnd_proc)
|
||||
self.old_windProc = SetWindowLong_WndProc_wrapper(
|
||||
self.hwnd, self.new_windProc)
|
||||
|
||||
def stop(self):
|
||||
from kivy.input.providers.wm_common import \
|
||||
SetWindowLong_WndProc_wrapper
|
||||
|
||||
if self.hwnd is None:
|
||||
return
|
||||
|
||||
self.new_windProc = SetWindowLong_WndProc_wrapper(
|
||||
self.hwnd, self.old_windProc)
|
||||
self.hwnd = self.new_windProc = self.old_windProc = None
|
||||
|
||||
def _wnd_proc(self, hwnd, msg, wParam, lParam):
|
||||
from kivy.input.providers.wm_common import WM_DPICHANGED, WM_NCCALCSIZE
|
||||
from ctypes import windll
|
||||
|
||||
if msg == WM_DPICHANGED:
|
||||
|
||||
def clock_callback(*args):
|
||||
if x_dpi != y_dpi:
|
||||
raise ValueError(
|
||||
'Can only handle DPI that are same for x and y')
|
||||
|
||||
self.window.dpi = x_dpi
|
||||
|
||||
x_dpi = wParam & 0xFFFF
|
||||
y_dpi = wParam >> 16
|
||||
Clock.schedule_once(clock_callback, -1)
|
||||
elif Config.getboolean('graphics', 'resizable') \
|
||||
and msg == WM_NCCALCSIZE and self.window.custom_titlebar:
|
||||
return 0
|
||||
return windll.user32.CallWindowProcW(
|
||||
self.old_windProc, hwnd, msg, wParam, lParam)
|
||||
@@ -0,0 +1,4 @@
|
||||
$HEADER$
|
||||
void main (void){
|
||||
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
|
||||
}
|
||||
|
After Width: | Height: | Size: 74 B |
@@ -0,0 +1,6 @@
|
||||
$HEADER$
|
||||
void main (void) {
|
||||
frag_color = color * vec4(1.0, 1.0, 1.0, opacity);
|
||||
tex_coord0 = vTexCoords0;
|
||||
gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
/* Outputs from the vertex shader */
|
||||
varying vec4 frag_color;
|
||||
varying vec2 tex_coord0;
|
||||
|
||||
/* uniform texture samplers */
|
||||
uniform sampler2D texture0;
|
||||
|
||||
uniform mat4 frag_modelview_mat;
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
/* Outputs to the fragment shader */
|
||||
varying vec4 frag_color;
|
||||
varying vec2 tex_coord0;
|
||||
|
||||
/* vertex attributes */
|
||||
attribute vec2 vPosition;
|
||||
attribute vec2 vTexCoords0;
|
||||
|
||||
/* uniform variables */
|
||||
uniform mat4 modelview_mat;
|
||||
uniform mat4 projection_mat;
|
||||
uniform vec4 color;
|
||||
uniform float opacity;
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1 @@
|
||||
{"defaulttheme-0.png": {"progressbar_background": [392, 227, 24, 24], "tab_btn_disabled": [332, 137, 32, 32], "tab_btn_pressed": [400, 137, 32, 32], "image-missing": [152, 171, 48, 48], "splitter_h": [174, 123, 32, 7], "splitter_down": [11, 10, 7, 32], "splitter_disabled_down": [2, 10, 7, 32], "vkeyboard_key_down": [2, 44, 32, 32], "vkeyboard_disabled_key_down": [434, 137, 32, 32], "selector_right": [438, 326, 64, 64], "player-background": [2, 287, 103, 103], "selector_middle": [372, 326, 64, 64], "spinner": [204, 82, 29, 37], "tab_btn_disabled_pressed": [366, 137, 32, 32], "switch-button_disabled": [375, 291, 43, 32], "textinput_disabled_active": [134, 221, 64, 64], "splitter_grip": [70, 50, 12, 26], "vkeyboard_key_normal": [36, 44, 32, 32], "button_disabled": [111, 82, 29, 37], "media-playback-stop": [302, 171, 48, 48], "splitter": [502, 137, 7, 32], "splitter_down_h": [140, 123, 32, 7], "sliderh_background_disabled": [115, 132, 41, 37], "modalview-background": [464, 456, 45, 54], "button": [80, 82, 29, 37], "splitter_disabled": [501, 87, 7, 32], "checkbox_radio_disabled_on": [467, 87, 32, 32], "slider_cursor": [352, 171, 48, 48], "vkeyboard_disabled_background": [266, 221, 64, 64], "checkbox_disabled_on": [331, 87, 32, 32], "sliderv_background_disabled": [41, 78, 37, 41], "button_disabled_pressed": [142, 82, 29, 37], "audio-volume-muted": [102, 171, 48, 48], "close": [487, 173, 20, 20], "action_group_disabled": [2, 121, 33, 48], "vkeyboard_background": [200, 221, 64, 64], "checkbox_off": [365, 87, 32, 32], "tab_disabled": [107, 291, 96, 32], "sliderh_background": [72, 132, 41, 37], "switch-button": [430, 253, 43, 32], "tree_closed": [418, 231, 20, 20], "bubble_btn_pressed": [454, 291, 32, 32], "selector_left": [306, 326, 64, 64], "filechooser_file": [174, 326, 64, 64], "checkbox_radio_disabled_off": [433, 87, 32, 32], "checkbox_radio_on": [230, 137, 32, 32], "checkbox_on": [399, 87, 32, 32], "button_pressed": [173, 82, 29, 37], "audio-volume-high": [464, 406, 48, 48], "audio-volume-low": [2, 171, 48, 48], "progressbar": [332, 227, 32, 24], "previous_normal": [488, 291, 19, 32], "separator": [504, 342, 5, 48], "filechooser_folder": [240, 326, 64, 64], "checkbox_radio_off": [196, 137, 32, 32], "textinput_active": [68, 221, 64, 64], "textinput": [2, 221, 64, 64], "player-play-overlay": [122, 395, 117, 115], "media-playback-pause": [202, 171, 48, 48], "sliderv_background": [2, 78, 37, 41], "ring": [354, 402, 108, 108], "bubble_arrow": [490, 241, 16, 10], "slider_cursor_disabled": [402, 171, 48, 48], "checkbox_disabled_off": [297, 87, 32, 32], "action_group_down": [37, 121, 33, 48], "spinner_disabled": [235, 82, 29, 37], "splitter_disabled_h": [106, 123, 32, 7], "bubble": [107, 325, 65, 65], "media-playback-start": [252, 171, 48, 48], "vkeyboard_disabled_key_normal": [468, 137, 32, 32], "overflow": [264, 137, 32, 32], "tree_opened": [440, 231, 20, 20], "action_item": [487, 195, 24, 24], "bubble_btn": [420, 291, 32, 32], "audio-volume-medium": [52, 171, 48, 48], "action_group": [452, 171, 33, 48], "spinner_pressed": [266, 82, 29, 37], "filechooser_selected": [2, 392, 118, 118], "tab": [332, 253, 96, 32], "action_bar": [158, 133, 36, 36], "action_view": [366, 227, 24, 24], "tab_btn": [298, 137, 32, 32], "switch-background": [205, 291, 83, 32], "splitter_disabled_down_h": [72, 123, 32, 7], "action_item_down": [475, 253, 32, 32], "switch-background_disabled": [290, 291, 83, 32], "textinput_disabled": [241, 399, 111, 111], "splitter_grip_h": [462, 239, 26, 12]}}
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 73 B |
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"title" : "Azerty",
|
||||
"description" : "A French keyboard without international keys",
|
||||
"cols" : 15,
|
||||
"rows": 5,
|
||||
"normal_1" : [
|
||||
["@", "@", "`", 1], ["&", "&", "1", 1], ["\u00e9", "\u00e9", "2", 1],
|
||||
["'", "'", "3", 1], ["\"", "\"", "4", 1], ["[", "[", "5", 1],
|
||||
["-", "-", "6", 1], ["\u00e8", "\u00e8", "7", 1], ["_", "_", "8", 1],
|
||||
["\u00e7", "\u00e7", "9", 1], ["\u00e0", "\u00e0", "0", 1], ["]", "]", "+", 1],
|
||||
["=", "=", "=", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"normal_2" : [
|
||||
["\u21B9", "\t", "tab", 1.5], ["a", "a", "a", 1], ["z", "z", "z", 1],
|
||||
["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1],
|
||||
["y", "y", "y", 1], ["u", "u", "u", 1], ["i", "i", "i", 1],
|
||||
["o", "o", "o", 1], ["p", "p", "p", 1], ["^", "^", "^", 1],
|
||||
["$", "$", "}", 1], ["\u23ce", null, "enter", 1.5]
|
||||
],
|
||||
"normal_3" : [
|
||||
["\u21ea", null, "capslock", 1.8], ["q", "q", "q", 1], ["s", "s", "s", 1],
|
||||
["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1],
|
||||
["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1],
|
||||
["l", "l", "l", 1], ["m", "m", "m", 1], ["\u00f9", "\u00f9", "%", 1],
|
||||
["*", "*", "*", 1], ["\u23ce", null, "enter", 1.2]
|
||||
],
|
||||
"normal_4" : [
|
||||
["\u21e7", null, "shift", 1.5], ["<", "<", null, 1], ["w", "w", null, 1],
|
||||
["x", "x", null, 1],
|
||||
["c", "c", null, 1], ["v", "v", null, 1], ["b", "b", null, 1],
|
||||
["n", "n", null, 1], [",", ",", null, 1], [";", ";", null, 1],
|
||||
[":", ":", null, 1], ["!", "!", null, 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"normal_5" : [
|
||||
[" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
"shift_1" : [
|
||||
["|", "|", "|", 1], ["1", "1", "1", 1], ["2", "2", "2", 1],
|
||||
["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1],
|
||||
["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1],
|
||||
["9", "9", "9", 1], ["0", "0", "0", 1], ["#", "#", "#", 1],
|
||||
["+", "+", "+", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"shift_2" : [
|
||||
["\u21B9", "\t", "tab", 1.5], ["A", "A", "a", 1], ["Z", "Z", null, 1],
|
||||
["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1],
|
||||
["Y", "Y", "y", 1], ["U", "U", "u", 1], ["I", "I", "i", 1],
|
||||
["O", "O", "o", 1], ["P", "P", "p", 1], ["[", "[", "[", 1],
|
||||
["]", "]", "]", 1], ["\u23ce", null, "enter", 1.5]
|
||||
],
|
||||
"shift_3" : [
|
||||
["\u21ea", null, "capslock", 1.8], ["Q", "Q", "q", 1], ["S", "S", "s", 1],
|
||||
["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1],
|
||||
["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1],
|
||||
["L", "L", "l", 1], ["M", "M", "m", 1], ["%", "%", "%", 1],
|
||||
["\u00b5", "\u00b5", "*", 1], ["\u23ce", null, "enter", 1.2]
|
||||
],
|
||||
"shift_4" : [
|
||||
["\u21e7", null, "shift", 1.5], [">", ">", ">", 1], ["W", "W", "w", 1],
|
||||
["X", "X", "x", 1], ["C", "C", "c", 1], ["V", "V", "v", 1],
|
||||
["B", "B", "b", 1], ["N", "N", "n", 1], ["?", "?", "?", 1],
|
||||
[".", ".", ".", 1], ["/", "/", "/", 1], ["\u00a7", "\u00a7", "!", 1],
|
||||
["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"shift_5" : [
|
||||
[" ", " ", "spacebar", 12], ["\u2b12", null, "layout", 1.5], ["\u2a2f", null, "escape", 1.5]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"title": "de",
|
||||
"description": "A true German keyboard",
|
||||
"cols": 15,
|
||||
"rows": 5,
|
||||
"normal_1": [
|
||||
["^", "^", "^", 1], ["1", "1", "1", 1], ["2", "2", "2", 1],
|
||||
["3", "3", "3", 1], ["4", "4", "4", 1], ["5", "5", "5", 1],
|
||||
["6", "6", "6", 1], ["7", "7", "7", 1], ["8", "8", "8", 1],
|
||||
["9", "9", "9", 1], ["0", "0", "0", 1], ["ß", "ß", "ß", 1],
|
||||
["´", "´", "´", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"normal_2" : [
|
||||
["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1],
|
||||
["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1],
|
||||
["z", "z", "z", 1], ["u", "u", "´", 1], ["i", "i", "i", 1],
|
||||
["o", "o", "o", 1], ["p", "p", "p", 1], ["ü", "ü", "ü", 1],
|
||||
["+", "+", "+", 1], ["\u23ce", null, "enter", 1.5]
|
||||
],
|
||||
"normal_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1],
|
||||
["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1],
|
||||
["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1],
|
||||
["l", "l", "l", 1], ["ö", "ö", "ö", 1], ["ä", "ä", "ä", 1],
|
||||
["#", "#", "#", 1], ["\u23ce", null, "enter", 1.2]
|
||||
],
|
||||
"normal_4": [
|
||||
["\u21e7", null, "shift", 1.5], ["<", "<", "<", 1], ["y", "y", "y", 1],
|
||||
["x", "x", "x", 1], ["c", "c", "c", 1], ["v", "v", "v", 1],
|
||||
["b", "b", "b", 1], ["n", "n", "n", 1], ["m", "m", "m", 1],
|
||||
[",", ",", ",", 1], [".", ".", ".", 1], ["-", "-", "-", 1],
|
||||
["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"normal_5": [
|
||||
["@€¿", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"shift_1": [
|
||||
["°", "°", "°", 1], ["!", "!", "!", 1], ["\"", "\"","\"", 1],
|
||||
["§", "§", "§", 1], ["$", "$", "$", 1], ["%", "%", "%", 1],
|
||||
["&", "&", "&", 1], ["/", "/", "/", 1], ["(", "(", "(", 1],
|
||||
[")", ")", ")", 1], ["=", "=", "=", 1], ["?", "?", "?", 1],
|
||||
["`", "`", "`", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"shift_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1],
|
||||
["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1],
|
||||
["Z", "Z", "z", 1], ["U", "U", "u", 1], ["I", "I", "i", 1],
|
||||
["O", "O", "o", 1], ["P", "P", "p", 1], ["Ü", "Ü", "Ü", 1],
|
||||
["*", "*", "*", 1], ["\u23ce", null, "enter", 1.5]
|
||||
],
|
||||
"shift_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1],
|
||||
["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1],
|
||||
["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1],
|
||||
["L", "L", "l", 1], ["Ö", "Ö", "Ö", 1], ["Ä", "Ä", "Ä", 1],
|
||||
["'", "'", "'", 1], ["\u23ce", null, "enter", 1.2]
|
||||
],
|
||||
"shift_4": [
|
||||
["\u21e7", null, "shift", 1.5], [">", ">", ">", 1], ["Y", "Y", "Y", 1],
|
||||
["X", "X", "X", 1], ["C", "C", "C", 1], ["V", "V", "V", 1],
|
||||
["B", "B", "B", 1], ["N", "N", "N", 1], ["M", "M", "M", 1],
|
||||
[";", ";", ";", 1], [":", ":", ":", 1], ["_", "_", "_", 1],
|
||||
["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"shift_5": [
|
||||
["@€¿", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"special_1": [
|
||||
["„", "„", "„", 1], ["¡", "¡", "¡", 1], ["“", "“", "“", 1],
|
||||
["¶", "¶", "¶", 1], ["¢", "¢", "¢", 1], ["[", "[", "[", 1],
|
||||
["]", "]", "]", 1], ["|", "|", "|", 1], ["{", "{", "{", 1],
|
||||
["}", "}", "}", 1], ["≠", "≠", "≠", 1], ["¿", "¿", "¿", 1],
|
||||
["'", "'", "'", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"special_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["@", "@", "@", 1], ["∑", "∑", "∑", 1],
|
||||
["€", "€", "€", 1], ["®", "®", "®", 1], ["†", "†", "†", 1],
|
||||
["Ω", "Ω", "Ω", 1], ["¨", "¨", "¨", 1], ["⁄", "⁄", "⁄", 1],
|
||||
["ø", "ø", "ø", 1], ["π", "π", "π", 1], ["•", "•", "•", 1],
|
||||
["±", "±", "±", 1], ["\u23ce", null, "enter", 1.5]
|
||||
],
|
||||
"special_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["æ", "æ", "æ", 1], ["‚", "‚", "‚", 1],
|
||||
["∂", "∂", "∂", 1], ["ƒ", "ƒ", "ƒ", 1], ["©", "©", "©", 1],
|
||||
["ª", "ª", "ª", 1], ["º", "º", "º", 1], ["∆", "∆", "∆", 1],
|
||||
["@", "@", "@", 1], ["œ", "œ", "œ", 1], ["æ", "æ", "æ", 1],
|
||||
["‘", "‘", "‘", 1], ["\u23ce", null, "enter", 1.2]
|
||||
],
|
||||
"special_4": [
|
||||
["\u21e7", null, "shift", 1.5], ["≤", "≤", "≤", 1], ["¥", "¥", "¥", 1],
|
||||
["≈", "≈", "≈", 1], ["ç", "ç", "ç", 1], ["√", "√", "√", 1],
|
||||
["∫", "∫", "∫", 1], ["~", "~", "~", 1], ["µ", "µ", "µ", 1],
|
||||
["∞", "∞", "∞", 1], ["…", "…", "…", 1], ["–", "–", "–", 1],
|
||||
["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"special_5": [
|
||||
["@€¿", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"title": "de_CH",
|
||||
"description": "A Swiss German keyboard, touch optimized (no shift+caps lock)",
|
||||
"cols": 15,
|
||||
"rows": 5,
|
||||
"normal_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"normal_2" : [
|
||||
["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1],
|
||||
["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1],
|
||||
["z", "z", "z", 1], ["u", "u", "u", 1], ["i", "i", "i", 1],
|
||||
["o", "o", "o", 1], ["p", "p", "p", 1], ["ü", "ü", "ü", 1],
|
||||
[":", ":", ":", 1], ["$", "$", "$", 1.5]
|
||||
],
|
||||
"normal_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1],
|
||||
["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1],
|
||||
["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1],
|
||||
["l", "l", "l", 1], ["ö", "ö", "ö", 1], ["ä", "ä", "ä", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"normal_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["y", "y", "y", 1], ["x", "x", "x", 1],
|
||||
["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1],
|
||||
["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1],
|
||||
[".", ".", ".", 1], ["-", "-", "-", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"normal_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"shift_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"shift_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1],
|
||||
["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1],
|
||||
["Z", "Z", "z", 1], ["U", "U", "u", 1], ["I", "I", "i", 1],
|
||||
["O", "O", "o", 1], ["P", "P", "p", 1], ["Ü", "Ü", "Ü", 1],
|
||||
[":", ":", ":", 1], ["/", "/", "/", 1.5]
|
||||
],
|
||||
"shift_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1],
|
||||
["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1],
|
||||
["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1],
|
||||
["L", "L", "l", 1], ["Ö", "Ö", "Ö", 1], ["Ä", "Ä", "Ä", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"shift_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["Y", "Y", "y", 1], ["X", "X", "x", 1],
|
||||
["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1],
|
||||
["N", "N", "n", 1], ["M", "M", "m", 1], [";", ";", ";", 1],
|
||||
[":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"shift_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"special_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"special_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["(", "(", "(", 1], [")", ")", ")", 1],
|
||||
["{", "{", "{", 1], ["}", "}", "}", 1], ["[", "[", "[", 1],
|
||||
["]", "]", "]", 1], ["€", "€", "€", 1], ["$", "$", "$", 1],
|
||||
["£", "£", "£", 1], ["¥", "¥", "¥", 1], ["è", "è", "è", 1],
|
||||
["•", "•", "•", 1], ["|", "|", "|", 1.5]
|
||||
],
|
||||
"special_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["“", "“", "“", 1], ["`", "`", "`", 1],
|
||||
["«", "«", "«", 1], ["»", "»", "»", 1], ["#", "#", "#", 1],
|
||||
["%", "%", "%", 1], ["^", "^", "^", 1], ["°", "°", "°", 1],
|
||||
["&", "&", "&", 1], ["é", "é", "é", 1], ["à", "à", "à", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"special_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["+", "+", "+", 1], ["=", "=", "=", 1],
|
||||
["<", "<", "<", 1], [">", ">", ">", 1], ["*", "*", "*", 1],
|
||||
["È", "È", "È", 1], ["É", "É", "É", 1], ["À", "À", "À", 1],
|
||||
[":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"special_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"title": "en_US",
|
||||
"description": "A US Keyboard, touch optimized (no shift+caps lock)",
|
||||
"cols": 15,
|
||||
"rows": 5,
|
||||
"normal_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"normal_2" : [
|
||||
["\u21B9", "\t", "tab", 1.5], ["q", "q", "q", 1], ["w", "w", "w", 1],
|
||||
["e", "e", "e", 1], ["r", "r", "r", 1], ["t", "t", "t", 1],
|
||||
["y", "y", "y", 1], ["u", "u", "u", 1], ["i", "i", "i", 1],
|
||||
["o", "o", "o", 1], ["p", "p", "p", 1], ["[", "[", "[", 1],
|
||||
["]", "]", "]", 1], ["\\", "\\", "\\", 1.5]
|
||||
],
|
||||
"normal_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["a", "a", "a", 1], ["s", "s", "s", 1],
|
||||
["d", "d", "d", 1], ["f", "f", "f", 1], ["g", "g", "g", 1],
|
||||
["h", "h", "h", 1], ["j", "j", "j", 1], ["k", "k", "k", 1],
|
||||
["l", "l", "l", 1], [";", ";", ";", 1], ["'", "'", "'", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"normal_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["z", "z", "z", 1], ["x", "x", "x", 1],
|
||||
["c", "c", "c", 1], ["v", "v", "v", 1], ["b", "b", "b", 1],
|
||||
["n", "n", "n", 1], ["m", "m", "m", 1], [",", ",", ",", 1],
|
||||
[".", ".", ".", 1], ["/", "/", "/", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"normal_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"shift_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"shift_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["Q", "Q", null, 1], ["W", "W", null, 1],
|
||||
["E", "E", "e", 1], ["R", "R", "r", 1], ["T", "T", "t", 1],
|
||||
["Y", "Y", "y", 1], ["U", "U", "u", 1], ["I", "I", "i", 1],
|
||||
["O", "O", "o", 1], ["P", "P", "p", 1], ["{", "{", "{", 1],
|
||||
["}", "}", "}", 1], ["|", "|", "|", 1.5]
|
||||
],
|
||||
"shift_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["A", "A", "a", 1], ["S", "S", "s", 1],
|
||||
["D", "D", "d", 1], ["F", "F", "f", 1], ["G", "G", "g", 1],
|
||||
["H", "H", "h", 1], ["J", "J", "j", 1], ["K", "K", "k", 1],
|
||||
["L", "L", "l", 1], [":", ":", ":", 1], ["\"", "\"", "\"", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"shift_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["Z", "Z", "z", 1], ["X", "X", "x", 1],
|
||||
["C", "C", "c", 1], ["V", "V", "v", 1], ["B", "B", "b", 1],
|
||||
["N", "N", "n", 1], ["M", "M", "m", 1], ["<", "<", "<", 1],
|
||||
[">", ">", ">", 1], ["?", "?", "?", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"shift_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
],
|
||||
|
||||
"special_1": [
|
||||
["1", "1", "1", 1], ["2", "2", "2", 1], ["3", "3", "3", 1],
|
||||
["4", "4", "4", 1], ["5", "5", "5", 1], ["6", "6", "6", 1],
|
||||
["7", "7", "7", 1], ["8", "8", "8", 1], ["9", "9", "9", 1],
|
||||
["0", "0", "0", 1], ["@", "@", "@", 1], ["?", "?", "?", 1],
|
||||
["!", "!", "!", 1], ["\u232b", null, "backspace", 2]
|
||||
],
|
||||
"special_2": [
|
||||
["\u21B9", "\t", "tab", 1.5], ["(", "(", "(", 1], [")", ")", ")", 1],
|
||||
["{", "{", "{", 1], ["}", "}", "}", 1], ["[", "[", "[", 1],
|
||||
["]", "]", "]", 1], ["€", "€", "€", 1], ["$", "$", "$", 1],
|
||||
["£", "£", "£", 1], ["¥", "¥", "¥", 1], ["˘", "˘", "˘", 1],
|
||||
["•", "•", "•", 1], ["|", "|", "|", 1.5]
|
||||
],
|
||||
"special_3": [
|
||||
["\u21ea", null, "capslock", 1.8], ["“", "“", "“", 1], ["`", "`", "`", 1],
|
||||
["«", "«", "«", 1], ["»", "»", "»", 1], ["#", "#", "#", 1],
|
||||
["%", "%", "%", 1], ["^", "^", "^", 1], ["°", "°", "°", 1],
|
||||
["&", "&", "&", 1], ["ÿ", "ÿ", "ÿ", 1], ["-", "-", "-", 1],
|
||||
["\u23ce", null, "enter", 2.2]
|
||||
],
|
||||
"special_4": [
|
||||
["\u21e7", null, "shift", 2.5], ["+", "+", "+", 1], ["=", "=", "=", 1],
|
||||
["<", "<", "<", 1], [">", ">", ">", 1], ["*", "*", "*", 1],
|
||||
["Ù", "Ù", "Ù", 1], ["~", "~", "~", 1], ["À", "À", "À", 1],
|
||||
[":", ":", ":", 1], ["_", "_", "_", 1], ["\u21e7", null, "shift", 2.5]
|
||||
],
|
||||
"special_5": [
|
||||
["#+=", null, "special", 2.5], [" ", " ", "spacebar", 11], ["\u2a2f", null, "escape", 1.5]
|
||||
]
|
||||
}
|
||||