summaryrefslogtreecommitdiff
path: root/src/python
diff options
context:
space:
mode:
authorDimitri John Ledkov <xnox@ubuntu.com>2014-06-24 20:05:13 +0100
committerDimitri John Ledkov <xnox@ubuntu.com>2014-06-24 20:05:13 +0100
commitdd22bd15f6ed3e5eb5c77ab427029be50fe20148 (patch)
treed9491ee40d80688b7f5b1f20504f022686827a57 /src/python
libavg (1.8.1-1) unstable; urgency=medium
* New upstream release (Closes: #739664) * Mark libdc1394-22-dev as linux-any build-dependency. * Add libvdpau-dev build-dependency. * Add libavresample-dev build-dependency. # imported from the archive
Diffstat (limited to 'src/python')
-rw-r--r--src/python/Makefile.am6
-rw-r--r--src/python/__init__.py29
-rw-r--r--src/python/app/Makefile.am3
-rw-r--r--src/python/app/__init__.py29
-rw-r--r--src/python/app/app.py385
-rw-r--r--src/python/app/debugpanel.py697
-rw-r--r--src/python/app/flashmessage.py98
-rw-r--r--src/python/app/keyboardmanager.py204
-rw-r--r--src/python/app/settings.py277
-rw-r--r--src/python/app/touchvisualization.py188
-rw-r--r--src/python/apphelpers.py235
-rw-r--r--src/python/appstarter.py396
-rw-r--r--src/python/avgapp.py142
-rw-r--r--src/python/camcalibrator.py469
-rw-r--r--src/python/coordcalibrator.py133
-rw-r--r--src/python/data/CamImgBorder.pngbin0 -> 3031 bytes
-rw-r--r--src/python/data/Feedback.pngbin0 -> 196 bytes
-rw-r--r--src/python/data/Makefile.am4
-rw-r--r--src/python/data/SimpleSkin.xml83
-rw-r--r--src/python/data/TouchFeedback.pngbin0 -> 4494 bytes
-rw-r--r--src/python/data/black.pngbin0 -> 142 bytes
-rw-r--r--src/python/data/border.pngbin0 -> 8074 bytes
-rw-r--r--src/python/data/button_bg_down.pngbin0 -> 4089 bytes
-rw-r--r--src/python/data/button_bg_up.pngbin0 -> 3995 bytes
-rw-r--r--src/python/data/checkbox_checked_disabled.pngbin0 -> 2908 bytes
-rw-r--r--src/python/data/checkbox_checked_down.pngbin0 -> 2902 bytes
-rw-r--r--src/python/data/checkbox_checked_up.pngbin0 -> 2902 bytes
-rw-r--r--src/python/data/checkbox_unchecked_disabled.pngbin0 -> 2856 bytes
-rw-r--r--src/python/data/checkbox_unchecked_down.pngbin0 -> 2849 bytes
-rw-r--r--src/python/data/checkbox_unchecked_up.pngbin0 -> 2849 bytes
-rw-r--r--src/python/data/crosshair.pngbin0 -> 189 bytes
-rw-r--r--src/python/data/mpeg1-48x48-sound.avibin0 -> 28534 bytes
-rw-r--r--src/python/data/mpeg1-48x48.movbin0 -> 9863 bytes
-rw-r--r--src/python/data/pause_button_down.pngbin0 -> 2826 bytes
-rw-r--r--src/python/data/pause_button_up.pngbin0 -> 2825 bytes
-rw-r--r--src/python/data/play_button_down.pngbin0 -> 2927 bytes
-rw-r--r--src/python/data/play_button_up.pngbin0 -> 2928 bytes
-rw-r--r--src/python/data/rgb24alpha-64x64.pngbin0 -> 3392 bytes
-rw-r--r--src/python/data/scrollarea_border.pngbin0 -> 3262 bytes
-rw-r--r--src/python/data/scrollbar_horiz_thumb_down.pngbin0 -> 2990 bytes
-rw-r--r--src/python/data/scrollbar_horiz_thumb_up.pngbin0 -> 2977 bytes
-rw-r--r--src/python/data/scrollbar_horiz_track.pngbin0 -> 2874 bytes
-rw-r--r--src/python/data/scrollbar_horiz_track_disabled.pngbin0 -> 2873 bytes
-rw-r--r--src/python/data/scrollbar_vert_thumb_down.pngbin0 -> 3008 bytes
-rw-r--r--src/python/data/scrollbar_vert_thumb_up.pngbin0 -> 2998 bytes
-rw-r--r--src/python/data/scrollbar_vert_track.pngbin0 -> 2903 bytes
-rw-r--r--src/python/data/scrollbar_vert_track_disabled.pngbin0 -> 2902 bytes
-rw-r--r--src/python/data/skin.xsd137
-rw-r--r--src/python/data/slider_horiz_track.pngbin0 -> 2858 bytes
-rw-r--r--src/python/data/slider_horiz_track_disabled.pngbin0 -> 2859 bytes
-rw-r--r--src/python/data/slider_thumb_down.pngbin0 -> 3200 bytes
-rw-r--r--src/python/data/slider_thumb_up.pngbin0 -> 3225 bytes
-rw-r--r--src/python/data/slider_vert_track.pngbin0 -> 2867 bytes
-rw-r--r--src/python/data/slider_vert_track_disabled.pngbin0 -> 2866 bytes
-rw-r--r--src/python/enumcompat.py37
-rw-r--r--src/python/filter.py95
-rw-r--r--src/python/geom.py202
-rw-r--r--src/python/gesture.py1000
-rw-r--r--src/python/graph.py229
-rw-r--r--src/python/mathutil.py168
-rw-r--r--src/python/methodref.py69
-rw-r--r--src/python/mtemu.py166
-rw-r--r--src/python/parsecamargs.py49
-rw-r--r--src/python/persist.py157
-rw-r--r--src/python/statemachine.py149
-rw-r--r--src/python/textarea.py833
-rw-r--r--src/python/utils.py63
-rw-r--r--src/python/widget/Makefile.am3
-rw-r--r--src/python/widget/__init__.py7
-rw-r--r--src/python/widget/base.py267
-rw-r--r--src/python/widget/button.py443
-rw-r--r--src/python/widget/keyboard.py302
-rw-r--r--src/python/widget/mediacontrol.py161
-rw-r--r--src/python/widget/scrollarea.py240
-rw-r--r--src/python/widget/skin.py161
-rw-r--r--src/python/widget/slider.py371
76 files changed, 8687 insertions, 0 deletions
diff --git a/src/python/Makefile.am b/src/python/Makefile.am
new file mode 100644
index 0000000..0ec168f
--- /dev/null
+++ b/src/python/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = widget data app
+pkgpyexec_PYTHON = enumcompat.py camcalibrator.py textarea.py \
+ mathutil.py avgapp.py appstarter.py utils.py filter.py \
+ mtemu.py geom.py parsecamargs.py apphelpers.py methodref.py \
+ statemachine.py coordcalibrator.py graph.py __init__.py gesture.py \
+ persist.py
diff --git a/src/python/__init__.py b/src/python/__init__.py
new file mode 100644
index 0000000..5d0d2cf
--- /dev/null
+++ b/src/python/__init__.py
@@ -0,0 +1,29 @@
+'''
+libavg is a high-level development platform for media-centric applications.
+https://www.libavg.de
+'''
+
+# Work around libstdc++ Mesa bug
+# (https://bugs.launchpad.net/ubuntu/+source/mesa/+bug/259219)
+from platform import system
+if system() == 'Linux':
+ from ctypes import cdll
+ cdll.LoadLibrary("libpixman-1.so.0")
+ cdll.LoadLibrary("libstdc++.so.6")
+del system
+
+from avg import *
+player = avg.Player.get()
+
+from enumcompat import *
+
+import textarea
+import statemachine
+from avgapp import AVGApp
+from appstarter import AVGAppStarter, AVGMTAppStarter, AppStarter
+import utils, methodref
+import gesture
+import filter
+import persist
+import app
+
diff --git a/src/python/app/Makefile.am b/src/python/app/Makefile.am
new file mode 100644
index 0000000..8c78161
--- /dev/null
+++ b/src/python/app/Makefile.am
@@ -0,0 +1,3 @@
+pkgwidgetdir = $(pkgpyexecdir)/app
+pkgwidget_PYTHON = __init__.py app.py settings.py flashmessage.py keyboardmanager.py \
+ debugpanel.py touchvisualization.py
diff --git a/src/python/app/__init__.py b/src/python/app/__init__.py
new file mode 100644
index 0000000..32d41e0
--- /dev/null
+++ b/src/python/app/__init__.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+from app import App
+from app import MainDiv
+
+instance = None
+
diff --git a/src/python/app/app.py b/src/python/app/app.py
new file mode 100644
index 0000000..8c7ddda
--- /dev/null
+++ b/src/python/app/app.py
@@ -0,0 +1,385 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+
+import os
+import math
+import time
+
+import libavg
+from libavg import avg, Point2D, mtemu
+
+import settings
+from settings import Option
+import keyboardmanager
+import debugpanel
+import flashmessage
+
+
+class MainDiv(libavg.avg.DivNode):
+ VERSION = 'undef'
+
+ def __init__(self, **kargs):
+ assert not 'parent' in kargs
+ super(MainDiv, self).__init__(**kargs)
+ self.registerInstance(self, None)
+
+ def onArgvParserCreated(self, parser):
+ pass
+
+ def onArgvParsed(self, options, args, parser):
+ pass
+
+ def onStartup(self):
+ pass
+
+ def onInit(self):
+ pass
+
+ def onExit(self):
+ pass
+
+ def onFrame(self):
+ pass
+
+
+class App(object):
+ def __init__(self):
+ self._setupInstance()
+
+ self._mainDiv = None
+ self._appParent = None
+ self._debugPanel = None
+ self._overlayPanel = None
+ self._resolution = None
+ self._windowSize = None
+ self._mtEmu = None
+
+ self.__lastFrameTimestamp = 0
+
+ self._setupSettings()
+
+ def run(self, mainDiv, **kargs):
+ assert isinstance(mainDiv, MainDiv)
+ self._mainDiv = mainDiv
+
+ self.mainDiv.settings = self._settings
+ self._applySettingsExtenders(kargs)
+ self._setupLogging()
+
+ mainDiv.onStartup()
+
+ self._setupResolution()
+ self._setupRootNode()
+ self._setupMouse()
+ pos, size, angle = self._getAppParentGeometry()
+ self._setupAppParent(pos, size, angle)
+ self._setupMainDiv()
+ self._setupTopPanel()
+
+ self._setupDebugPanel()
+ self._setupKeyboardManager()
+ self._setupDebuggingWidgets()
+ self._applyResolution()
+ self._setupOnInit()
+
+ self.onBeforeLaunch()
+
+ self.__lastFrameTimestamp = time.time()
+
+ try:
+ self._runLoop()
+ except Exception, e:
+ self._teardownKeyboardManager()
+ raise
+
+ mainDiv.onExit()
+
+ self._teardownKeyboardManager()
+
+ return 0
+
+ @property
+ def mainDiv(self):
+ return self._mainDiv
+
+ @property
+ def debugPanel(self):
+ return self._debugPanel
+
+ @property
+ def overlayPanel(self):
+ return self._overlayPanel
+
+ @property
+ def settings(self):
+ return self._settings
+
+ def onBeforeLaunch(self):
+ pass
+
+ def takeScreenshot(self, targetFolder='.'):
+ screenBmp = libavg.player.screenshot()
+
+ filenameTemplate = os.path.join(targetFolder, '%s-%03d.png')
+
+ i = 1
+ while i < 1000:
+ filename = filenameTemplate % (self.__class__.__name__, i)
+ if os.path.exists(filename):
+ i += 1
+ else:
+ break
+
+ if i == 1000:
+ flashmessage.FlashMessage('Maximum number of screenshots reached',
+ parent=self._appParent, isError=True)
+ else:
+ screenBmp.save(filename)
+ flashmessage.FlashMessage('Screenshot saved as %s' % filename,
+ parent=self._appParent)
+
+ def dumpTextObjectCount(self):
+ objects = libavg.player.getTestHelper().getObjectCount()
+ savedSeverity = libavg.logger.getCategories()[libavg.logger.Category.APP]
+ libavg.logger.configureCategory(libavg.logger.Category.APP,
+ libavg.logger.Severity.INFO)
+ libavg.logger.info('Dumping objects count')
+ for key, value in objects.iteritems():
+ libavg.logger.info(' %-25s: %s' % (key, value))
+
+ libavg.logger.configureCategory(libavg.logger.Category.APP, savedSeverity)
+
+ def _setupInstance(self):
+ import libavg.app
+
+ if libavg.app.instance is not None:
+ raise RuntimeError('%s has been already instantiated' %
+ self.__class__.__name__)
+
+ libavg.app.instance = self
+
+ def _setupSettings(self):
+ self._settings = settings.Settings()
+ self._settings.addOption(Option('app_resolution', '640x480'))
+ self._settings.addOption(Option('app_window_size', ''))
+ self._settings.addOption(Option('app_fullscreen', 'false'))
+ self._settings.addOption(Option('app_show_cursor', 'true'))
+ self._settings.addOption(Option('app_rotation', 'normal'))
+ self._settings.addOption(Option('app_panel_fontsize', '10'))
+ self._settings.addOption(Option('app_mouse_enabled', 'true'))
+ self._settings.addOption(Option('multitouch_enabled', 'false'))
+ self._settings.addOption(Option('multitouch_driver', ''))
+ self._settings.addOption(Option('multitouch_tuio_port', ''))
+ self._settings.addOption(Option('multitouch_mtdev_device', ''))
+ self._settings.addOption(Option('log_avg_categories', ''))
+
+ def _applySettingsExtenders(self, kargs):
+ self.settings.applyExtender(settings.KargsExtender(kargs))
+ argvExtender = settings.ArgvExtender(self.mainDiv.VERSION)
+ self.mainDiv.onArgvParserCreated(argvExtender.parser)
+ self.settings.applyExtender(argvExtender)
+ self.mainDiv.onArgvParsed(argvExtender.parsedArgs[0], argvExtender.parsedArgs[1],
+ argvExtender.parser)
+
+ def _setupLogging(self):
+ catMap = self.settings.get('log_avg_categories').strip()
+ if catMap:
+ for catPair in catMap.split(' '):
+ cat, strLevel = catPair.split(':')
+ level = getattr(avg.logger.Severity, strLevel)
+
+ libavg.avg.logger.configureCategory(cat, level)
+
+ def _setupRootNode(self):
+ libavg.player.loadString('''<?xml version="1.0"?>
+ <!DOCTYPE avg SYSTEM "../../libavg/doc/avg.dtd">
+ <avg width="%s" height="%s">
+ </avg>''' % tuple(self._resolution))
+
+ def _setupMouse(self):
+ libavg.player.enableMouse(self.settings.getBoolean('app_mouse_enabled'))
+
+ def _setupMultitouch(self):
+ if self.settings.getBoolean('multitouch_enabled'):
+ driver = self.settings.get('multitouch_driver').upper()
+ if driver:
+ os.putenv('AVG_MULTITOUCH_DRIVER', driver)
+
+ tuio_port = self.settings.get('multitouch_tuio_port').upper()
+ if tuio_port:
+ os.putenv('AVG_TUIO_PORT', tuio_port)
+
+ mtdev_device = self.settings.get('multitouch_mtdev_device').upper()
+ if mtdev_device:
+ os.putenv('AVG_LINUX_MULTITOUCH_DEVICE', mtdev_device)
+
+ libavg.player.enableMultitouch()
+
+ def _getAppParentGeometry(self):
+ rotation = self.settings.get('app_rotation').lower()
+ size = self._resolution
+ pos = (0, 0)
+ angle = 0
+
+ if rotation == 'left':
+ angle = -math.pi / 2
+ size = (self._resolution.y, self._resolution.x)
+ pos = ((self._resolution.x - self._resolution.y) / 2,
+ (self._resolution.y - self._resolution.x) / 2)
+ elif rotation == 'right':
+ angle = math.pi / 2
+ size = (self._resolution.y, self._resolution.x)
+ pos = ((self._resolution.x - self._resolution.y) / 2,
+ (self._resolution.y - self._resolution.x) / 2)
+ elif rotation == 'inverted':
+ angle = math.pi
+ elif rotation != 'normal':
+ raise TypeError('Invalid rotation %s' % rotation)
+
+ return (pos, size, angle)
+
+ def _setupAppParent(self, pos, size, angle):
+ self._appParent = libavg.avg.DivNode(parent=libavg.player.getRootNode(),
+ pos=pos, size=size, angle=angle)
+
+ def _setupMainDiv(self):
+ self._appParent.appendChild(self.mainDiv)
+ self.mainDiv.size = self._appParent.size
+
+ def _setupTopPanel(self):
+ self._overlayPanel = libavg.avg.DivNode(parent=self._appParent, id='overlayPanel')
+
+ def _setupDebugPanel(self):
+ self._debugPanel = debugpanel.DebugPanel(parent=self._appParent,
+ size=self._appParent.size, id='debugPanel',
+ fontsize=self.settings.getFloat('app_panel_fontsize'))
+
+ def _setupDebuggingWidgets(self):
+ pass
+
+ def _setupResolution(self):
+ rotation = self.settings.get('app_rotation').lower()
+ resolutionStr = self.settings.get('app_resolution').lower()
+ if resolutionStr != '':
+ resolution = self.settings.getPoint2D('app_resolution')
+ else:
+ resolution = libavg.player.getScreenResolution()
+
+ windowSizeStr = self.settings.get('app_window_size')
+ if windowSizeStr != '':
+ windowSize = self.settings.getPoint2D('app_window_size')
+ else:
+ windowSize = resolution
+
+ if rotation in ('left', 'right'):
+ resolution = Point2D(resolution.y, resolution.x)
+ windowSize = Point2D(windowSize.y, windowSize.x)
+
+ self._resolution = resolution
+ self._windowSize = windowSize
+
+ def _applyResolution(self):
+ fullscreen = self.settings.getBoolean('app_fullscreen')
+
+ if fullscreen:
+ resolution = self._resolution
+ else:
+ resolution = self._windowSize
+
+ libavg.player.setResolution(
+ fullscreen,
+ int(resolution.x), int(resolution.y),
+ 0 # color depth
+ )
+
+ libavg.player.showCursor(self.settings.getBoolean('app_show_cursor'))
+
+ def _setupKeyboardManager(self):
+ keyboardmanager.init()
+ keyboardmanager.bindKeyDown(
+ keystring='d',
+ handler=self._debugPanel.toggleVisibility,
+ help='Show/hide the debug panel',
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ keyboardmanager.bindKeyDown(
+ keystring='h',
+ handler=lambda: libavg.player.showCursor(
+ not libavg.player.isCursorShown()),
+ help='Show/hide cursor',
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ keyboardmanager.bindKeyDown(
+ keystring='p',
+ handler=self.takeScreenshot,
+ help='Take screenshot',
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ keyboardmanager.bindKeyDown(
+ keystring='b',
+ handler=self.dumpTextObjectCount,
+ help='Dump objects count to the console',
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ keyboardmanager.bindKeyDown(
+ keystring='e',
+ handler=self._toggleMtEmulation,
+ help='Toggle multitouch emulation',
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ self.debugPanel.setupKeys()
+
+ def _toggleMtEmulation(self):
+ if self._mtEmu is None:
+ self._mtEmu = mtemu.MTemu()
+ keyboardmanager.bindKeyDown('shift', self._mtEmu.enableDualTouch,
+ 'Enable pinch gesture emulation')
+ keyboardmanager.bindKeyUp('shift', self._mtEmu.disableDualTouch,
+ 'Disable pinch gesture emulation')
+
+ keyboardmanager.bindKeyDown('t', self._mtEmu.toggleSource,
+ 'Toggle source between TOUCH and TRACK', libavg.avg.KEYMOD_CTRL)
+ else:
+ self._mtEmu.deinit()
+ keyboardmanager.unbindKeyDown('t', libavg.avg.KEYMOD_CTRL)
+ keyboardmanager.unbindKeyDown('shift')
+ keyboardmanager.unbindKeyUp('shift')
+
+ del self._mtEmu
+ self._mtEmu = None
+
+ def _teardownKeyboardManager(self):
+ keyboardmanager.unbindAll()
+
+ def _setupOnInit(self):
+ libavg.player.setTimeout(0, self._onInitInternal)
+
+ def _runLoop(self):
+ libavg.player.play()
+
+ def _onInitInternal(self):
+ self._setupMultitouch()
+ self.mainDiv.onInit()
+ libavg.player.subscribe(libavg.player.ON_FRAME, self.mainDiv.onFrame)
diff --git a/src/python/app/debugpanel.py b/src/python/app/debugpanel.py
new file mode 100644
index 0000000..c786785
--- /dev/null
+++ b/src/python/app/debugpanel.py
@@ -0,0 +1,697 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original authors of this file are
+# OXullo Interecans <x at brainrapers dot org>
+# Richard Klemm <richy at coding-reality.de>
+
+from collections import defaultdict
+from collections import deque
+import math
+
+import libavg
+from libavg import avg
+from touchvisualization import DebugTouchVisualization
+from touchvisualization import TouchVisualizationOverlay as TouchVisOverlay
+
+
+import keyboardmanager as kbmgr
+
+g_fontsize = 10
+
+PANGO_ENTITIES_MAP = {
+ "&": "&amp;",
+ '"': "&quot;",
+ "'": "&apos;",
+ ">": "&gt;",
+ "<": "&lt;",
+}
+
+def subscribe(publisher, msgID, callable_):
+ publisher.subscribe(msgID, callable_)
+ return lambda: publisher.unsubscribe(msgID, callable_)
+
+
+class DebugWidgetFrame(avg.DivNode):
+
+ BORDER = 7
+ FRAME_HEIGHT_CHANGED = avg.Publisher.genMessageID()
+
+ def __init__(self, size, widgetCls, *args, **kwargs):
+ super(DebugWidgetFrame, self).__init__(size=size, *args, **kwargs)
+ self.registerInstance(self, None)
+ self.setup(widgetCls)
+ self.subscribe(self.SIZE_CHANGED, self._onSizeChanged)
+ self.size = size
+ self._onSizeChanged(size)
+
+ def setup(self, widgetCls):
+ self.__background = avg.RectNode(parent=self, opacity=0.8,
+ fillcolor='000000', fillopacity=0.8)
+ self.__widget = widgetCls(parent=self,
+ size=(max(0, self.width - self.BORDER * 2), 0),
+ pos=(self.BORDER, self.BORDER))
+ self.__selectHighlight = avg.RectNode(parent=self, color="35C0CD",
+ strokewidth=self.BORDER, opacity=0.8,
+ pos=(self.BORDER / 2, self.BORDER / 2), active=False, sensitive=False)
+ self.__boundary = avg.RectNode(parent=self, sensitive=False)
+
+ self.publish(DebugWidgetFrame.FRAME_HEIGHT_CHANGED)
+
+ self.__widget.subscribe(self.__widget.WIDGET_HEIGHT_CHANGED,
+ self.adjustWidgetHeight)
+ self.__widget.update()
+
+ def _onSizeChanged(self, size):
+ self.__boundary.size = size
+ self.__background.size = size
+ childSize = (max(0, size[0] - self.BORDER * 2), max(0, size[1] - self.BORDER * 2))
+ self.__selectHighlight.size = (max(0, size[0] - self.BORDER),
+ max(0, size[1] - self.BORDER))
+ self.__widget.size = childSize
+ self.__widget.syncSize(childSize)
+
+ def adjustWidgetHeight(self, height):
+ self.size = (max(0, self.width), height + 2 * self.BORDER)
+ self.notifySubscribers(DebugWidgetFrame.FRAME_HEIGHT_CHANGED, [])
+
+ def toggleSelect(self, event=None):
+ self.__selectHighlight.active = not(self.__selectHighlight.active)
+
+ def isSelected(self):
+ return self.__selectHighlight.active
+
+ def select(self):
+ self.__selectHighlight.active = True
+
+ def unselect(self):
+ self.__selectHighlight.active = False
+
+ def show(self):
+ self.active = True
+ self.__widget.onShow()
+ self.__widget.update()
+
+ def hide(self):
+ self.active = False
+ self.__widget.onHide()
+
+ @property
+ def widget(self):
+ return self.__widget
+
+
+class DebugWidget(avg.DivNode):
+ SLOT_HEIGHT = 200
+ CAPTION = ''
+
+ WIDGET_HEIGHT_CHANGED = avg.Publisher.genMessageID()
+
+ def __init__(self, parent=None, **kwargs):
+ super(DebugWidget, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ self.publish(DebugWidget.WIDGET_HEIGHT_CHANGED)
+ if self.CAPTION:
+ self._caption = avg.WordsNode(text=self.CAPTION, pivot=(0, 0),
+ opacity=0.5, fontsize=14, parent=self)
+ self._caption.angle = math.pi / 2
+ self._caption.pos = (self.width, 0)
+
+ def syncSize(self, size):
+ self._caption.width = size[1]
+
+ def update(self):
+ pass
+
+ def onShow(self):
+ pass
+
+ def onHide(self):
+ pass
+
+ def kill(self):
+ pass
+
+
+NUM_COLS = 10
+COL_WIDTH = 60
+ROW_HEIGHT = g_fontsize + 2
+
+
+class TableRow(avg.DivNode):
+ COL_POS_X = 0
+ ROW_ID = 0
+
+ def __init__(self, parent=None, **kwargs):
+ super(TableRow, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ global NUM_COLS
+ NUM_COLS = int((self.parent.width - COL_WIDTH * 4) / COL_WIDTH)
+ self._initRow()
+ TableRow.ROW_ID += 1
+
+ def _initRow(self):
+ self.columnBackground = avg.RectNode(parent=self, fillcolor="222222",
+ fillopacity=0.6, opacity=0)
+ self.columnContainer = avg.DivNode(parent=self)
+ if TableRow.ROW_ID % 2 != 0:
+ self.columnBackground.fillopacity = 0
+ self.cols = [0] * NUM_COLS
+ self.liveColumn = avg.WordsNode(parent=self.columnContainer, fontsize=g_fontsize,
+ text="N/A - SPECIAL", size=(COL_WIDTH, ROW_HEIGHT), variant="bold")
+ for i in xrange(0, NUM_COLS):
+ self.cols[i] = (avg.WordsNode(parent=self.columnContainer,
+ fontsize=g_fontsize,
+ text="0", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
+ pos=((i+1) * COL_WIDTH, 0)),
+ avg.WordsNode(parent=self.columnContainer,
+ fontsize=g_fontsize,
+ text="(0)", size=(COL_WIDTH / 2.0, ROW_HEIGHT),
+ pos=((i+1) * COL_WIDTH + COL_WIDTH / 2, 0),
+ color="000000"))
+
+ self.rowData = deque([(0, 0)] * (NUM_COLS + 1), maxlen=NUM_COLS + 1)
+ self.label = avg.WordsNode(parent=self, fontsize=g_fontsize, variant="bold")
+ self.setLabel("NONE")
+
+ @property
+ def height(self):
+ return self.label.height
+
+ def setLabel(self, label):
+ if self.label.text == label + ":":
+ return
+ self.label.text = label + ":"
+ TableRow.COL_POS_X = max(TableRow.COL_POS_X, self.label.width)
+ if self.label.width < TableRow.COL_POS_X:
+ self.parent.labelColumnSizeChanged()
+
+ def resizeLabelColumn(self):
+ self.columnContainer.pos = (TableRow.COL_POS_X + 10, 0)
+ self.columnBackground.size = (self.columnContainer.x + self.liveColumn.x +
+ self.liveColumn.width, g_fontsize)
+
+ def insertValue(self, data):
+ prevValue = self.rowData[0][0]
+ self.rowData.appendleft([data, data-prevValue])
+ for i in xrange(0, len(self.rowData)-1):
+ val, diff = self.rowData[i]
+ column = self.cols[i]
+ column[0].text = str(val)
+ column[1].text = "({diff})".format(diff=diff)
+ column[1].pos = (column[0].x + column[0].getLineExtents(0)[0] + 2,
+ column[0].y)
+ if diff == 0:
+ column[1].color = "000000"
+ elif diff < 0:
+ column[1].color = "00FF00"
+ else:
+ column[1].color = "FF0000"
+
+ def updateLiveColumn(self, value):
+ self.liveColumn.text = str(value)
+
+
+class Table(avg.DivNode):
+ def __init__(self, parent=None, **kwargs):
+ super(Table, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ def labelColumnSizeChanged(self):
+ for childID in xrange(0, self.getNumChildren()):
+ child = self.getChild(childID)
+ child.resizeLabelColumn()
+
+
+class ObjectDumpWidget(DebugWidget):
+ CAPTION = 'Objects count'
+
+ def __init__(self, parent=None, **kwargs):
+ super(ObjectDumpWidget, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ self.tableContainer = Table(parent=self, size=(self.width, self.SLOT_HEIGHT))
+ self.tableDivs = defaultdict(lambda: TableRow(parent=self.tableContainer))
+
+ def update(self):
+ objDump = libavg.player.getTestHelper().getObjectCount()
+ pos = (0, 0)
+ for key in sorted(objDump.iterkeys()):
+ val = objDump[key]
+ self.tableDivs[key].updateLiveColumn(val)
+ self.tableDivs[key].setLabel(key)
+ self.tableDivs[key].pos = pos
+ pos = (0, pos[1] + self.tableDivs[key].height)
+ height = len(objDump) * self.tableDivs[key].height
+ if self.height != height:
+ self.notifySubscribers(DebugWidget.WIDGET_HEIGHT_CHANGED, [height])
+
+ def persistColumn(self):
+ objDump = libavg.player.getTestHelper().getObjectCount()
+ for key, val in objDump.iteritems():
+ self.tableDivs[key].insertValue(val)
+
+ def syncSize(self, size):
+ self.tableContainer.size = (size[0], size[1] - (g_fontsize + 2))
+
+ def onShow(self):
+ self.intervalID = libavg.player.setInterval(1000, self.update)
+ kbmgr.bindKeyDown(keystring='i',
+ handler=self.persistColumn,
+ help="Object count snapshot",
+ modifiers=libavg.KEYMOD_CTRL)
+
+ def onHide(self):
+ if self.intervalID:
+ libavg.player.clearInterval(self.intervalID)
+ self.intervalID = None
+ kbmgr.unbindKeyDown(keystring='i', modifiers=libavg.KEYMOD_CTRL)
+
+ def kill(self):
+ self.onHide()
+ self.tableDivs = None
+
+
+class GraphWidget(DebugWidget):
+ def __init__(self, **kwargs):
+ super(GraphWidget, self).__init__(**kwargs)
+ self.registerInstance(self, None)
+ self.__graph = None
+
+ def onShow(self):
+ if self.__graph:
+ self.__graph.active = True
+ else:
+ self.__graph = self._createGraph()
+
+ def onHide(self):
+ if self.__graph:
+ self.__graph.active = False
+
+ def kill(self):
+ self.__graph.unlink(True)
+
+ def _createGraph(self):
+ pass
+
+
+class MemoryGraphWidget(GraphWidget):
+ CAPTION = 'Memory usage'
+
+ def _createGraph(self):
+ return libavg.graph.AveragingGraph(parent=self, size=self.size,
+ getValue=avg.getMemoryUsage)
+
+
+class FrametimeGraphWidget(GraphWidget):
+ CAPTION = 'Time per frame'
+
+ def _createGraph(self):
+ return libavg.graph.SlidingBinnedGraph(parent=self,
+ getValue=libavg.player.getFrameTime,
+ binsThresholds=[0.0, 20.0, 40.0, 80.0, 160.0],
+ size=self.size)
+
+
+class GPUMemoryGraphWidget(GraphWidget):
+ CAPTION = 'GPU Memory usage'
+
+ def _createGraph(self):
+ try:
+ libavg.player.getVideoMemUsed()
+ except RuntimeError:
+ return avg.WordsNode(parent=self,
+ text='GPU memory graph is not supported on this hardware',
+ color='ff5555')
+ else:
+ return libavg.graph.AveragingGraph(parent=self, size=self.size,
+ getValue=libavg.player.getVideoMemUsed)
+
+
+class KeyboardManagerBindingsShower(DebugWidget):
+ CAPTION = 'Keyboard bindings'
+
+ def __init__(self, *args, **kwargs):
+ super(KeyboardManagerBindingsShower, self).__init__(**kwargs)
+ self.registerInstance(self, None)
+ self.keybindingWordNodes = []
+ kbmgr.publisher.subscribe(kbmgr.publisher.BINDINGS_UPDATED, self.update)
+
+ def clear(self):
+ for node in self.keybindingWordNodes:
+ node.unlink(True)
+ self.keybindingWordNodes = []
+
+ def update(self):
+ self.clear()
+ for binding in kbmgr.getCurrentBindings():
+ keystring = binding.keystring.decode('utf8')
+ modifiersStr = self.__modifiersToString(binding.modifiers)
+
+ if modifiersStr is not None:
+ key = '%s-%s' % (modifiersStr, keystring)
+ else:
+ key = keystring
+
+ if binding.type == libavg.avg.KEYDOWN:
+ key = '%s %s' % (unichr(8595), key)
+ else:
+ key = '%s %s' % (unichr(8593), key)
+
+ node = avg.WordsNode(
+ text='<span size="large"><b>%s</b></span>: %s' %
+ (key, binding.help),
+ fontsize=g_fontsize, parent=self)
+ self.keybindingWordNodes.append(node)
+
+ self._placeNodes()
+
+ def _placeNodes(self):
+ if not self.keybindingWordNodes:
+ return
+
+ maxWidth = max([node.width for node in self.keybindingWordNodes])
+ columns = int(self.parent.width / maxWidth)
+ rows = len(self.keybindingWordNodes) / columns
+ remainder = len(self.keybindingWordNodes) % columns
+
+ if remainder != 0:
+ rows += 1
+
+ colSize = self.parent.width / columns
+
+ currentColumn = 0
+ currentRow = 0
+ heights = [0] * columns
+ for node in self.keybindingWordNodes:
+ if currentRow == rows and currentColumn < columns - 1:
+ currentRow = 0
+ currentColumn += 1
+
+ node.pos = (currentColumn * colSize, heights[currentColumn])
+ heights[currentColumn] += node.height
+ currentRow += 1
+
+ finalHeight = max(heights)
+ if self.height != finalHeight:
+ self.notifySubscribers(self.WIDGET_HEIGHT_CHANGED, [finalHeight])
+
+ def __modifiersToString(self, modifiers):
+ def isSingleBit(number):
+ bitsSet = 0
+ for i in xrange(8):
+ if (1 << i) & number:
+ bitsSet += 1
+
+ return bitsSet == 1
+
+ if modifiers in (0, kbmgr.KEYMOD_ANY):
+ return None
+
+ allModifiers = []
+ for mod in dir(avg):
+ if 'KEYMOD_' in mod:
+ maskVal = int(getattr(avg, mod))
+ if isSingleBit(maskVal):
+ allModifiers.append((maskVal, mod))
+
+ modifiersStringsList = []
+ for modval, modstr in allModifiers:
+ if modifiers & modval:
+ modifiersStringsList.append(modstr.replace('KEYMOD_', ''))
+
+ for doubleMod in ['CTRL', 'META', 'SHIFT']:
+ left = 'L' + doubleMod
+ right = 'R' + doubleMod
+ if left in modifiersStringsList and right in modifiersStringsList:
+ modifiersStringsList.remove(left)
+ modifiersStringsList.remove(right)
+ modifiersStringsList.append(doubleMod)
+
+ return '/'.join(modifiersStringsList).lower()
+
+
+class DebugPanel(avg.DivNode):
+ def __init__(self, parent=None, fontsize=10, **kwargs):
+ super(DebugPanel, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ avg.RectNode(size=self.size, opacity=0, fillopacity=0.3, fillcolor='ff0000',
+ parent=self)
+ avg.WordsNode(text='Debug panel', fontsize=fontsize,
+ pos=(0, self.height - fontsize - fontsize / 3),
+ parent=self)
+
+ self.sensitive = False
+ self.active = False
+ self.__panel = None
+ self.__callables = []
+ self.__fontsize = fontsize
+ self.__touchVisOverlay = None
+
+ def setupKeys(self):
+ kbmgr.bindKeyDown(keystring='g',
+ handler=lambda: self.toggleWidget(GPUMemoryGraphWidget),
+ help="GPU memory graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='m',
+ handler=lambda: self.toggleWidget(MemoryGraphWidget),
+ help="Memory graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='f',
+ handler=lambda: self.toggleWidget(FrametimeGraphWidget),
+ help="Frametime graph",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='?',
+ handler=lambda: self.toggleWidget(KeyboardManagerBindingsShower),
+ help="Show keyboard bindings",
+ modifiers=kbmgr.KEYMOD_ANY)
+
+ kbmgr.bindKeyDown(keystring='o',
+ handler=lambda: self.toggleWidget(ObjectDumpWidget),
+ help="Object count table",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ kbmgr.bindKeyDown(keystring='v', handler=self.toggleTouchVisualization,
+ help="Cursor visualization",
+ modifiers=libavg.avg.KEYMOD_CTRL)
+
+ def addWidget(self, widgetCls, *args, **kwargs):
+ callable_ = lambda: self.__panel.addWidget(widgetCls, *args, **kwargs)
+ if self.__panel:
+ callable_()
+ else:
+ self.__callables.append(callable_)
+
+ def toggleWidget(self, *args, **kwargs):
+ if not self.active:
+ self.show()
+ self.__panel.ensureWidgetWisible(*args, **kwargs)
+ else:
+ self.__panel.toggleWidget(*args, **kwargs)
+
+ if not self.__panel.activeWidgetClasses:
+ self.hide()
+
+ def hide(self):
+ if self.__panel and self.active:
+ self.__panel.hide()
+ self.active = False
+
+ def show(self):
+ if self.__panel:
+ if not self.active:
+ self.__panel.show()
+ else:
+ self.forceLoadPanel()
+
+ self.active = True
+
+ def toggleVisibility(self):
+ if self.active:
+ self.hide()
+ else:
+ self.show()
+
+ def toggleTouchVisualization(self):
+ if self.__touchVisOverlay is None:
+ self.__touchVisOverlay = TouchVisOverlay(
+ isDebug=True,
+ visClass=DebugTouchVisualization,
+ size=self.parent.size,
+ parent=self.parent)
+ else:
+ self.__touchVisOverlay.unlink(True)
+ self.__touchVisOverlay = None
+
+ def forceLoadPanel(self):
+ if self.__panel is None:
+ self.__panel = _DebugPanel(parent=self, size=self.size,
+ fontsize=self.__fontsize)
+ for callable_ in self.__callables:
+ callable_()
+
+
+class _DebugPanel(avg.DivNode):
+
+ def __init__(self, parent=None, fontsize=10, **kwargs):
+ super(_DebugPanel, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__slots = []
+
+ self.maxSize = self.size
+ self.size = (self.size[0], 0)
+ self.activeWidgetClasses = []
+ self.__selectedWidget = None
+
+ global g_fontsize
+ g_fontsize = fontsize
+
+ self.show()
+
+ def show(self):
+ for widgetFrame in self.__slots:
+ if widgetFrame:
+ widgetFrame.show()
+ self.updateWidgets()
+
+ def hide(self):
+ for widget in self.__slots:
+ if widget:
+ widget.hide()
+
+ def ensureWidgetWisible(self, widgetClass, *args, **kwargs):
+ if not widgetClass in self.activeWidgetClasses:
+ self.toggleWidget(widgetClass, *args, **kwargs)
+
+ def toggleWidget(self, widgetClass, *args, **kwargs):
+ if widgetClass in self.activeWidgetClasses:
+ self._removeWidgetByClass(widgetClass)
+ else:
+ self.addWidget(widgetClass, *args, **kwargs)
+
+ def addWidget(self, widgetClass, *args, **kwargs):
+ if widgetClass in self.activeWidgetClasses:
+ libavg.logger.warning("You can't add the same widget twice")
+ return
+
+ widgetFrame = DebugWidgetFrame((max(0, self.width), DebugWidget.SLOT_HEIGHT),
+ widgetClass)
+ height = 0
+ for frame in self.__slots:
+ if frame:
+ height += frame.height
+ height += widgetFrame.height
+
+ if height > self.maxSize[1]:
+ libavg.logger.warning("No vertical space left. "
+ "Delete a widget and try again")
+ return False
+
+ self.appendChild(widgetFrame)
+
+ widgetPlaced = False
+ for idx, slot in enumerate(self.__slots):
+ if slot is None:
+ self.__slots[idx] = widgetFrame
+ widgetPlaced = True
+ break
+ if not widgetPlaced:
+ self.__slots.append(widgetFrame)
+ widgetFrame.subscribe(widgetFrame.FRAME_HEIGHT_CHANGED, self._heightChanged)
+
+ self.reorderWidgets()
+ widgetFrame.show()
+ self.updateWidgets()
+ self.activeWidgetClasses.append(widgetClass)
+
+ def _removeWidgetByClass(self, widgetClass):
+ for frame in self.__slots:
+ if frame and frame.widget.__class__ == widgetClass:
+ self.removeWidgetFrame(frame)
+ return
+
+ def _heightChanged(self):
+ height = 0
+ for childID in xrange(0, self.getNumChildren()):
+ child = self.getChild(childID)
+ height += child.height
+ self.height = height
+ self.reorderWidgets()
+
+ def updateWidgets(self):
+ for childID in xrange(0, self.getNumChildren()):
+ self.getChild(childID).widget.update()
+
+ def selectWidget(self, id):
+ id = id % self.getNumChildren()
+ for childID in xrange(0, self.getNumChildren()):
+ self.getChild(childID).unselect()
+ self.getChild(id).select()
+ self.__selectedWidget = id
+
+ def selectPreviousWidget(self):
+ if self.__selectedWidget is None:
+ self.selectWidget(-1)
+ else:
+ self.selectWidget(self.__selectedWidget - 1)
+
+ def selectNextWidget(self):
+ if self.__selectedWidget is None:
+ self.selectWidget(0)
+ else:
+ self.selectWidget(self.__selectedWidget + 1)
+
+ def removeWidgetFrame(self, widgetFrame):
+ self.activeWidgetClasses.remove(widgetFrame.widget.__class__)
+ for idx, slot in enumerate(self.__slots):
+ if slot == widgetFrame:
+ self.__slots[idx] = None
+ break
+ widgetFrame.widget.kill()
+ widgetFrame.unlink(True)
+ self.reorderWidgets()
+ self.updateWidgets()
+
+ def removeSelectedWidgetFrames(self):
+ candidates = []
+ for childID in xrange(0, self.getNumChildren()):
+ child = self.getChild(childID)
+ if child.isSelected():
+ candidates.append(child)
+ for widgetFrame in candidates:
+ self.removeWidgetFrame(widgetFrame)
+ self.__selectedWidget = None
+
+ def reorderWidgets(self):
+ #TODO: This is no layout management, yet
+ count = 0
+ height = 0
+ for idx, widgetFrame in enumerate(self.__slots):
+ if widgetFrame:
+ widgetFrame.pos = (0, height)
+ count += 1
+ height += widgetFrame.height
+ self.size = (self.maxSize[0], height)
diff --git a/src/python/app/flashmessage.py b/src/python/app/flashmessage.py
new file mode 100644
index 0000000..ebaab85
--- /dev/null
+++ b/src/python/app/flashmessage.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+
+'''
+Simple user notification API to report information to the user
+'''
+
+import libavg
+
+class FlashMessage(object):
+ DEFAULT_TIMEOUT = 3000
+ LINE_HEIGHT = 20
+ BORDER = 2
+
+ messages = []
+
+ @classmethod
+ def remove(cls, killedMessage):
+ cls.messages.remove(killedMessage)
+ for index, message in enumerate(cls.messages):
+ message.move(index)
+
+ def __init__(self, text, timeout=DEFAULT_TIMEOUT, parent=None, isError=False,
+ acknowledge=False):
+ FlashMessage.messages.append(self)
+
+ if parent is None:
+ parent = libavg.player.getRootNode()
+
+ if isError:
+ color = 'ff0000'
+ else:
+ color = 'ffffff'
+
+ rootNode = libavg.player.getRootNode()
+ self.__container = libavg.avg.DivNode(sensitive=acknowledge, parent=parent)
+ libavg.avg.RectNode(opacity=0, fillcolor='ffffff', fillopacity=1,
+ pos=(self.BORDER, self.BORDER),
+ size=(rootNode.size.x - self.BORDER * 2, self.LINE_HEIGHT - self.BORDER),
+ parent=self.__container)
+ libavg.avg.RectNode(opacity=0, fillcolor='000000', fillopacity=0.8,
+ pos=(self.BORDER, self.BORDER),
+ size=(rootNode.size.x - self.BORDER * 2, self.LINE_HEIGHT - self.BORDER),
+ parent=self.__container)
+ libavg.avg.WordsNode(text=text, fontsize=(self.LINE_HEIGHT - 3),
+ sensitive=False,
+ color=color,
+ pos=(self.BORDER, self.BORDER),
+ parent=self.__container)
+
+ self.move(len(FlashMessage.messages) - 1, animate=False)
+
+ if acknowledge:
+ self.__container.subscribe(self.__container.CURSOR_DOWN,
+ lambda e: self.__kill())
+ else:
+ libavg.player.setTimeout(timeout, self.__kill)
+
+ def move(self, index, animate=True):
+ finalPos = (self.BORDER, index * self.LINE_HEIGHT)
+
+ if animate:
+ libavg.avg.LinearAnim(self.__container, 'pos', duration=150,
+ startValue=self.__container.pos,
+ endValue=finalPos).start()
+ else:
+ self.__container.pos = finalPos
+
+ def __kill(self):
+ def finalizeRemoval():
+ self.__container.unlink(True)
+ self.__container = None
+ FlashMessage.remove(self)
+
+ libavg.avg.fadeOut(self.__container, 200, finalizeRemoval)
+
diff --git a/src/python/app/keyboardmanager.py b/src/python/app/keyboardmanager.py
new file mode 100644
index 0000000..accdf52
--- /dev/null
+++ b/src/python/app/keyboardmanager.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+
+from collections import namedtuple
+
+from libavg import avg, player
+
+IGNORED_KEYMODS = avg.KEYMOD_NUM
+KEYMOD_ANY = -1
+
+LOGCAT = avg.logger.configureCategory('KEYBOARDMANAGER',
+ avg.logger.Severity.WARN)
+
+class KeyboardManagerPublisher(avg.Publisher):
+ BINDINGS_UPDATED = avg.Publisher.genMessageID()
+ def __init__(self):
+ super(KeyboardManagerPublisher, self).__init__()
+ self.publish(self.BINDINGS_UPDATED)
+
+ def notifyUpdate(self):
+ self.notifySubscribers(self.BINDINGS_UPDATED, [])
+
+publisher = KeyboardManagerPublisher()
+
+_KeyBinding = namedtuple('_KeyBinding',
+ ['keystring', 'handler', 'help', 'modifiers', 'type'])
+
+
+_modifiedKeyBindings = []
+_plainKeyBindings = []
+_plainKeyBindingsStack = []
+_isEnabled = True
+
+
+def init():
+ player.subscribe(player.KEY_DOWN, _onKeyDown)
+ player.subscribe(player.KEY_UP, _onKeyUp)
+ avg.logger.debug('Keyboardmanager initialized', LOGCAT)
+
+def bindKeyDown(keystring, handler, help, modifiers=avg.KEYMOD_NONE):
+ _bindKey(keystring, handler, help, modifiers, avg.KEYDOWN)
+
+def bindKeyUp(keystring, handler, help, modifiers=avg.KEYMOD_NONE):
+ _bindKey(keystring, handler, help, modifiers, avg.KEYUP)
+
+def unbindKeyUp(keystring, modifiers=avg.KEYMOD_NONE):
+ _unbindKey(keystring, modifiers, avg.KEYUP)
+
+def unbindKeyDown(keystring, modifiers=avg.KEYMOD_NONE):
+ _unbindKey(keystring, modifiers, avg.KEYDOWN)
+
+def unbindAll():
+ global _modifiedKeyBindings, _plainKeyBindings, _plainKeyBindingsStack
+ _modifiedKeyBindings = []
+ _plainKeyBindings = []
+ _plainKeyBindingsStack = []
+ publisher.notifyUpdate()
+
+def push():
+ '''
+ Push the current non-modified defined key bindings to the stack
+ '''
+ global _plainKeyBindings
+ _plainKeyBindingsStack.append(_plainKeyBindings)
+ _plainKeyBindings = []
+ publisher.notifyUpdate()
+
+def pop():
+ '''
+ Pop from the stack the current non-modified defined key bindings
+ '''
+ global _plainKeyBindings
+ _plainKeyBindings = _plainKeyBindingsStack.pop()
+ publisher.notifyUpdate()
+
+def getCurrentBindings():
+ return _modifiedKeyBindings + _plainKeyBindings
+
+def enable():
+ global _isEnabled
+ _isEnabled = True
+
+def disable():
+ global _isEnabled
+ _isEnabled = False
+
+def _bindKey(keystring, handler, help, modifiers, type_):
+ if type(keystring) == unicode:
+ keystring = keystring.encode('utf8')
+
+ avg.logger.info('Binding key <%s> (mod:%s) to handler %s (%s)' % (keystring,
+ modifiers, handler, type), LOGCAT)
+ _checkDuplicates(keystring, modifiers, type_)
+ keyBinding = _KeyBinding(keystring, handler, help, modifiers, type_)
+
+ if modifiers != avg.KEYMOD_NONE:
+ _modifiedKeyBindings.append(keyBinding)
+ else:
+ _plainKeyBindings.append(keyBinding)
+
+ publisher.notifyUpdate()
+
+def _findAndRemoveKeybinding(keystring, modifiers, type, list):
+ for keybinding in list:
+ if keybinding.keystring == keystring and \
+ keybinding.modifiers == modifiers and \
+ keybinding.type == type:
+ list.remove(keybinding)
+ break;
+
+def _unbindKey(keystring, modifiers, type_):
+ if type(keystring) == unicode:
+ keystring = keystring.encode('utf8')
+
+ avg.logger.info('Unbinding key <%s> (mod:%s) (%s)' % (keystring,
+ modifiers, type), LOGCAT)
+ if modifiers != avg.KEYMOD_NONE:
+ _findAndRemoveKeybinding(keystring, modifiers, type_, _modifiedKeyBindings)
+ else:
+ _findAndRemoveKeybinding(keystring, modifiers, type_, _plainKeyBindings)
+
+ publisher.notifyUpdate()
+
+def _onKeyDown(event):
+ if _isEnabled:
+ _processEvent(event, avg.KEYDOWN)
+
+def _onKeyUp(event):
+ if _isEnabled:
+ _processEvent(event, avg.KEYUP)
+
+def _testModifiers(mod1, mod2):
+ if mod1 == KEYMOD_ANY or mod2 == KEYMOD_ANY:
+ return True
+
+ mod1 &= ~IGNORED_KEYMODS
+ mod2 &= ~IGNORED_KEYMODS
+ return mod1 == mod2 or mod1 & mod2
+
+def _testPatternMatch(pattern, text):
+ if pattern in ('shift', 'alt', 'ctrl', 'meta', 'super'):
+ return pattern in text
+ else:
+ return False
+
+def _testMatchString(keyBinding, keyString, type_):
+ sameType = keyBinding.type == type_
+ patternMatch = _testPatternMatch(keyBinding.keystring, keyString)
+ directMatch = keyBinding.keystring == keyString
+
+ return sameType and (directMatch or patternMatch)
+
+def _testMatchEvent(keyBinding, event, type_):
+ if not _testModifiers(event.modifiers, keyBinding.modifiers):
+ return False
+
+ if _testMatchString(keyBinding, event.keystring, type_):
+ return True
+
+ if type_ == avg.KEYDOWN:
+ return _testMatchString(keyBinding,
+ unichr(event.unicode).encode('utf8'), type_)
+ else:
+ return False
+
+def _processEvent(event, type_):
+ avg.logger.debug('Processing event keystring=%s '
+ 'modifiers=%s type=%s' % (event.keystring, event.modifiers, event.type),
+ LOGCAT)
+ for keyBinding in _plainKeyBindings + _modifiedKeyBindings:
+ if _testMatchEvent(keyBinding, event, type_):
+ avg.logger.debug(' Found keyBinding=%s' % (keyBinding,), LOGCAT)
+ keyBinding.handler()
+ return
+
+def _checkDuplicates(keystring, modifiers, type_):
+ for keyBinding in _plainKeyBindings + _modifiedKeyBindings:
+ if (_testModifiers(keyBinding.modifiers, modifiers) and
+ _testMatchString(keyBinding, keystring, type_)):
+ raise RuntimeError('Key binding keystring=%s modifiers=%s type=%s '
+ 'already defined' % (keystring, modifiers, type_))
+
diff --git a/src/python/app/settings.py b/src/python/app/settings.py
new file mode 100644
index 0000000..a25eece
--- /dev/null
+++ b/src/python/app/settings.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+
+import sys
+import re
+import optparse
+
+import libavg
+
+
+class Option(object):
+ def __init__(self, key, value, help=None):
+ if not isinstance(key, str):
+ raise ValueError('The type of %s key is not string (value=%s)' % (key, value))
+
+ self.__key = key
+ self.value = value
+ self.__help = help
+
+ def __repr__(self):
+ return '<%s key=%s value=%s help=%s>' % (self.__class__.__name__,
+ self.key, self.value, self.help)
+
+ @property
+ def key(self):
+ return self.__key
+
+ @property
+ def value(self):
+ return self.__value
+
+ @value.setter
+ def value(self, value):
+ if not isinstance(value, str):
+ raise ValueError('The type of %s value (%s) '
+ 'must be string instead of %s' % (self.__key, value, type(value)))
+
+ self.__value = value
+
+ @property
+ def group(self):
+ components = self.__getComponents()
+ if len(components) == 1:
+ return 'DEFAULT'
+ else:
+ return components[0]
+
+ @property
+ def tail(self):
+ components = self.__getComponents()
+ if len(components) == 1:
+ return self.key
+ else:
+ return components[1]
+
+ @property
+ def help(self):
+ return self.__help
+
+ def __getComponents(self):
+ return self.key.split('_', 1)
+
+
+class KargsExtender(object):
+ def __init__(self, optionsKargs):
+ self.__optionsKargs = optionsKargs
+
+ def __call__(self, optionsList):
+ optionsKeyset = set([option.key for option in optionsList])
+ kaKeyset = set(self.__optionsKargs.keys())
+
+ if not optionsKeyset.issuperset(kaKeyset):
+ raise RuntimeError('No such option/s: %s' % list(kaKeyset - optionsKeyset))
+
+ for option in optionsList:
+ if option.key in self.__optionsKargs:
+ option.value = self.__optionsKargs[option.key]
+
+ return optionsList
+
+
+class HelpPrintingOptionParser(optparse.OptionParser):
+ def error(self, *args, **kargs):
+ self.print_help()
+ optparse.OptionParser.error(self, *args, **kargs)
+
+
+class ArgvExtender(object):
+ def __init__(self, appVersionInfo, args=None):
+ self.__appVersionInfo = appVersionInfo
+ self.__parser = HelpPrintingOptionParser()
+ self.__args = args
+ self.__parsedArgs = None
+
+ def __call__(self, optionsList):
+ self.__parser.add_option('-v', '--version', dest='version', action='store_true',
+ help='print libavg and application version information')
+
+ groups = self.__groupOptionsKeys(optionsList)
+
+ for group in sorted(groups):
+ parserGroup = optparse.OptionGroup(self.__parser,
+ '%s section' % group.title())
+
+ keys = sorted(groups[group])
+
+ for option in [option for option in optionsList if option.key in keys]:
+ cliKey = '--%s' % option.key.replace('_', '-').lower()
+ currentValue = option.value if option.value else '<undefined>'
+
+ help = '[Default: %s]' % currentValue
+
+ if option.help:
+ help = '%s %s' % (option.help, help)
+
+ parserGroup.add_option(cliKey, help=help)
+
+ self.__parser.add_option_group(parserGroup)
+
+ if self.__args is None:
+ self.__args = sys.argv[1:]
+
+ self.__parsedArgs = self.__parser.parse_args(args=self.__args)
+
+ parsedOptions = self.__parsedArgs[0]
+
+ if parsedOptions.version:
+ print 'libavg'
+ vi = libavg.VersionInfo()
+ print ' version : %s' % vi.full
+ print ' builder : %s (%s)' % (vi.builder, vi.buildtime)
+ print ' branchurl: %s' % vi.branchurl
+ print
+ print 'application'
+ print ' version: %s' % self.__appVersionInfo
+ sys.exit(0)
+
+ for key, value in parsedOptions.__dict__.iteritems():
+ if value is not None:
+ for option in optionsList:
+ if option.key == key:
+ option.value = value
+
+ return optionsList
+
+ @property
+ def parsedArgs(self):
+ if self.__parsedArgs is None:
+ raise RuntimeError('Cannot provide parsedArgs before applying the extender')
+
+ return self.__parsedArgs
+
+ @property
+ def parser(self):
+ return self.__parser
+
+ def __groupOptionsKeys(self, optionsList):
+ groups = {}
+ for option in optionsList:
+ if not option.group in groups:
+ groups[option.group] = []
+
+ groups[option.group].append(option.key)
+
+ return groups
+
+
+class Settings(object):
+ def __init__(self, defaults=[]):
+ if (type(defaults) not in (tuple, list) or
+ not all([isinstance(opt, Option) for opt in defaults])):
+ raise ValueError('Settings must be initialized with a list '
+ 'of Option instances')
+
+ self.__options = []
+
+ for option in defaults:
+ self.addOption(option)
+
+ def __iter__(self):
+ return self.__options.__iter__()
+
+ def applyExtender(self, extender):
+ self.__options = extender(self.__options)
+
+ def hasOption(self, key):
+ return self.__getOptionOrNone(key) is not None
+
+ def getOption(self, key):
+ option = self.__getOptionOrNone(key)
+
+ if option is None:
+ raise RuntimeError('Cannot find key %s in the settings' % key)
+
+ return option
+
+ def get(self, key, convertFunc=lambda v: v):
+ option = self.getOption(key)
+
+ try:
+ return convertFunc(option.value)
+ except (TypeError, ValueError), e:
+ raise ValueError('%s (option=%s)' % (e, option))
+
+ def getJson(self, key):
+ import json
+
+ return self.get(key, json.loads)
+
+ def getPoint2D(self, key):
+ value = self.get(key)
+ maybeTuple = re.split(r'\s*[,xX]\s*', value)
+
+ if len(maybeTuple) != 2:
+ raise ValueError('Cannot convert key %s value %s to Point2D' % (key, value))
+
+ return libavg.Point2D(map(float, maybeTuple))
+
+ def getInt(self, key):
+ return self.get(key, int)
+
+ def getFloat(self, key):
+ return self.get(key, float)
+
+ def getBoolean(self, key):
+ value = self.get(key).lower()
+
+ if value in ('yes', 'true'):
+ return True
+ elif value in ('no', 'false'):
+ return False
+ else:
+ raise ValueError('Cannot convert %s to boolean' % value)
+
+ def set(self, key, value):
+ option = self.getOption(key)
+ option.value = value
+
+ def addOption(self, option):
+ if not isinstance(option, Option):
+ raise TypeError('Must be an instance of Option')
+
+ if self.__getOptionOrNone(option.key):
+ raise RuntimeError('Option %s has been already defined' % option.key)
+
+ self.__options.append(option)
+
+ def __getOptionOrNone(self, key):
+ for option in self.__options:
+ if option.key == key:
+ return option
+
+ return None
+
+
diff --git a/src/python/app/touchvisualization.py b/src/python/app/touchvisualization.py
new file mode 100644
index 0000000..7953b02
--- /dev/null
+++ b/src/python/app/touchvisualization.py
@@ -0,0 +1,188 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+import os
+
+from libavg import avg, player
+
+
+class BaseTouchVisualization(avg.DivNode):
+
+ def __init__(self, event, parent=None, **kwargs):
+ avg.DivNode.__init__(self, **kwargs)
+ self.registerInstance(self, parent)
+
+ event.contact.subscribe(avg.Contact.CURSOR_MOTION, self._onMotion)
+ event.contact.subscribe(avg.Contact.CURSOR_UP, self._onUp)
+ self.pos = avg.Point2D(event.pos)
+ self._fingerSize = 7*player.getPixelsPerMM() # Assume 14mm width for a finger.
+ self._radius = self._getRadius(event)
+
+ def _abort(self):
+ self.unlink(True)
+ del self
+
+ def _onMotion(self, event):
+ self.pos = event.pos
+ self._radius = self._getRadius(event)
+
+ def _onUp(self, event):
+ self.unlink(True)
+ del self
+
+ def _getRadius(self, event):
+ if event.source in [avg.Event.MOUSE]:
+ return self._fingerSize
+ else:
+ return max(self._fingerSize, event.majoraxis.getNorm())
+
+
+
+class DebugTouchVisualization(BaseTouchVisualization):
+
+ def __init__(self, event, **kwargs):
+ BaseTouchVisualization.__init__(self, event, **kwargs)
+ self.positions = [event.pos]
+
+ if event.source == avg.Event.TOUCH:
+ color = 'e5d8d8'
+ else:
+ color = 'd8e5e5'
+ self.opacity = 0.5
+
+ self.__transparentCircle = avg.CircleNode(r=self._radius+20, fillcolor=color,
+ fillopacity=0.2, opacity=0.0, strokewidth=1, sensitive=False, parent=self)
+ self.__pulsecircle = avg.CircleNode(r=self._radius, fillcolor=color, color=color,
+ fillopacity=0.5, opacity=0.5, strokewidth=1,
+ sensitive=False, parent=self)
+ if event.source in [avg.Event.TOUCH, avg.Event.TRACK]:
+ self.__majorAxis = avg.LineNode(pos1=(0,0), pos2=event.majoraxis,
+ color='FFFFFF', sensitive=False, parent=self)
+ self.__minorAxis = avg.LineNode(pos1=(0,0), pos2=event.minoraxis,
+ color='FFFFFF', sensitive=False, parent=self)
+ if event.source == avg.Event.TOUCH:
+ self.__handAxis = avg.LineNode(pos1=(0,0), pos2=self.__getHandVector(event),
+ opacity=0.5, color='A0FFA0', sensitive=False, parent=self)
+ fontPos = avg.Point2D(self.__pulsecircle.r, 0)
+
+ if event.cursorid == -1:
+ text = 'MOUSE'
+ else:
+ text = '%s %d' % (event.source, event.cursorid)
+ avg.WordsNode(pos=fontPos, text=text, fontsize=9, parent=self)
+ self.motionPath = avg.PolyLineNode(pos=self.positions,
+ opacity=0.7, color=color, parent=kwargs['parent'])
+ self.motionVector = avg.LineNode(pos1=(0,0) , pos2=-event.contact.motionvec,
+ opacity=0.4, parent=self)
+ pulseCircleAnim = avg.LinearAnim(self.__pulsecircle, 'r', 200, 50, self._radius)
+ pulseCircleAnim.start()
+
+ def unlink(self, kill=True):
+ if self.motionPath:
+ self.motionPath.unlink(True)
+ super(DebugTouchVisualization, self).unlink(kill)
+
+ def _onMotion(self, event):
+ BaseTouchVisualization._onMotion(self, event)
+ self.positions.append(event.pos)
+ if len(self.positions) > 100:
+ self.positions.pop(0)
+
+ self.__pulsecircle.r = self._radius
+ self.setAxisSecondPos(event)
+ self.motionVector.pos2 = -event.contact.motionvec
+ if event.source == avg.Event.TOUCH:
+ self.__handAxis.pos2 = self.__getHandVector(event)
+ self.motionPath.pos = self.positions
+
+ def __getHandVector(self, event):
+ return -avg.Point2D.fromPolar(event.handorientation, 30)
+
+ def setAxisSecondPos(self, event):
+ if event.source not in [avg.Event.MOUSE]:
+ self.__majorAxis.pos2 = event.majoraxis
+ self.__minorAxis.pos2 = event.minoraxis
+
+
+class TouchVisualization(BaseTouchVisualization):
+
+ mediadir = os.path.join(os.path.dirname(__file__), os.path.pardir, 'data')
+ sources = [avg.Event.TOUCH]
+ bmp = avg.Bitmap(mediadir+"/TouchFeedback.png")
+
+ def __init__(self, event, **kwargs):
+ BaseTouchVisualization.__init__(self, event, **kwargs)
+
+ if event.source in self.sources:
+ self.__circle = avg.ImageNode(parent=self)
+ self.__circle.setBitmap(self.bmp)
+ self.__setRadius(self._radius)
+ avg.LinearAnim(self.__circle, "opacity", 200, 0.7, 0.4).start()
+ else:
+ self.unlink(True)
+ self._abort()
+
+ def _onMotion(self, event):
+ BaseTouchVisualization._onMotion(self, event)
+ self.__setRadius(self._radius)
+
+ def _onUp(self, event):
+
+ def gone(self):
+ BaseTouchVisualization._onUp(self, event)
+ self.unlink(True)
+ del self
+
+ avg.fadeIn(self.__circle, 100, 1)
+ avg.LinearAnim(self.__circle, "size", 100, self.__circle.size, (4,4)).start()
+ avg.LinearAnim(self.__circle, "pos", 100, self.__circle.pos, (-2,-2)).start()
+ player.setTimeout(100, lambda: gone(self))
+
+ def __setRadius(self, radius):
+ self.__circle.pos = (-radius, -radius)
+ self.__circle.size = (radius*2,radius*2)
+
+
+class TouchVisualizationOverlay(avg.DivNode):
+ def __init__(self, isDebug, visClass, rootNode=None, parent=None,
+ **kwargs):
+ super(TouchVisualizationOverlay, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.sensitive = False
+ self.visClass = visClass
+ if rootNode is None:
+ rootNode = player.getRootNode()
+
+ if isDebug:
+ self.elementoutlinecolor='FFFFAA'
+ avg.RectNode(parent=self, size=self.size, fillopacity=0.2, fillcolor='000000')
+ rootNode.subscribe(avg.Node.CURSOR_DOWN, self.__onTouchDown)
+ rootNode.subscribe(avg.Node.HOVER_DOWN, self.__onTouchDown)
+
+ def unlink(self, kill=True):
+ rootNode = player.getRootNode()
+ if rootNode:
+ rootNode.unsubscribe(avg.Node.CURSOR_DOWN, self.__onTouchDown)
+ rootNode.unsubscribe(avg.Node.HOVER_DOWN, self.__onTouchDown)
+ super(TouchVisualizationOverlay, self).unlink(kill)
+
+ def __onTouchDown(self, event):
+ self.visClass(event, parent=self)
diff --git a/src/python/apphelpers.py b/src/python/apphelpers.py
new file mode 100644
index 0000000..b23b69c
--- /dev/null
+++ b/src/python/apphelpers.py
@@ -0,0 +1,235 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+import os
+
+from libavg import avg, player
+
+from app.touchvisualization import *
+
+
+class KeysCaptionNode(avg.DivNode):
+ def __init__(self, parent=None, **kwargs):
+ super(KeysCaptionNode, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.sensitive = False
+ self.opacity = 0
+
+ self.__background = avg.RectNode(fillcolor='000000', fillopacity=0.6,
+ opacity=0, size=(450, 450), parent=self)
+
+ self.__keysNode = avg.WordsNode(pos=(10, 10), fontsize=18,
+ color='DDDDDD', parent=self)
+
+ self.__isShown = False
+
+ def toggleHelp(self):
+ self.__isShown = not self.__isShown
+
+ keys = g_KbManager.getActiveKeyBindings()
+
+ if self.__isShown:
+ helpText = '<span><b> ACTIVE KEYS </b><br/></span>'
+
+ for keyObj in sorted(keys, key=lambda ko: ko.key):
+ if keyObj.state == 'up':
+ stateAddition = ' (up)'
+ else:
+ stateAddition = ''
+
+ helpText += ('<span><b>%s</b> '
+ '<small>%s%s</small></span><br/>' % (keyObj.key,
+ keyObj.description, stateAddition))
+
+ self.__keysNode.text = helpText
+ self.opacity = 1
+ self.__background.size = self.__keysNode.getMediaSize()
+
+ self.getParent().reorderChild(
+ self.getParent().indexOf(self),
+ self.getParent().getNumChildren()-1)
+ else:
+ self.__keysNode.text = ''
+ self.opacity = 0
+
+
+class KeyBinding(object):
+ def __init__(self, key, description, state, callback):
+ if not isinstance(key, unicode) and not isinstance(key, str):
+ raise TypeError('KeyBinding key should be either a string or unicode object')
+
+ self.__key = key
+ self.__description = description
+ self.__state = state
+ self.__callback = callback
+
+ def __repr__(self):
+ return '<%s key=%s (%s) state=%s>' % (self.__class__.__name__,
+ self.__key, self.__description, self.__state)
+
+ @property
+ def key(self):
+ return self.__key
+
+ @property
+ def description(self):
+ return self.__description
+
+ @property
+ def state(self):
+ return self.__state
+
+ def checkKey(self, key, state):
+ if state is not None and self.__state != state:
+ return False
+
+ return self.__key == key
+
+ def checkEvent(self, event, state):
+ if self.__state != state:
+ return False
+
+ if isinstance(self.__key, unicode):
+ return self.__key == unichr(event.unicode)
+ else:
+ return self.__key == event.keystring
+
+ def executeCallback(self):
+ self.__callback()
+
+
+class KeyboardManager(object):
+ _instance = None
+ TOGGLE_HELP_UNICODE = 63
+
+ def __init__(self):
+ if self._instance is not None:
+ raise RuntimeError('KeyboardManager has been already instantiated')
+
+ self.__keyBindings = []
+ self.__keyBindingsStack = []
+
+ self.__onKeyDownCb = lambda e: False
+ self.__onKeyUpCb = lambda e: False
+
+ self.__keyCaptionsNode = None
+
+ KeyboardManager._instance = self
+
+ @classmethod
+ def get(cls):
+ if cls._instance is None:
+ cls()
+
+ return cls._instance
+
+ def setup(self, onKeyDownCb, onKeyUpCb):
+ player.subscribe(avg.Player.KEY_DOWN, self.__onKeyDown)
+ player.subscribe(avg.Player.KEY_UP, self.__onKeyUp)
+
+ self.__onKeyDownCb = onKeyDownCb
+ self.__onKeyUpCb = onKeyUpCb
+
+ self.__keyCaptionsNode = KeysCaptionNode(pos=(5,5), parent=player.getRootNode())
+
+ def teardown(self):
+ self.__keyBindings = []
+
+ self.__keyCaptionsNode.unlink(True)
+ del self.__keyCaptionsNode
+ self.__keyCaptionsNode = None
+
+ def push(self):
+ self.__keyBindingsStack.append(self.__keyBindings)
+ self.__keyBindings = []
+
+ def pop(self):
+ if not self.__keyBindingsStack:
+ raise RuntimeError('Empty stack')
+
+ self.__keyBindings = self.__keyBindingsStack.pop()
+
+ def getActiveKeyBindings(self):
+ return self.__keyBindings
+
+ def bindKey(self, key, func, funcName, state='down'):
+ import warnings
+ warnings.warn('libavg.KeyboardManager is deprecated, use '
+ 'libavg.app.keyboardmanager instead')
+
+ if isinstance(key, unicode) and state != 'down':
+ raise RuntimeError('bindKey() with unicode keys '
+ 'can be used only with state=down')
+
+ if key == unichr(self.TOGGLE_HELP_UNICODE):
+ raise RuntimeError('%s key is reserved')
+
+ keyObj = self.__findKeyByKeystring(key, state)
+ if keyObj is not None:
+ raise RuntimeError('Key %s has already been bound (%s)' % (key, keyObj))
+
+ self.__keyBindings.append(KeyBinding(key, funcName, state, func))
+
+ def unbindKey(self, key):
+ keyObj = self.__findKeyByKeystring(key)
+
+ if keyObj is not None:
+ self.__keyBindings.remove(keyObj)
+ else:
+ raise KeyError('Key %s not found' % key)
+
+ def bindUnicode(self, key, func, funcName, state='down'):
+ raise DeprecationWarning('Use bindKey() passing an unicode object as keystring')
+
+ def __findKeyByEvent(self, event, state):
+ for keyObj in self.__keyBindings:
+ if keyObj.checkEvent(event, state):
+ return keyObj
+
+ return None
+
+ def __findKeyByKeystring(self, key, state=None):
+ for keyObj in self.__keyBindings:
+ if keyObj.checkKey(key, state):
+ return keyObj
+
+ return None
+
+ def __onKeyDown(self, event):
+ if self.__onKeyDownCb(event):
+ return
+ elif event.unicode == self.TOGGLE_HELP_UNICODE:
+ self.__keyCaptionsNode.toggleHelp()
+ else:
+ keyObj = self.__findKeyByEvent(event, 'down')
+ if keyObj is not None:
+ keyObj.executeCallback()
+
+ def __onKeyUp(self, event):
+ if self.__onKeyUpCb(event):
+ return
+ else:
+ keyObj = self.__findKeyByEvent(event, 'up')
+ if keyObj is not None:
+ keyObj.executeCallback()
+
+
+g_KbManager = KeyboardManager.get()
diff --git a/src/python/appstarter.py b/src/python/appstarter.py
new file mode 100644
index 0000000..aca4fe0
--- /dev/null
+++ b/src/python/appstarter.py
@@ -0,0 +1,396 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is Martin Heistermann <mh at sponc dot de>
+#
+
+import os
+import gc
+import math
+
+from libavg import avg, Point2D, player
+import graph
+from mtemu import MTemu
+import apphelpers
+
+
+DEFAULT_RESOLUTION = (640, 480)
+
+g_KbManager = apphelpers.KeyboardManager.get()
+
+
+class AppStarter(object):
+ '''Starts an AVGApp'''
+ def __init__(self, appClass, resolution=DEFAULT_RESOLUTION,
+ debugWindowSize=None, fakeFullscreen=False):
+
+ resolution = Point2D(resolution)
+ testMode = not 'AVG_DEPLOY' in os.environ
+
+ if testMode and debugWindowSize is not None:
+ debugWindowSize = Point2D(debugWindowSize)
+ else:
+ debugWindowSize = Point2D(0, 0)
+
+ if fakeFullscreen:
+ if os.name != 'nt':
+ raise RuntimeError('Fakefullscreen is supported only on windows')
+ elif not testMode:
+ self.__enableFakeFullscreen()
+
+ fullscreen = False
+ else:
+ fullscreen = not testMode
+
+ player.enableMouse(not 'AVG_DISABLE_MOUSE' in os.environ)
+ player.showCursor(testMode)
+ self._setupBaseDivs(resolution)
+
+ player.setResolution(
+ fullscreen,
+ int(debugWindowSize.x), int(debugWindowSize.y),
+ 0 # color depth
+ )
+
+ self._startApp(appClass)
+
+ def _startApp(self, appClass):
+ self._onBeforePlay()
+ player.setTimeout(0, self._onStart)
+ self._appInstance = appClass(self._appNode)
+ g_KbManager.setup(
+ self._appInstance.onKeyDown,
+ self._appInstance.onKeyUp)
+
+ self._setupDefaultKeys()
+
+ self._appInstance.setStarter(self)
+ player.play()
+ self._appInstance.exit()
+ g_KbManager.teardown()
+
+ def _setupBaseDivs(self, resolution):
+ player.loadString('''
+<?xml version="1.0"?>
+<!DOCTYPE avg SYSTEM "../../libavg/doc/avg.dtd">
+<avg width="%s" height="%s">
+</avg>''' % (resolution.x, resolution.y))
+
+ rootNode = player.getRootNode()
+ self._appNode = avg.DivNode(opacity=0, sensitive=False,
+ size=rootNode.size, parent=rootNode)
+
+ def _setupDefaultKeys(self):
+ pass
+
+ def _onBeforePlay(self):
+ pass
+
+ def _onStart(self):
+ self._appInstance.init()
+ self._appNode.opacity = 1
+ self._appNode.sensitive = True
+ self._activeApp = self._appInstance
+ self._appInstance.enter()
+
+ def __enableFakeFullscreen(self):
+ player.setWindowPos(0, 0)
+ player.setWindowFrame(False)
+
+
+class AVGAppStarter(AppStarter):
+ def __init__(self, *args, **kwargs):
+ self.__graphs = []
+ self._mtEmu = None
+ self.__memGraph = None
+ self.__vidMemGraph = None
+ self.__frGraph = None
+ self.__notifyNode = None
+ self.__debugTouchVisOverlay = None
+
+ super(AVGAppStarter, self).__init__(*args, **kwargs)
+
+ def _setupDefaultKeys(self):
+ super(AVGAppStarter, self)._setupDefaultKeys()
+ g_KbManager.bindKey('o', self.__dumpObjects, 'Dump objects')
+ g_KbManager.bindKey('m', self.showMemoryUsage, 'Show memory usage graph')
+
+ g_KbManager.bindKey('f', self.showFrameRate, 'Show framerate graph')
+ g_KbManager.bindKey('t', self.__switchMtemu, 'Activate multitouch emulation')
+ g_KbManager.bindKey('e', self.__switchShowMTEvents, 'Show multitouch events')
+ g_KbManager.bindKey('s', self.__screenshot, 'Take screenshot')
+
+ def _onStart(self):
+ try:
+ player.getVideoMemUsed()
+ g_KbManager.bindKey('v', self.showVideoMemoryUsage,
+ 'Show video memory usage graph')
+ except RuntimeError:
+ # Video memory query not supported.
+ pass
+
+ AppStarter._onStart(self)
+
+ def __dumpObjects(self):
+ gc.collect()
+ testHelper = player.getTestHelper()
+ testHelper.dumpObjects()
+ print 'Num anims: ', avg.getNumRunningAnims()
+ print 'Num python objects: ', len(gc.get_objects())
+
+ def showMemoryUsage(self):
+ if self.__memGraph:
+ self.__memGraph.unlink(True)
+ self.__graphs.remove(self.__memGraph)
+ self.__memGraph = None
+ else:
+ size = (self._appNode.width, self._appNode.height/6.0)
+ self.__memGraph = graph.AveragingGraph(title = 'Memory Usage',
+ getValue = avg.getMemoryUsage, parent=player.getRootNode(), size=size)
+ self.__graphs.append(self.__memGraph)
+ self.__positionGraphs()
+
+ def showVideoMemoryUsage(self):
+ if self.__vidMemGraph:
+ self.__vidMemGraph.unlink(True)
+ self.__graphs.remove(self.__vidMemGraph)
+ self.__vidMemGraph = None
+ else:
+ size = (self._appNode.width, self._appNode.height/6.0)
+ self.__vidMemGraph = graph.AveragingGraph(title='Video Memory Usage',
+ getValue=player.getVideoMemUsed, parent=player.getRootNode(),
+ size=size)
+ self.__graphs.append(self.__vidMemGraph)
+ self.__positionGraphs()
+
+ def showFrameRate(self):
+ if self.__frGraph:
+ self.__frGraph.unlink(True)
+ self.__graphs.remove(self.__frGraph)
+ self.__frGraph = None
+ else:
+ size = (self._appNode.width, self._appNode.height/6.0)
+ self.__frGraph = graph.SlidingGraph(title = 'Time per Frame',
+ getValue = player.getFrameTime, parent = self._appNode, size=size)
+ self.__graphs.append(self.__frGraph)
+ self.__positionGraphs()
+
+ def __positionGraphs(self):
+ ypos = 10
+ for gr in self.__graphs:
+ gr.y = ypos
+ ypos += gr.height + 10
+
+ def __switchMtemu(self):
+ if self._mtEmu is None:
+ self._mtEmu = MTemu()
+ g_KbManager.bindKey('left shift', self._mtEmu.toggleDualTouch,
+ 'Toggle Multitouch Emulation')
+ g_KbManager.bindKey('right shift', self._mtEmu.toggleDualTouch,
+ 'Toggle Multitouch Emulation')
+ g_KbManager.bindKey('left ctrl', self._mtEmu.toggleSource,
+ 'Toggle Touch Source')
+ g_KbManager.bindKey('right ctrl', self._mtEmu.toggleSource,
+ 'Toggle Touch Source')
+ else:
+ self._mtEmu.deinit()
+ g_KbManager.unbindKey('left ctrl')
+ g_KbManager.unbindKey('right ctrl')
+ g_KbManager.unbindKey('left shift')
+ g_KbManager.unbindKey('right shift')
+
+ del self._mtEmu
+ self._mtEmu = None
+
+ def __switchShowMTEvents(self):
+ if self.__debugTouchVisOverlay is None:
+ rootNode = player.getRootNode()
+ self.__debugTouchVisOverlay = apphelpers.TouchVisualizationOverlay(
+ isDebug=True, visClass=apphelpers.DebugTouchVisualization,
+ size=self._appNode.size, parent=rootNode)
+ else:
+ self.__debugTouchVisOverlay.unlink(True)
+ del self.__debugTouchVisOverlay
+ self.__debugTouchVisOverlay = None
+
+ def __killNotifyNode(self):
+ if self.__notifyNode:
+ self.__notifyNode.unlink()
+ self.__notifyNode = None
+
+ def __screenshot(self):
+ fnum = 0
+ fnameTemplate = 'screenshot-%03d.png'
+ while os.path.exists(fnameTemplate % fnum):
+ fnum += 1
+
+ try:
+ player.screenshot().save('screenshot-%03d.png' % fnum)
+ except RuntimeError:
+ text = 'Cannot save snapshot file'
+ else:
+ text = 'Screenshot saved as ' + fnameTemplate % fnum
+
+ self.__killNotifyNode()
+
+ self.__notifyNode = avg.WordsNode(
+ text=text, x=player.getRootNode().width - 50,
+ y=player.getRootNode().height - 50, alignment='right', fontsize=20,
+ sensitive=False, parent=player.getRootNode())
+
+ player.setTimeout(2000, self.__killNotifyNode)
+
+
+class AVGMTAppStarter(AVGAppStarter):
+
+ def __init__(self, *args, **kwargs):
+ self.__touchVisOverlay = None
+ super(AVGMTAppStarter, self).__init__(*args, **kwargs)
+
+ def setTouchVisualization(self, visClass):
+ if not(self.__touchVisOverlay is None):
+ self.__touchVisOverlay.unlink(True)
+ del self.__touchVisOverlay
+ self.__touchVisOverlay = None
+ if not(visClass is None):
+ rootNode = player.getRootNode()
+ self.__touchVisOverlay = apphelpers.TouchVisualizationOverlay(
+ isDebug=False, visClass=visClass, size=self._appNode.size,
+ parent=rootNode)
+
+ def toggleTrackerImage(self):
+ if self.__showTrackerImage:
+ self.hideTrackerImage()
+ else:
+ self.showTrackerImage()
+
+ def showTrackerImage(self):
+ if self.__showTrackerImage:
+ return
+ self.__showTrackerImage = True
+ self.__updateTrackerImageInterval = \
+ player.subscribe(player.ON_FRAME, self.__updateTrackerImage)
+ self.__trackerImageNode.opacity = 1
+ self.tracker.setDebugImages(False, True)
+
+ def hideTrackerImage(self):
+ if not self.__showTrackerImage:
+ return
+ self.__showTrackerImage = False
+ if self.__updateTrackerImageInterval:
+ player.clearInterval(self.__updateTrackerImageInterval)
+ self.__updateTrackerImageInterval = None
+ self.__trackerImageNode.opacity = 0
+ self.tracker.setDebugImages(False, False)
+
+ def __updateTrackerImage(self):
+ def transformPos((x,y)):
+ if self.trackerFlipX:
+ x = 1 - x
+ if self.trackerFlipY:
+ y = 1 - y
+ return (x, y)
+
+ fingerBitmap = self.tracker.getImage(avg.IMG_FINGERS)
+ node = self.__trackerImageNode
+ node.setBitmap(fingerBitmap)
+ node.pos = self.tracker.getDisplayROIPos()
+ node.size = self.tracker.getDisplayROISize()
+
+ grid = node.getOrigVertexCoords()
+ grid = [ [ transformPos(pos) for pos in line ] for line in grid]
+ node.setWarpedVertexCoords(grid)
+
+ def _onStart(self):
+ from camcalibrator import Calibrator
+
+ # we must add the tracker first, calibrator depends on it
+ try:
+ player.enableMultitouch()
+ except RuntimeError, err:
+ avg.logger.warning(str(err))
+
+ self.tracker = player.getTracker()
+
+ if self.tracker:
+ if Calibrator:
+ self.__calibratorNode = player.createNode('div',{
+ 'opacity': 0,
+ 'active': False,
+ })
+ rootNode = player.getRootNode()
+ rootNode.appendChild(self.__calibratorNode)
+ self.__calibratorNode.size = rootNode.size
+ self.__calibrator = Calibrator(self.__calibratorNode, appStarter=self)
+ self.__calibrator.setOnCalibrationSuccess(self.__onCalibrationSuccess)
+ self.__calibrator.init()
+ else:
+ self.__calibrator = None
+
+ self.__showTrackerImage = False
+ self.__updateTrackerImageInterval = None
+ self.__trackerImageNode = player.createNode('image', {'sensitive': False})
+ player.getRootNode().appendChild(self.__trackerImageNode)
+
+ self.__updateTrackerImageFixup()
+
+ g_KbManager.bindKey('h', self.tracker.resetHistory, 'RESET tracker history')
+ g_KbManager.bindKey('d', self.toggleTrackerImage, 'toggle tracker image')
+
+ if self.__calibrator:
+ g_KbManager.bindKey('c', self.__enterCalibrator, 'enter calibrator')
+ AVGAppStarter._onStart(self)
+
+ def __updateTrackerImageFixup(self):
+ # finger bitmap might need to be rotated/flipped
+ trackerAngle = float(self.tracker.getParam('/transform/angle/@value'))
+ angle = round(trackerAngle/math.pi) * math.pi
+ self.__trackerImageNode.angle = angle
+ self.trackerFlipX = (float(self.tracker.getParam('/transform/displayscale/@x'))
+ < 0)
+ self.trackerFlipY = (float(self.tracker.getParam('/transform/displayscale/@y'))
+ < 0)
+
+ def __onCalibrationSuccess(self):
+ self.__updateTrackerImageFixup()
+
+ def __enterCalibrator(self):
+
+ def leaveCalibrator():
+ g_KbManager.unbindKey('e')
+ self._activeApp = self._appInstance
+ self._appInstance.enter()
+ self.__calibrator.leave()
+ self._appNode.opacity = 1
+ self._appNode.active = True
+ self.__calibratorNode.opacity = 0
+ self.__calibratorNode.active = False
+
+ if self.__calibrator.isRunning():
+ print "calibrator already running!"
+ return
+
+ self._activeApp = self.__calibrator
+ self.__calibrator.enter()
+ g_KbManager.bindKey('e', leaveCalibrator, 'leave Calibrator')
+ self._appInstance.leave()
+ self.__calibratorNode.opacity = 1
+ self.__calibratorNode.active = True
+ self._appNode.opacity = 0
+ self._appNode.active = False
diff --git a/src/python/avgapp.py b/src/python/avgapp.py
new file mode 100644
index 0000000..5e3d4b3
--- /dev/null
+++ b/src/python/avgapp.py
@@ -0,0 +1,142 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is Martin Heistermann <mh at sponc dot de>
+#
+
+from appstarter import AppStarter
+
+
+class AVGApp(object):
+ _instances = {}
+ multitouch = False
+ fakeFullscreen = False
+
+ def __init__(self, parentNode):
+ '''
+ Initialization before Player.play()
+ Use this only when needed, e.g. for
+ WordsNode.addFontDir(). Do not forget to call
+ super(YourApp, self).__init__(parentNode)
+ '''
+
+ import warnings
+ warnings.warn('AVGApp is deprecated, use libavg.app.App instead')
+
+ appname = self.__class__.__name__
+ if appname in AVGApp._instances:
+ raise RuntimeError('App %s already setup' % appname)
+
+ AVGApp._instances[appname] = self
+
+ self.__isRunning = False
+ self._parentNode = parentNode
+ self._starter = None
+
+ if 'onKey' in dir(self):
+ raise DeprecationWarning, \
+ 'AVGApp.onKey() has been renamed to AVGApp.onKeyDown().'
+
+ @classmethod
+ def get(cls):
+ '''
+ Get the Application instance
+
+ Note: this class method has to be called from the top-level app class:
+
+ >>> class MyApp(libavg.AVGApp):
+ ... pass
+ >>> instance = MyApp.get()
+ '''
+ return cls._instances.get(cls.__name__, None)
+
+ @classmethod
+ def start(cls, **kwargs):
+ if cls.multitouch:
+ from appstarter import AVGMTAppStarter
+ starter = AVGMTAppStarter
+ else:
+ from appstarter import AVGAppStarter
+ starter = AVGAppStarter
+
+ starter(appClass=cls, fakeFullscreen=cls.fakeFullscreen, **kwargs)
+
+ def init(self):
+ """main initialization
+ build node hierarchy under self.__parentNode."""
+ pass
+
+ def exit(self):
+ """Deinitialization
+ Called after player.play() returns. End of program run."""
+ pass
+
+ def _enter(self):
+ """enter the application, internal interface.
+ override this and start all animations, intervals
+ etc. here"""
+ pass
+
+ def _leave(self):
+ """leave the application, internal interface.
+ override this and stop all animations, intervals
+ etc. Take care your application does not use any
+ non-needed resources after this."""
+ pass
+
+ def enter(self, onLeave = lambda: None):
+ """enter the application, external interface.
+ Do not override this."""
+ self.__isRunning = True
+ self._onLeave = onLeave
+ self._enter()
+
+ def leave(self):
+ """leave the application, external interface.
+ Do not override this."""
+ self.__isRunning = False
+ self._onLeave()
+ self._leave()
+
+ def onKeyDown(self, event):
+ """returns bool indicating if the event was handled
+ by the application """
+ return False
+
+ def onKeyUp(self, event):
+ """returns bool indicating if the event was handled
+ by the application """
+ return False
+
+ def isRunning(self):
+ return self.__isRunning
+
+ def setStarter(self, starter):
+ self._starter = starter
+
+ def getStarter(self):
+ return self._starter
+
+
+class App(object):
+ @classmethod
+ def start(cls, *args, **kargs):
+ raise RuntimeError('avgapp.App cannot be used any longer. Use libavg.AVGApp for '
+ 'a compatible class or switch to the new libavg.app.App')
+
diff --git a/src/python/camcalibrator.py b/src/python/camcalibrator.py
new file mode 100644
index 0000000..08bd308
--- /dev/null
+++ b/src/python/camcalibrator.py
@@ -0,0 +1,469 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is igor <igor (at) c-base (dot) org>
+#
+
+import sys, os
+from libavg import avg, AVGApp, player
+
+import coordcalibrator
+import apphelpers
+
+mediadir = os.path.join(os.path.dirname(__file__), 'data')
+g_KbManager = apphelpers.KeyboardManager.get()
+
+
+def camera_setup(CameraType):
+ if CameraType == "Fire-i":
+ paramList = [
+ {'Name':"Brightness",
+ 'path':"/camera/brightness/@value",
+ 'min':128, 'max':383, 'increment':1, 'precision':0},
+ {'Name':"Exposure",
+ 'path':"/camera/exposure/@value",
+ 'min':-1, 'max':511, 'increment':1, 'precision':0},
+ {'Name':"Shutter",
+ 'path':"/camera/shutter/@value",
+ 'min':0, 'max':7, 'increment':1, 'precision':0},
+ {'Name':"Gain",
+ 'path':"/camera/gain/@value",
+ 'min':0, 'max':255, 'increment':1, 'precision':0},
+ ]
+ elif CameraType == "FireFly":
+ paramList = [
+ {'Name':"Brightness",
+ 'path':"/camera/brightness/@value",
+ 'min':1, 'max':255, 'increment':1, 'precision':0},
+ {'Name':"Shutter",
+ 'path':"/camera/shutter/@value",
+ 'min':1, 'max':533, 'increment':1, 'precision':0},
+ {'Name':"Gain",
+ 'path':"/camera/gain/@value",
+ 'min':16, 'max':64, 'increment':1, 'precision':0},
+ {'Name':"Gamma",
+ 'path':"/camera/gamma/@value",
+ 'min':0, 'max':1, 'increment':1, 'precision':0},
+ ]
+ elif CameraType == "DragonFly":
+ paramList = [
+ {'Name':"Brightness",
+ 'path':"/camera/brightness/@value",
+ 'min':1, 'max':255, 'increment':1, 'precision':0},
+ {'Name':"Gamma",
+ 'path':"/camera/gamma/@value",
+ 'min':512, 'max':4095, 'increment':5, 'precision':0},
+ {'Name':"Shutter",
+ 'path':"/camera/shutter/@value",
+ 'min':0, 'max':709, 'increment':2, 'precision':0},
+ {'Name':"Gain",
+ 'path':"/camera/gain/@value",
+ 'min':16, 'max':683, 'increment':2, 'precision':0},
+ ]
+ else:
+ avg.logger.error("Unknown CameraType %s" % CameraType)
+ sys.exit()
+
+ paramList.extend([
+ # Touch
+ {'Name':"Threshold",
+ 'path':"/tracker/touch/threshold/@value",
+ 'min':1, 'max':255, 'increment':1, 'precision':0},
+ {'Name':"Similarity",
+ 'path':"/tracker/touch/similarity/@value",
+ 'min':1, 'max':300, 'increment':1, 'precision':1},
+
+ {'Name':"Min Area",
+ 'path':"/tracker/touch/areabounds/@min",
+ 'min':1, 'max':1000000, 'increment':3, 'precision':0},
+ {'Name':"Max Area",
+ 'path':"/tracker/touch/areabounds/@max",
+ 'min':20, 'max':1000000, 'increment':10, 'precision':0},
+
+ {'Name':"Ecc. Min",
+ 'path':"/tracker/touch/eccentricitybounds/@min",
+ 'min':1, 'max':30, 'increment':1, 'precision':1},
+ {'Name':"Ecc. Max",
+ 'path':"/tracker/touch/eccentricitybounds/@max",
+ 'min':1, 'max':2000, 'increment':1, 'precision':1},
+
+ {'Name':"Bandpass Min",
+ 'path':"/tracker/touch/bandpass/@min",
+ 'min':0, 'max':15, 'increment':.1, 'precision':1},
+ {'Name':"Bandpass Max",
+ 'path':"/tracker/touch/bandpass/@max",
+ 'min':0, 'max':15, 'increment':.1, 'precision':1},
+ {'Name':"Bandpass Postmult",
+ 'path':"/tracker/touch/bandpasspostmult/@value",
+ 'min':0, 'max':30, 'increment':.1, 'precision':1},
+
+ # Track
+ {'Name':"Threshold",
+ 'path':"/tracker/track/threshold/@value",
+ 'min':1, 'max':255, 'increment':1, 'precision':0},
+
+ {'Name':"Min Area",
+ 'path':"/tracker/track/areabounds/@min",
+ 'min':1, 'max':1000000, 'increment':3, 'precision':0},
+ {'Name':"Max Area",
+ 'path':"/tracker/track/areabounds/@max",
+ 'min':20, 'max':1000000, 'increment':10, 'precision':0},
+
+ {'Name':"Ecc. Min",
+ 'path':"/tracker/track/eccentricitybounds/@min",
+ 'min':1, 'max':30, 'increment':1, 'precision':1},
+ {'Name':"Ecc. Max",
+ 'path':"/tracker/track/eccentricitybounds/@max",
+ 'min':1, 'max':2000, 'increment':1, 'precision':1},
+
+ # Transform
+ {'Name':"p2",
+ 'path':"/transform/distortionparams/@p2",
+ 'min':-3, 'max':3, 'increment':0.001, 'precision':3},
+ {'Name':"Trapezoid",
+ 'path':"/transform/trapezoid/@value",
+ 'min':-3, 'max':3, 'increment':0.0001, 'precision':4},
+ {'Name':"Angle",
+ 'path':"/transform/angle/@value",
+ 'min':-3.15, 'max':3.15, 'increment':0.01, 'precision':2},
+ {'Name':"Displ. x",
+ 'path':"/transform/displaydisplacement/@x",
+ 'min':-5000, 'max':0, 'increment':1, 'precision':0},
+ {'Name':"Displ. y",
+ 'path':"/transform/displaydisplacement/@y",
+ 'min':-5000, 'max':0, 'increment':1, 'precision':0},
+ {'Name':"Scale x",
+ 'path':"/transform/displayscale/@x",
+ 'min':-3, 'max':8, 'increment':0.01, 'precision':2},
+ {'Name':"Scale y",
+ 'path':"/transform/displayscale/@y",
+ 'min':-3, 'max':8, 'increment':0.01, 'precision':2},
+ ])
+ return paramList
+
+class Calibrator(AVGApp):
+ def __init__(self, parentNode, CameraType = "FireFly", appStarter = None):
+ super(Calibrator, self).__init__(parentNode)
+ self.paramList = camera_setup(CameraType)
+ self.parentNode=parentNode
+ self.appStarter = appStarter
+ self.mainNode = player.createNode(
+ """
+ <div active="False" opacity="0">
+ <image width="1280" height="800" href="black.png"/>
+ <image id="cal_distorted" x="0" y="0" width="1280" height="800"
+ sensitive="false" opacity="1"/>
+ <words id="cal_fps" x="30" y="30" color="00FF00" text=""/>
+ <words id="cal_notification" x="390" y="390" width="500" fontsize="18"
+ color="ff3333" alignment="center" />
+ <div id="cal_gui" x="30" y="540">
+ <image id="cal_shadow" x="0" y="13" width="500" height="150"
+ href="black.png" opacity="0.6"/>
+
+ <words x="2" y="13" text="camera" fontsize="16" color="00FF00"/>
+ <image x="2" y="32" href="CamImgBorder.png"/>
+ <image id="cal_camera" x="4" y="34" width="160" height="120"/>
+
+ <words x="168" y="13" text="nohistory" fontsize="16" color="00FF00"/>
+ <image x="168" y="32" href="CamImgBorder.png"/>
+ <image id="cal_nohistory" x="170" y="34" width="160" height="120"/>
+
+ <words x="334" y="13" text="histogram" fontsize="16" color="00FF00"/>
+ <image x="334" y="32" href="CamImgBorder.png"/>
+ <image id="cal_histogram" x="336" y="34" width="160" height="120"/>
+
+ <div id="cal_params" y="170" opacity="0.9">
+ <image id="cal_shadow2" width="750" height="65" href="black.png" opacity="0.6"/>
+ <div id="cal_paramdiv0" x="2">
+ <words text="camera" y="0" fontsize="10" color="00ff00" />
+ <words id="cal_param0" y="12" fontsize="10"/>
+ <words id="cal_param1" y="24" fontsize="10"/>
+ <words id="cal_param2" y="36" fontsize="10"/>
+ <words id="cal_param3" y="48" fontsize="10"/>
+ </div>
+ <div id="cal_paramdiv1" x="80">
+ <words text="touch" y="0" fontsize="10" color="00ff00" />
+ <words id="cal_param4" y="12" fontsize="10"/>
+ <words id="cal_param5" y="24" fontsize="10"/>
+ <words id="cal_param6" y="36" fontsize="10"/>
+ <words id="cal_param7" y="48" fontsize="10"/>
+ </div>
+ <div id="cal_paramdiv2" x="200">
+ <words id="cal_param8" y="0" fontsize="10"/>
+ <words id="cal_param9" y="12" fontsize="10"/>
+ <words id="cal_param10" y="24" fontsize="10"/>
+ <words id="cal_param11" y="36" fontsize="10"/>
+ <words id="cal_param12" y="48" fontsize="10"/>
+ </div>
+ <div id="cal_paramdiv3" x="350">
+ <words text="track" y="0" fontsize="10" color="00ff00" />
+ <words id="cal_param13" y="12" fontsize="10"/>
+ <words id="cal_param14" y="24" fontsize="10"/>
+ <words id="cal_param15" y="36" fontsize="10"/>
+ <words id="cal_param16" y="48" fontsize="10"/>
+ </div>
+ <div id="cal_paramdiv4" x="500">
+ <words id="cal_param17" y="0" fontsize="10"/>
+ <words text="distort" y="12" fontsize="10" color="00ff00"/>
+ <words id="cal_param18" y="24" fontsize="10"/>
+ <words id="cal_param19" y="36" fontsize="10"/>
+ <words id="cal_param20" y="48" fontsize="10"/>
+ </div>
+ <div id="cal_paramdiv5" x="650">
+ <words id="cal_param21" y="0" fontsize="10"/>
+ <words id="cal_param22" y="12" fontsize="10"/>
+ <words id="cal_param23" y="24" fontsize="10"/>
+ <words id="cal_param24" y="36" fontsize="10"/>
+ <words id="cal_param25" y="48" fontsize="10"/>
+ </div>
+ </div>
+ </div>
+ <div id="cal_coordcalibrator" opacity="0" active="false">
+ <image x="0" y="0" width="1280" height="800" href="border.png"/>
+ <div id="cal_messages" x="100" y="100"/>
+ <image id="cal_crosshair" href="crosshair.png"/>
+ <image id="cal_feedback" href="Feedback.png"/>
+ </div>
+ </div>
+ """)
+ self.mainNode.mediadir=mediadir
+ parentNode.insertChild(self.mainNode, 0)
+
+ self.coordCal = None
+ self.tracker = player.getTracker()
+ self.curParam = 0
+ self.saveIndex = 0
+ self.hideMainNodeTimeout = None
+ self.video = []
+ self.__guiOpacity = 1
+ self.__showBigCamImage = False
+ self.__notificationTimer = None
+ self.__onCalibrationSuccess = None
+
+
+
+ def _enter(self):
+
+ g_KbManager.push()
+
+ g_KbManager.bindKey('d', self.__trackerSetDebugImages, 'tracker set debug images')
+ g_KbManager.bindKey('b', self.__bigCamImage, 'big cam image')
+ g_KbManager.bindKey('up', self.__keyFuncUP, 'select parameter up')
+ g_KbManager.bindKey('down', self.__keyFuncDOWN, 'select parameter down')
+ g_KbManager.bindKey('left', self.__keyFuncLEFT, 'value up')
+ g_KbManager.bindKey('right', self.__keyFuncRIGHT, 'value down')
+ g_KbManager.bindKey('page up', self.__keyFuncPAGEUp, 'value up * 10')
+ g_KbManager.bindKey('page down', self.__keyFuncPAGEDown, 'value down * 10')
+ g_KbManager.bindKey('s', self.__trackerSaveConfig, 'save configuration')
+ g_KbManager.bindKey('g', self.__toggleGUI, 'toggle GUI')
+ g_KbManager.bindKey('c', self.__startCoordCalibration,
+ 'start geometry calibration')
+ g_KbManager.bindKey('w', self.__saveTrackerIMG, 'SAVE trager image')
+ g_KbManager.bindKey('h', self.appStarter.tracker.resetHistory, 'RESET history')
+
+ self.appStarter.showTrackerImage()
+ self.mainNode.active=True
+ self.tracker.setDebugImages(True, True)
+ avg.fadeIn(self.mainNode, 400, 1)
+ Bitmap = self.tracker.getImage(avg.IMG_DISTORTED) # Why is this needed?
+ self.__onFrameID = player.subscribe(player.ON_FRAME, self.__onFrame)
+ #grandparent = self.parentNode.getParent()
+ #if grandparent:
+ # grandparent.reorderChild(grandparent.indexOf(self.parentNode), grandparent.getNumChildren()-1)
+ self.displayParams()
+ if self.hideMainNodeTimeout:
+ player.clearInterval(self.hideMainNodeTimeout)
+
+ def _leave(self):
+ #unbind all calibrator keys - bind old keys
+ g_KbManager.pop()
+
+ def hideMainNode():
+ self.mainNode.opacity=0
+ self.mainNode.active = False
+ self.appStarter.hideTrackerImage()
+ #grandparent = self.parentNode.getParent()
+ #if grandparent:
+ # grandparent.reorderChild(grandparent.indexOf(self.parentNode), 0)
+ self.hideMainNodeTimeout = player.setTimeout(400, hideMainNode)
+ player.clearInterval(self.__onFrameID)
+
+ def reparent(self, newParent):
+ """reparents the calibrator node; returns the old(!) parent node"""
+ oldParent = self.mainNode.getParent()
+ self.mainNode.unlink()
+ newParent.appendChild(self.mainNode)
+ return oldParent
+
+ def __deferredRefreshCB(self):
+ self.displayParams()
+ self.tracker.resetHistory()
+ self.setNotification('')
+ g_KbManager.pop()
+ player.getElementByID('cal_params').opacity = 0.9
+
+ def __clearNotification(self):
+ self.__notificationTimer = None
+ self.setNotification('')
+
+ def __toggleGUI(self):
+ self.__guiOpacity = 1 - self.__guiOpacity
+ player.getElementByID('cal_gui').opacity = self.__guiOpacity
+
+ def __onFrame(self):
+ def showTrackerImage(trackerImageID, nodeID, size, pos=(0,0)):
+ bitmap = self.tracker.getImage(trackerImageID)
+ node = player.getElementByID(nodeID)
+ node.setBitmap(bitmap)
+ node.size = size
+ if pos != (0,0):
+ node.pos = pos
+
+ # flip:
+ grid = node.getOrigVertexCoords()
+ grid = [ [ (1-pos[0], pos[1]) for pos in line ] for line in grid]
+ node.setWarpedVertexCoords(grid)
+
+ if self.__showBigCamImage:
+ showTrackerImage(avg.IMG_CAMERA, "cal_distorted", (1280, 960))
+ else:
+ pos = self.tracker.getDisplayROIPos()
+ size = self.tracker.getDisplayROISize()
+ showTrackerImage(avg.IMG_DISTORTED, "cal_distorted", pos = pos, size = size)
+ showTrackerImage(avg.IMG_CAMERA, "cal_camera", (160, 120))
+ showTrackerImage(avg.IMG_NOHISTORY, "cal_nohistory", (160, 120))
+ showTrackerImage(avg.IMG_HISTOGRAM, "cal_histogram", (160, 120))
+ fps = player.getEffectiveFramerate()
+ player.getElementByID("cal_fps").text = '%(val).2f' % {'val': fps}
+
+ def __trackerSetDebugImages(self):
+ self.appStarter.toggleTrackerImage()
+ # toggleTrackerImage() will influence setDebugImages status, so we have to reset it:
+ self.tracker.setDebugImages(True, True)
+
+ def __bigCamImage(self):
+ self.__showBigCamImage = not(self.__showBigCamImage)
+
+ def __keyFuncUP(self):
+ if self.curParam > 0:
+ self.curParam -= 1
+ self.displayParams()
+
+ def __keyFuncDOWN(self):
+ if self.curParam < len(self.paramList)-1:
+ self.curParam += 1
+ self.displayParams()
+
+ def __keyFuncLEFT(self):
+ self.changeParam(-1)
+ self.displayParams()
+
+ def __keyFuncRIGHT(self):
+ self.changeParam(1)
+ self.displayParams()
+
+ def __keyFuncPAGEUp(self):
+ self.changeParam(10)
+ self.displayParams()
+
+ def __keyFuncPAGEDown(self):
+ self.changeParam(-10)
+ self.displayParams()
+
+ def __trackerSaveConfig(self):
+ self.tracker.saveConfig()
+ self.setNotification('Tracker configuration saved', 2000)
+ avg.logger.info("Tracker configuration saved.")
+
+ def __saveTrackerIMG(self):
+ def saveTrackerImage(id, name):
+ self.tracker.getImage(id).save("img"+str(self.saveIndex)+"_"+name+".png")
+
+ self.saveIndex += 1
+ saveTrackerImage(avg.IMG_CAMERA, "camera")
+ saveTrackerImage(avg.IMG_DISTORTED, "distorted")
+ saveTrackerImage(avg.IMG_NOHISTORY, "nohistory")
+ saveTrackerImage(avg.IMG_HIGHPASS, "highpass")
+ saveTrackerImage(avg.IMG_FINGERS, "fingers")
+ saveTrackerImage(avg.IMG_HISTOGRAM, "histogram")
+ self.setNotification('Tracker images dumped', 2000)
+ avg.logger.info("Tracker images saved.")
+
+ def __startCoordCalibration(self):
+ assert(not self.coordCal)
+
+ self.__savedShutter = self.tracker.getParam("/camera/shutter/@value")
+ self.tracker.setParam("/camera/shutter/@value", "8")
+ self.__savedGain = self.tracker.getParam("/camera/gain/@value")
+ self.tracker.setParam("/camera/gain/@value", "16")
+ self.__savedStrobe = self.tracker.getParam("/camera/strobeduration/@value")
+ self.tracker.setParam("/camera/strobeduration/@value", "-1")
+ self.coordCal = coordcalibrator.CoordCalibrator(self.__onCalibrationTerminated)
+
+ def __onCalibrationTerminated(self, isSuccessful):
+ self.coordCal = None
+ self.tracker.setParam("/camera/shutter/@value", self.__savedShutter)
+ self.tracker.setParam("/camera/gain/@value", self.__savedGain)
+ self.tracker.setParam("/camera/strobeduration/@value", self.__savedStrobe)
+ self.deferredRefresh()
+
+ def setOnCalibrationSuccess(self, callback):
+ self.__onCalibrationSuccess = callback
+
+ def deferredRefresh(self):
+ player.setTimeout(1500, self.__deferredRefreshCB)
+ self.setNotification('Please wait for settlement')
+ g_KbManager.push()
+ player.getElementByID('cal_params').opacity = 0.3
+
+ def setNotification(self, text, timeout=0):
+ player.getElementByID('cal_notification').text = text
+ if timeout:
+ if self.__notificationTimer is not None:
+ player.clearInterval(self.__notificationTimer)
+
+ self.__notificationTimer = player.setTimeout(timeout,
+ self.__clearNotification)
+
+ def displayParams(self):
+ i = 0
+ for Param in self.paramList:
+ Node = player.getElementByID("cal_param"+str(i))
+ Path = Param['path']
+ Val = float(self.tracker.getParam(Path))
+ Node.text = (Param['Name']+": "
+ +('%(val).'+str(Param['precision'])+'f') % {'val': Val})
+ if self.curParam == i:
+ Node.color = "FFFFFF"
+ else:
+ Node.color = "A0A0FF"
+ i += 1
+
+ def changeParam(self, Change):
+ param = self.paramList[self.curParam]
+ if param['increment'] >= 1:
+ Val = int(float(self.tracker.getParam(param['path'])))
+ else:
+ Val = float(self.tracker.getParam(param['path']))
+ Val += Change*param['increment']
+ if Val < param['min']:
+ Val = param['min']
+ if Val > param['max']:
+ Val = param['max']
+ self.tracker.setParam(param['path'], str(Val))
diff --git a/src/python/coordcalibrator.py b/src/python/coordcalibrator.py
new file mode 100644
index 0000000..c3dad01
--- /dev/null
+++ b/src/python/coordcalibrator.py
@@ -0,0 +1,133 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+from libavg import avg, player
+
+import apphelpers
+
+g_KbManager = apphelpers.KeyboardManager.get()
+
+
+class CoordCalibrator(object):
+ def __init__(self, calibrationTerminatedCb):
+ self.__calibrationTerminatedCb = calibrationTerminatedCb
+ self.__CurPointIndex = 0
+ self.__CPPCal = player.getTracker().startCalibration()
+ self.__LastCenter = None
+ self.__NumMessages = 0
+ self._mycursor = None
+ mainNode = player.getElementByID("cal_coordcalibrator")
+ mainNode.active = True
+ mainNode.opacity = 1
+ mainNode.setEventHandler(avg.Event.CURSOR_DOWN, avg.Event.TOUCH,
+ self.__onTouchDown)
+ mainNode.setEventHandler(avg.Event.CURSOR_MOTION, avg.Event.TOUCH,
+ self.__onTouchMove)
+ mainNode.setEventHandler(avg.Event.CURSOR_UP, avg.Event.TOUCH, self.__onTouchUp)
+ self.__crosshair = player.getElementByID("cal_crosshair")
+ self.__feedback = player.getElementByID("cal_feedback")
+ self.__feedback.opacity = 0
+ self.__addMessage("Starting calibration.")
+ self.__moveMarker()
+
+ g_KbManager.push()
+ g_KbManager.bindKey('space', self.__nextPoint, 'sample next point')
+ g_KbManager.bindKey('a', self.__abortCalibration, 'abort calibration')
+
+ def __endCalibration(self, isSuccessful):
+ player.getElementByID("cal_coordcalibrator").active = False
+ player.getElementByID("cal_coordcalibrator").opacity = 0
+ MsgsNode = player.getElementByID("cal_messages")
+ for i in range(0, MsgsNode.getNumChildren()):
+ MsgsNode.removeChild(0)
+
+ g_KbManager.pop()
+ self.__calibrationTerminatedCb(isSuccessful)
+
+ def __nextPoint(self):
+ if self.__LastCenter:
+ self.__CPPCal.setCamPoint(self.__LastCenter)
+ self.__addMessage (" Using: %(x).2f, %(y).2f" %
+ { "x": self.__LastCenter[0], "y": self.__LastCenter[1]})
+ self._mycursor = None
+ self.__LastCenter = None
+
+ hasNextPoint = self.__CPPCal.nextPoint()
+
+ if not hasNextPoint:
+ # Note: may raise RuntimeError. A rollback doesn't appear to be possible,
+ # which means crashing here is safer than handling the exception
+ player.getTracker().endCalibration()
+ self.__endCalibration(True)
+ else:
+ self.__CurPointIndex += 1
+ self.__moveMarker()
+
+ def __abortCalibration(self):
+ player.getTracker().abortCalibration()
+ self.__endCalibration(False)
+
+ def __moveMarker(self):
+ self.__crosshair.x, self.__crosshair.y = self.__CPPCal.getDisplayPoint()
+ self.__crosshair.x, self.__crosshair.y = self.__feedback.x, self.__feedback.y = \
+ (self.__crosshair.x-7, self.__crosshair.y-7)
+ self.__addMessage("Calibrating point "+str(self.__CurPointIndex))
+
+ def __addMessage(self, text):
+ MsgsNode = player.getElementByID("cal_messages")
+ if self.__NumMessages > 38:
+ for i in range(0, MsgsNode.getNumChildren()-1):
+ MsgsNode.getChild(i).text = MsgsNode.getChild(i+1).text
+ MsgsNode.removeChild(MsgsNode.getNumChildren()-1)
+ else:
+ self.__NumMessages += 1
+ Node = player.createNode(
+ "<words fontsize='10' font='Eurostile' color='00FF00'/>")
+ Node.x = 0
+ Node.y = self.__NumMessages*13
+ Node.text = text
+ MsgsNode.appendChild(Node)
+
+ def __onTouchDown(self, Event):
+ if Event.source != avg.Event.TOUCH:
+ return
+ if not self._mycursor:
+ self._mycursor = Event.cursorid
+ else:
+ return
+ self.__LastCenter = Event.center
+ self.__addMessage(" Touch at %(x).2f, %(y).2f" % {
+ "x": Event.center[0], "y": Event.center[1]})
+ self.__feedback.opacity = 1
+
+ def __onTouchMove(self,Event):
+ if Event.source != avg.Event.TOUCH:
+ return
+ if self._mycursor == Event.cursorid:
+ self.__LastCenter = Event.center
+
+ def __onTouchUp(self, Event):
+ if Event.source != avg.Event.TOUCH:
+ return
+ self.__addMessage("touchup")
+ self.__feedback.opacity = 0
+ if self._mycursor:
+ self._mycursor = None
+ else:
+ return
diff --git a/src/python/data/CamImgBorder.png b/src/python/data/CamImgBorder.png
new file mode 100644
index 0000000..4bc1a37
--- /dev/null
+++ b/src/python/data/CamImgBorder.png
Binary files differ
diff --git a/src/python/data/Feedback.png b/src/python/data/Feedback.png
new file mode 100644
index 0000000..3c71f9c
--- /dev/null
+++ b/src/python/data/Feedback.png
Binary files differ
diff --git a/src/python/data/Makefile.am b/src/python/data/Makefile.am
new file mode 100644
index 0000000..02a416a
--- /dev/null
+++ b/src/python/data/Makefile.am
@@ -0,0 +1,4 @@
+EXTRA_DIST = $(wildcard *.xml) $(wildcard *.xsd) $(wildcard *.png) $(wildcard *.avg) \
+ $(wildcard *.mpg) $(wildcard *.avi) $(wildcard *.mov)
+datadir = $(pkgpyexecdir)/data
+data_DATA = $(EXTRA_DIST)
diff --git a/src/python/data/SimpleSkin.xml b/src/python/data/SimpleSkin.xml
new file mode 100644
index 0000000..3f21274
--- /dev/null
+++ b/src/python/data/SimpleSkin.xml
@@ -0,0 +1,83 @@
+<skin>
+ <fontdef id="stdFont" font="Bitstream Vera Sans" variant="Roman" fontsize="12"
+ color="000000" letterspacing="0" linespacing="-1"/>
+ <fontdef id="downFont" baseid="stdFont" color="CCCCCC"/>
+ <fontdef id="disabledFont" baseid="stdFont" color="444444"/>
+ <textbutton
+ upSrc="button_bg_up.png"
+ downSrc="button_bg_down.png"
+ font="stdFont"
+ downFont="downFont"
+ disabledFont="disabledFont"
+ endsExtent="(7,7)"/>
+ <slider>
+ <horizontal
+ trackSrc="slider_horiz_track.png"
+ trackDisabledSrc="slider_horiz_track_disabled.png"
+ trackEndsExtent="6"
+ thumbUpSrc="slider_thumb_up.png"
+ thumbDownSrc="slider_thumb_down.png"/>
+ <vertical
+ trackSrc="slider_vert_track.png"
+ trackDisabledSrc="slider_vert_track_disabled.png"
+ trackEndsExtent="6"
+ thumbUpSrc="slider_thumb_up.png"
+ thumbDownSrc="slider_thumb_down.png"/>
+ </slider>
+ <scrollbar>
+ <horizontal
+ trackSrc="scrollbar_horiz_track.png"
+ trackDisabledSrc="scrollbar_horiz_track_disabled.png"
+ trackEndsExtent="2"
+ thumbUpSrc="scrollbar_horiz_thumb_up.png"
+ thumbDownSrc="scrollbar_horiz_thumb_down.png"
+ thumbEndsExtent="4"/>
+ <vertical
+ trackSrc="scrollbar_vert_track.png"
+ trackDisabledSrc="scrollbar_vert_track_disabled.png"
+ trackEndsExtent="2"
+ thumbUpSrc="scrollbar_vert_thumb_up.png"
+ thumbDownSrc="scrollbar_vert_thumb_down.png"
+ thumbEndsExtent="4"/>
+ </scrollbar>
+ <progressbar>
+ <horizontal
+ trackSrc="scrollbar_horiz_track.png"
+ trackEndsExtent="2"
+ thumbUpSrc="scrollbar_horiz_thumb_up.png"
+ thumbDisabledSrc="scrollbar_vert_thumb_down.png"
+ thumbEndsExtent="4"/>
+ <vertical
+ trackSrc="scrollbar_vert_track.png"
+ trackEndsExtent="2"
+ thumbUpSrc="scrollbar_vert_thumb_up.png"
+ thumbDisabledSrc="scrollbar_vert_thumb_down.png"
+ thumbEndsExtent="4"/>
+ </progressbar>
+ <scrollarea
+ borderSrc="scrollarea_border.png"
+ borderEndsExtent="(8,8)"
+ margins="(1,1,8,8)"
+ friction="-1"
+ sensitiveScrollBars="True"/>
+ <checkbox
+ uncheckedUpSrc="checkbox_unchecked_up.png"
+ uncheckedDownSrc="checkbox_unchecked_down.png"
+ uncheckedDisabledSrc="checkbox_unchecked_disabled.png"
+ checkedUpSrc="checkbox_checked_up.png"
+ checkedDownSrc="checkbox_checked_down.png"
+ checkedDisabledSrc="checkbox_checked_disabled.png"
+ font="stdFont"
+ downFont="stdFont"
+ disabledFont="disabledFont"/>
+ <mediacontrol
+ playUpSrc="play_button_up.png"
+ playDownSrc="play_button_down.png"
+ pauseUpSrc="pause_button_up.png"
+ pauseDownSrc="pause_button_down.png"
+ font="stdFont"
+ timePos="(15,0)"
+ timeLeftPos="(-42,0)"
+ barPos="(55,2)"
+ barRight="-45"/>
+</skin>
diff --git a/src/python/data/TouchFeedback.png b/src/python/data/TouchFeedback.png
new file mode 100644
index 0000000..9f5dcda
--- /dev/null
+++ b/src/python/data/TouchFeedback.png
Binary files differ
diff --git a/src/python/data/black.png b/src/python/data/black.png
new file mode 100644
index 0000000..23bd512
--- /dev/null
+++ b/src/python/data/black.png
Binary files differ
diff --git a/src/python/data/border.png b/src/python/data/border.png
new file mode 100644
index 0000000..447a3e6
--- /dev/null
+++ b/src/python/data/border.png
Binary files differ
diff --git a/src/python/data/button_bg_down.png b/src/python/data/button_bg_down.png
new file mode 100644
index 0000000..c44179b
--- /dev/null
+++ b/src/python/data/button_bg_down.png
Binary files differ
diff --git a/src/python/data/button_bg_up.png b/src/python/data/button_bg_up.png
new file mode 100644
index 0000000..475c9c9
--- /dev/null
+++ b/src/python/data/button_bg_up.png
Binary files differ
diff --git a/src/python/data/checkbox_checked_disabled.png b/src/python/data/checkbox_checked_disabled.png
new file mode 100644
index 0000000..da58829
--- /dev/null
+++ b/src/python/data/checkbox_checked_disabled.png
Binary files differ
diff --git a/src/python/data/checkbox_checked_down.png b/src/python/data/checkbox_checked_down.png
new file mode 100644
index 0000000..4fbbd83
--- /dev/null
+++ b/src/python/data/checkbox_checked_down.png
Binary files differ
diff --git a/src/python/data/checkbox_checked_up.png b/src/python/data/checkbox_checked_up.png
new file mode 100644
index 0000000..ca901f4
--- /dev/null
+++ b/src/python/data/checkbox_checked_up.png
Binary files differ
diff --git a/src/python/data/checkbox_unchecked_disabled.png b/src/python/data/checkbox_unchecked_disabled.png
new file mode 100644
index 0000000..e8c2116
--- /dev/null
+++ b/src/python/data/checkbox_unchecked_disabled.png
Binary files differ
diff --git a/src/python/data/checkbox_unchecked_down.png b/src/python/data/checkbox_unchecked_down.png
new file mode 100644
index 0000000..69f8282
--- /dev/null
+++ b/src/python/data/checkbox_unchecked_down.png
Binary files differ
diff --git a/src/python/data/checkbox_unchecked_up.png b/src/python/data/checkbox_unchecked_up.png
new file mode 100644
index 0000000..e354492
--- /dev/null
+++ b/src/python/data/checkbox_unchecked_up.png
Binary files differ
diff --git a/src/python/data/crosshair.png b/src/python/data/crosshair.png
new file mode 100644
index 0000000..be025b1
--- /dev/null
+++ b/src/python/data/crosshair.png
Binary files differ
diff --git a/src/python/data/mpeg1-48x48-sound.avi b/src/python/data/mpeg1-48x48-sound.avi
new file mode 100644
index 0000000..be415db
--- /dev/null
+++ b/src/python/data/mpeg1-48x48-sound.avi
Binary files differ
diff --git a/src/python/data/mpeg1-48x48.mov b/src/python/data/mpeg1-48x48.mov
new file mode 100644
index 0000000..16ab499
--- /dev/null
+++ b/src/python/data/mpeg1-48x48.mov
Binary files differ
diff --git a/src/python/data/pause_button_down.png b/src/python/data/pause_button_down.png
new file mode 100644
index 0000000..c06b34a
--- /dev/null
+++ b/src/python/data/pause_button_down.png
Binary files differ
diff --git a/src/python/data/pause_button_up.png b/src/python/data/pause_button_up.png
new file mode 100644
index 0000000..1a923b9
--- /dev/null
+++ b/src/python/data/pause_button_up.png
Binary files differ
diff --git a/src/python/data/play_button_down.png b/src/python/data/play_button_down.png
new file mode 100644
index 0000000..07b167b
--- /dev/null
+++ b/src/python/data/play_button_down.png
Binary files differ
diff --git a/src/python/data/play_button_up.png b/src/python/data/play_button_up.png
new file mode 100644
index 0000000..e16ec9e
--- /dev/null
+++ b/src/python/data/play_button_up.png
Binary files differ
diff --git a/src/python/data/rgb24alpha-64x64.png b/src/python/data/rgb24alpha-64x64.png
new file mode 100644
index 0000000..41b69c3
--- /dev/null
+++ b/src/python/data/rgb24alpha-64x64.png
Binary files differ
diff --git a/src/python/data/scrollarea_border.png b/src/python/data/scrollarea_border.png
new file mode 100644
index 0000000..2e95f57
--- /dev/null
+++ b/src/python/data/scrollarea_border.png
Binary files differ
diff --git a/src/python/data/scrollbar_horiz_thumb_down.png b/src/python/data/scrollbar_horiz_thumb_down.png
new file mode 100644
index 0000000..6dc6fd1
--- /dev/null
+++ b/src/python/data/scrollbar_horiz_thumb_down.png
Binary files differ
diff --git a/src/python/data/scrollbar_horiz_thumb_up.png b/src/python/data/scrollbar_horiz_thumb_up.png
new file mode 100644
index 0000000..02748f2
--- /dev/null
+++ b/src/python/data/scrollbar_horiz_thumb_up.png
Binary files differ
diff --git a/src/python/data/scrollbar_horiz_track.png b/src/python/data/scrollbar_horiz_track.png
new file mode 100644
index 0000000..052002a
--- /dev/null
+++ b/src/python/data/scrollbar_horiz_track.png
Binary files differ
diff --git a/src/python/data/scrollbar_horiz_track_disabled.png b/src/python/data/scrollbar_horiz_track_disabled.png
new file mode 100644
index 0000000..7ac86fe
--- /dev/null
+++ b/src/python/data/scrollbar_horiz_track_disabled.png
Binary files differ
diff --git a/src/python/data/scrollbar_vert_thumb_down.png b/src/python/data/scrollbar_vert_thumb_down.png
new file mode 100644
index 0000000..c7b09d1
--- /dev/null
+++ b/src/python/data/scrollbar_vert_thumb_down.png
Binary files differ
diff --git a/src/python/data/scrollbar_vert_thumb_up.png b/src/python/data/scrollbar_vert_thumb_up.png
new file mode 100644
index 0000000..f6c2f88
--- /dev/null
+++ b/src/python/data/scrollbar_vert_thumb_up.png
Binary files differ
diff --git a/src/python/data/scrollbar_vert_track.png b/src/python/data/scrollbar_vert_track.png
new file mode 100644
index 0000000..58af284
--- /dev/null
+++ b/src/python/data/scrollbar_vert_track.png
Binary files differ
diff --git a/src/python/data/scrollbar_vert_track_disabled.png b/src/python/data/scrollbar_vert_track_disabled.png
new file mode 100644
index 0000000..695b112
--- /dev/null
+++ b/src/python/data/scrollbar_vert_track_disabled.png
Binary files differ
diff --git a/src/python/data/skin.xsd b/src/python/data/skin.xsd
new file mode 100644
index 0000000..4b8e545
--- /dev/null
+++ b/src/python/data/skin.xsd
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <xsd:complexType name="SliderDef">
+ <xsd:attribute name="trackSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="trackDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="trackEndsExtent" type="xsd:integer"/>
+ <xsd:attribute name="thumbUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="thumbDownSrc" type="xsd:string"/>
+ <xsd:attribute name="thumbDisabledSrc" type="xsd:string"/>
+ </xsd:complexType>
+ <xsd:complexType name="ScrollBarDef">
+ <xsd:complexContent>
+ <xsd:extension base="SliderDef">
+ <xsd:attribute name="thumbEndsExtent" type="xsd:integer"/>
+ </xsd:extension>
+ </xsd:complexContent>
+ </xsd:complexType>
+ <xsd:complexType name="ProgressBarDef">
+ <xsd:attribute name="trackSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="trackEndsExtent" type="xsd:integer"/>
+ <xsd:attribute name="thumbUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="thumbDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="thumbEndsExtent" type="xsd:integer"/>
+ </xsd:complexType>
+ <xsd:simpleType name="bool">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="True"/>
+ <xsd:enumeration value="False"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:element name="skin">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded">
+ <xsd:element name="fontdef" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="id" type="xsd:ID" use="required"/>
+ <xsd:attribute name="baseid" type="xsd:IDREF"/>
+ <xsd:attribute name="font" type="xsd:string"/>
+ <xsd:attribute name="variant" type="xsd:string"/>
+ <xsd:attribute name="color" type="xsd:string"/>
+ <xsd:attribute name="aagamma" type="xsd:decimal"/>
+ <xsd:attribute name="fontsize" type="xsd:decimal"/>
+ <xsd:attribute name="indent" type="xsd:integer"/>
+ <xsd:attribute name="linespacing" type="xsd:decimal"/>
+ <xsd:attribute name="alignment" type="xsd:string"/>
+ <xsd:attribute name="wrapmode" type="xsd:string"/>
+ <xsd:attribute name="justify" type="bool"/>
+ <xsd:attribute name="letterspacing" type="xsd:decimal"/>
+ <xsd:attribute name="hint" type="bool"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="textbutton" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="id" type="xsd:ID"/>
+ <xsd:attribute name="upSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="downSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="disabledSrc" type="xsd:string"/>
+ <xsd:attribute name="endsExtent" type="xsd:string" use="required"/>
+ <xsd:attribute name="font" type="xsd:IDREF" use="required"/>
+ <xsd:attribute name="downFont" type="xsd:IDREF"/>
+ <xsd:attribute name="disabledFont" type="xsd:IDREF"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="checkbox" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="id" type="xsd:ID"/>
+ <xsd:attribute name="checkedUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="checkedDownSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="checkedDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="uncheckedUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="uncheckedDownSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="uncheckedDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="font" type="xsd:IDREF" use="required"/>
+ <xsd:attribute name="downFont" type="xsd:IDREF"/>
+ <xsd:attribute name="disabledFont" type="xsd:IDREF"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="mediacontrol" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="id" type="xsd:ID"/>
+ <xsd:attribute name="playUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="playDownSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="playDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="pauseUpSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="pauseDownSrc" type="xsd:string" use="required"/>
+ <xsd:attribute name="pauseDisabledSrc" type="xsd:string"/>
+ <xsd:attribute name="font" type="xsd:IDREF" use="required"/>
+ <xsd:attribute name="timePos" type="xsd:string" use="required"/>
+ <xsd:attribute name="timeLeftPos" type="xsd:string" use="required"/>
+ <xsd:attribute name="barPos" type="xsd:string" use="required"/>
+ <xsd:attribute name="barRight" type="xsd:string" use="required"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="slider" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:all>
+ <xsd:element name="vertical" type="SliderDef" minOccurs="0" maxOccurs="1"/>
+ <xsd:element name="horizontal" type="SliderDef" minOccurs="0"
+ maxOccurs="1"/>
+ </xsd:all>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="scrollbar" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:all>
+ <xsd:element name="vertical" type="ScrollBarDef" minOccurs="0"
+ maxOccurs="1"/>
+ <xsd:element name="horizontal" type="ScrollBarDef" minOccurs="1"
+ maxOccurs="1"/>
+ </xsd:all>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="progressbar" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:all>
+ <xsd:element name="vertical" type="ProgressBarDef" minOccurs="0"
+ maxOccurs="1"/>
+ <xsd:element name="horizontal" type="ProgressBarDef" minOccurs="0"
+ maxOccurs="1"/>
+ </xsd:all>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="scrollarea" minOccurs="0" maxOccurs="unbounded">
+ <xsd:complexType>
+ <xsd:attribute name="id" type="xsd:ID"/>
+ <xsd:attribute name="scrollBarID" type="xsd:IDREF"/>
+ <xsd:attribute name="borderSrc" type="xsd:string"/>
+ <xsd:attribute name="borderEndsExtent" type="xsd:string"/>
+ <xsd:attribute name="margins" type="xsd:string"/>
+ <xsd:attribute name="friction" type="xsd:string"/>
+ <xsd:attribute name="sensitiveScrollBars" type="bool" use="required"/>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+</xsd:schema>
diff --git a/src/python/data/slider_horiz_track.png b/src/python/data/slider_horiz_track.png
new file mode 100644
index 0000000..6e233a5
--- /dev/null
+++ b/src/python/data/slider_horiz_track.png
Binary files differ
diff --git a/src/python/data/slider_horiz_track_disabled.png b/src/python/data/slider_horiz_track_disabled.png
new file mode 100644
index 0000000..01c880d
--- /dev/null
+++ b/src/python/data/slider_horiz_track_disabled.png
Binary files differ
diff --git a/src/python/data/slider_thumb_down.png b/src/python/data/slider_thumb_down.png
new file mode 100644
index 0000000..1f2acc2
--- /dev/null
+++ b/src/python/data/slider_thumb_down.png
Binary files differ
diff --git a/src/python/data/slider_thumb_up.png b/src/python/data/slider_thumb_up.png
new file mode 100644
index 0000000..885506d
--- /dev/null
+++ b/src/python/data/slider_thumb_up.png
Binary files differ
diff --git a/src/python/data/slider_vert_track.png b/src/python/data/slider_vert_track.png
new file mode 100644
index 0000000..51bac37
--- /dev/null
+++ b/src/python/data/slider_vert_track.png
Binary files differ
diff --git a/src/python/data/slider_vert_track_disabled.png b/src/python/data/slider_vert_track_disabled.png
new file mode 100644
index 0000000..bc8d7b6
--- /dev/null
+++ b/src/python/data/slider_vert_track_disabled.png
Binary files differ
diff --git a/src/python/enumcompat.py b/src/python/enumcompat.py
new file mode 100644
index 0000000..df859a5
--- /dev/null
+++ b/src/python/enumcompat.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+import avg
+avg.KEYUP = avg.Event.KEY_UP
+avg.KEYDOWN = avg.Event.KEY_DOWN
+avg.CURSORMOTION = avg.Event.CURSOR_MOTION
+avg.CURSORUP = avg.Event.CURSOR_UP
+avg.CURSORDOWN = avg.Event.CURSOR_DOWN
+avg.CURSOROVER = avg.Event.CURSOR_OVER
+avg.CURSOROUT = avg.Event.CURSOR_OUT
+avg.CUSTOMEVENT = avg.Event.CUSTOM_EVENT
+
+avg.MOUSE = avg.Event.MOUSE
+avg.TOUCH = avg.Event.TOUCH
+avg.TRACK = avg.Event.TRACK
+avg.CUSTOM = avg.Event.CUSTOM
+avg.NONE = avg.Event.NONE
+
diff --git a/src/python/filter.py b/src/python/filter.py
new file mode 100644
index 0000000..f6c268b
--- /dev/null
+++ b/src/python/filter.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+import math
+
+# Input filter based on:
+# Casiez, G., Roussel, N. and Vogel, D. (2012). 1€ Filter: A Simple Speed-based Low-pass
+# Filter for Noisy Input in Interactive Systems. Proceedings of the ACM Conference on
+# Human Factors in Computing Systems (CHI '12). Austin, Texas (May 5-12, 2012). New York:
+# ACM Press, pp. 2527-2530.
+
+class LowPassFilter(object):
+
+ def __init__(self, alpha):
+ self.__setAlpha(alpha)
+ self.__y = None
+ self.__s = None
+
+ def __setAlpha(self, alpha):
+ alpha = float(alpha)
+ if alpha <= 0 or alpha > 1.0:
+ raise RuntimeError("LowPassFilter alpha (%s) should be in (0.0, 1.0]"%alpha)
+ self.__alpha = alpha
+
+ def apply(self, value, timestamp=None, alpha=None):
+ if alpha is not None:
+ self.__setAlpha(alpha)
+ if self.__y is None:
+ s = value
+ else:
+ s = self.__alpha*value + (1.0-self.__alpha)*self.__s
+ self.__y = value
+ self.__s = s
+ return s
+
+ def lastValue(self):
+ return self.__y
+
+
+class OneEuroFilter(object):
+
+ def __init__(self, mincutoff=1.0, beta=0.0, dcutoff=1.0):
+ if mincutoff<=0:
+ raise ValueError("mincutoff should be >0")
+ if dcutoff<=0:
+ raise ValueError("dcutoff should be >0")
+ self.__freq = 60 # Initial freq, updated as soon as we have > 1 sample
+ self.__mincutoff = float(mincutoff)
+ self.__beta = float(beta)
+ self.__dcutoff = float(dcutoff)
+ self.__x = LowPassFilter(self.__alpha(self.__mincutoff))
+ self.__dx = LowPassFilter(self.__alpha(self.__dcutoff))
+ self.__lasttime = None
+
+ def __alpha(self, cutoff):
+ te = 1.0 / self.__freq
+ tau = 1.0 / (2*math.pi*cutoff)
+ return 1.0 / (1.0 + tau/te)
+
+ def apply(self, x, timestamp):
+ timestamp /= 1000.
+ if self.__lasttime == timestamp:
+ return x
+ else:
+ # ---- update the sampling frequency based on timestamps
+ if self.__lasttime and timestamp:
+ self.__freq = 1.0 / (timestamp-self.__lasttime)
+ self.__lasttime = timestamp
+ # ---- estimate the current variation per second
+ prev_x = self.__x.lastValue()
+ dx = 0.0 if prev_x is None else (x-prev_x)*self.__freq # FIXME: 0.0 or value?
+ edx = self.__dx.apply(dx, timestamp, alpha=self.__alpha(self.__dcutoff))
+ # ---- use it to update the cutoff frequency
+ cutoff = self.__mincutoff + self.__beta*math.fabs(edx)
+ # ---- filter the given value
+ return self.__x.apply(x, timestamp, alpha=self.__alpha(cutoff))
+
diff --git a/src/python/geom.py b/src/python/geom.py
new file mode 100644
index 0000000..5f6f98c
--- /dev/null
+++ b/src/python/geom.py
@@ -0,0 +1,202 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+from libavg import avg
+
+class RoundedRect(avg.PolygonNode):
+ def __init__(self, size, radius, pos=(0,0), parent=None, **kwargs):
+ super(RoundedRect, self).__init__(**kwargs)
+ self.__pos = avg.Point2D(pos)
+ self.__size = avg.Point2D(size)
+ self.__radius = radius
+ self.__calcPolygon()
+ self.registerInstance(self, parent)
+
+ def getPos(self):
+ return self.__pos
+
+ def setPos(self, pos):
+ self.__pos = avg.Point2D(pos)
+ self.__calcPolygon()
+ polyPos = avg.PolygonNode.pos
+ pos = property(getPos, setPos)
+
+ def getSize(self):
+ return self.__size
+
+ def setSize(self, size):
+ self.__size = avg.Point2D(size)
+ self.__calcPolygon()
+ size = property(getSize, setSize)
+
+ def getRadius(self):
+ return self.__radius
+
+ def setRadius(self, radius):
+ self.__radius = radius
+ self.__calcPolygon()
+ radius = property(getRadius, setRadius)
+
+ def __calcPolygon(self):
+ def calcQuarterCircle(center, r, startAngle):
+ pos = []
+ for i in xrange(int(r)+1):
+ angle = i*(1.57/r)+startAngle
+ p = avg.Point2D(center)+avg.Point2D.fromPolar(angle, r)
+ pos.append(p)
+ return pos
+
+ r = self.__radius
+ if self.__size.x < r*2:
+ r = self.__size.x/2
+ if self.__size.y < r*2:
+ r = self.__size.y/2
+ if r == 0:
+ r = 0.01
+ pos = []
+ size = self.__size
+ pos.extend(calcQuarterCircle(self.pos+(size.x-r,r), r, -1.57))
+ pos.extend(calcQuarterCircle(self.pos+(size.x-r,size.y-r), r, 0))
+ pos.extend(calcQuarterCircle(self.pos+(r,size.y-r), r, 1.57))
+ pos.extend(calcQuarterCircle(self.pos+(r,r), r, 3.14))
+ self.polyPos = pos
+
+
+class PieSlice(avg.PolygonNode):
+ def __init__(self, radius, startangle, endangle, pos=(0,0), parent=None,
+ **kwargs):
+ super(PieSlice, self).__init__(**kwargs)
+ self.__pos = avg.Point2D(pos)
+ self.__radius = radius
+ self.__startangle = startangle
+ self.__endangle = endangle
+ self.__calcPolygon()
+ self.registerInstance(self, parent)
+
+ def getPos(self):
+ return self.__pos
+
+ def setPos(self, pos):
+ self.__pos = avg.Point2D(pos)
+ self.__calcPolygon()
+ polyPos = avg.PolygonNode.pos
+ pos = property(getPos, setPos)
+
+ def getRadius(self):
+ return self.__radius
+
+ def setRadius(self, radius):
+ self.__radius = radius
+ self.__calcPolygon()
+ radius = property(getRadius, setRadius)
+
+ def getStartAngle(self):
+ return self.__startangle
+
+ def setStartAngle(self, startangle):
+ self.__startangle = startangle
+ self.__calcPolygon()
+ startangle = property(getStartAngle, setStartAngle)
+
+ def getEndAngle(self):
+ return self.__endangle
+
+ def setEndAngle(self, endangle):
+ self.__endangle = endangle
+ self.__calcPolygon()
+ endangle = property(getEndAngle, setEndAngle)
+
+ def __calcPolygon(self):
+
+ def getCirclePoint(i):
+ angle = self.__startangle + (self.__endangle-self.__startangle)*i
+ return avg.Point2D(self.__pos)+avg.Point2D.fromPolar(angle, self.__radius)
+
+ pos = []
+ circlePart = (self.__endangle - self.__startangle)/6.28
+ numPoints = self.__radius*2.*circlePart
+ if numPoints < 4:
+ numPoints = 4
+ for i in xrange(0, int(numPoints)):
+ pos.append(getCirclePoint(i/numPoints))
+ pos.append(getCirclePoint(1))
+ pos.append(self.__pos)
+ self.polyPos = pos
+
+
+class Arc(avg.PolyLineNode):
+ # TODO: Code duplication with PieSlice
+ def __init__(self, radius, startangle, endangle, pos=(0,0), parent=None,
+ **kwargs):
+ super(Arc, self).__init__(**kwargs)
+ self.__pos = avg.Point2D(pos)
+ self.__radius = radius
+ self.__startangle = startangle
+ self.__endangle = endangle
+ self.__calcPolygon()
+ self.registerInstance(self, parent)
+
+ def getPos(self):
+ return self.__pos
+
+ def setPos(self, pos):
+ self.__pos = avg.Point2D(pos)
+ self.__calcPolygon()
+ polyPos = avg.PolyLineNode.pos
+ pos = property(getPos, setPos)
+
+ def getRadius(self):
+ return self.__radius
+
+ def setRadius(self, radius):
+ self.__radius = radius
+ self.__calcPolygon()
+ radius = property(getRadius, setRadius)
+
+ def getStartAngle(self):
+ return self.__startangle
+
+ def setStartAngle(self, startangle):
+ self.__startangle = startangle
+ self.__calcPolygon()
+ startangle = property(getStartAngle, setStartAngle)
+
+ def getEndAngle(self):
+ return self.__endangle
+
+ def setEndAngle(self, endangle):
+ self.__endangle = endangle
+ self.__calcPolygon()
+ endangle = property(getEndAngle, setEndAngle)
+
+ def __calcPolygon(self):
+
+ def getCirclePoint(i):
+ angle = self.__startangle + (self.__endangle-self.__startangle)*i
+ return avg.Point2D(self.__pos)+avg.Point2D.fromPolar(angle, self.__radius)
+
+ pos = []
+ circlePart = (self.__endangle - self.__startangle)/6.28
+ numPoints = self.__radius*2.*circlePart
+ for i in xrange(0, int(numPoints)):
+ pos.append(getCirclePoint(i/numPoints))
+ pos.append(getCirclePoint(1))
+ self.polyPos = pos
+
diff --git a/src/python/gesture.py b/src/python/gesture.py
new file mode 100644
index 0000000..0334e26
--- /dev/null
+++ b/src/python/gesture.py
@@ -0,0 +1,1000 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+from libavg import avg, statemachine, player, filter
+
+import weakref
+
+import math
+
+class Recognizer(avg.Publisher):
+
+ POSSIBLE = avg.Publisher.genMessageID()
+ DETECTED = avg.Publisher.genMessageID()
+ FAILED = avg.Publisher.genMessageID()
+ MOTION = avg.Publisher.genMessageID()
+ UP = avg.Publisher.genMessageID()
+ END = avg.Publisher.genMessageID()
+
+ def __init__(self, node, isContinuous, maxContacts, initialEvent,
+ possibleHandler=None, failHandler=None, detectedHandler=None,
+ endHandler=None):
+ super(Recognizer, self).__init__()
+
+ if node:
+ self.__node = weakref.ref(node)
+ else:
+ self.__node = None
+ self.__isContinuous = isContinuous
+ self.__maxContacts = maxContacts
+
+ self.__setEventHandler()
+ self.__isEnabled = True
+ self._contacts = set()
+ self.__dirty = False
+
+ self.publish(Recognizer.POSSIBLE)
+ self.publish(Recognizer.DETECTED)
+ self.publish(Recognizer.FAILED)
+ self.publish(Recognizer.END)
+ self.__stateMachine = statemachine.StateMachine(str(type(self)), "IDLE")
+ if self.__isContinuous:
+ self.publish(Recognizer.MOTION)
+ self.publish(Recognizer.UP)
+ self.__stateMachine.addState("IDLE", ("POSSIBLE", "RUNNING"))
+ self.__stateMachine.addState("POSSIBLE", ("IDLE", "RUNNING"))
+ self.__stateMachine.addState("RUNNING", ("IDLE",))
+ else:
+ self.__stateMachine.addState("IDLE", ("POSSIBLE",))
+ self.__stateMachine.addState("POSSIBLE", ("IDLE",))
+
+ self.subscribe(Recognizer.POSSIBLE, possibleHandler)
+ self.subscribe(Recognizer.FAILED, failHandler)
+ self.subscribe(Recognizer.DETECTED, detectedHandler)
+ self.subscribe(Recognizer.END, endHandler)
+ # self.__stateMachine.traceChanges(True)
+ self.__frameHandlerID = None
+
+ if initialEvent:
+ self.__onDown(initialEvent)
+
+ @property
+ def contacts(self):
+ return list(self._contacts)
+
+ def abort(self):
+ if self.__isEnabled:
+ self.__abort()
+ self.__setEventHandler()
+
+ def enable(self, isEnabled):
+ if bool(isEnabled) != self.__isEnabled:
+ self.__isEnabled = bool(isEnabled)
+ if isEnabled:
+ self.__setEventHandler()
+ else:
+ self.__abort()
+
+ def isEnabled(self):
+ return self.__isEnabled
+
+ def getState(self):
+ return self.__stateMachine.state
+
+ def _setPossible(self, event):
+ self.__stateMachine.changeState("POSSIBLE")
+ self.notifySubscribers(Recognizer.POSSIBLE, [])
+
+ def _setFail(self, event):
+ assert(self.__stateMachine.state != "RUNNING")
+ if self.__stateMachine.state != "IDLE":
+ self.__stateMachine.changeState("IDLE")
+ self._disconnectContacts()
+ self.notifySubscribers(Recognizer.FAILED, [])
+
+ def _setDetected(self, event):
+ if self.__isContinuous:
+ self.__stateMachine.changeState("RUNNING")
+ else:
+ self.__stateMachine.changeState("IDLE")
+ self.notifySubscribers(Recognizer.DETECTED, [])
+
+ def _setEnd(self, event):
+ assert(self.__stateMachine.state != "POSSIBLE")
+ if self.__stateMachine.state != "IDLE":
+ self.__stateMachine.changeState("IDLE")
+ self.notifySubscribers(Recognizer.END, [])
+
+ def __onDown(self, event):
+ nodeGone = self._handleNodeGone()
+ if event.contact and not(nodeGone):
+ if (self.__maxContacts == None or len(self._contacts) <
+ self.__maxContacts):
+ event.contact.subscribe(avg.Contact.CURSOR_MOTION, self.__onMotion)
+ event.contact.subscribe(avg.Contact.CURSOR_UP, self.__onUp)
+ self._contacts.add(event.contact)
+ if len(self._contacts) == 1:
+ self.__frameHandlerID = player.subscribe(player.ON_FRAME,
+ self._onFrame)
+ self.__dirty = True
+ return self._handleDown(event)
+
+ def __onMotion(self, event):
+ nodeGone = self._handleNodeGone()
+ if event.contact and not(nodeGone):
+ self.__dirty = True
+ self._handleMove(event)
+
+ def __onUp(self, event):
+ nodeGone = self._handleNodeGone()
+ if event.contact and not(nodeGone):
+ self.__dirty = True
+ self._contacts.remove(event.contact)
+ if len(self._contacts) == 0:
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__frameHandlerID = None
+ self._handleUp(event)
+
+ def __abort(self):
+ if self.__stateMachine.state != "IDLE":
+ self.__stateMachine.changeState("IDLE")
+ if len(self._contacts) != 0:
+ self._disconnectContacts()
+ if self.__node and self.__node():
+ self.__node().unsubscribe(avg.Node.CURSOR_DOWN, self.__onDown)
+
+ def _disconnectContacts(self):
+ for contact in self._contacts:
+ contact.unsubscribe(avg.Contact.CURSOR_MOTION, self.__onMotion)
+ contact.unsubscribe(avg.Contact.CURSOR_UP, self.__onUp)
+ self._contacts = set()
+ if self.__frameHandlerID:
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__frameHandlerID = None
+
+ def _handleDown(self, event):
+ pass
+
+ def _handleMove(self, event):
+ pass
+
+ def _handleUp(self, event):
+ pass
+
+ def _handleChange(self):
+ pass
+
+ def _onFrame(self):
+ nodeGone = self._handleNodeGone()
+ if not(nodeGone) and self.__dirty:
+ self._handleChange()
+ self.__dirty = False
+
+ def _handleNodeGone(self):
+ if self.__node and not(self.__node()):
+ self.enable(False)
+ return True
+ else:
+ return False
+
+ def __setEventHandler(self):
+ if self.__node and self.__node():
+ self.__node().subscribe(avg.Node.CURSOR_DOWN, self.__onDown)
+
+
+class TapRecognizer(Recognizer):
+
+ MAX_TAP_DIST = None
+
+ def __init__(self, node, maxTime=None, maxDist=None, initialEvent=None,
+ possibleHandler=None, failHandler=None, detectedHandler=None):
+ self.__maxTime = maxTime
+ if maxDist == None:
+ maxDist = TapRecognizer.MAX_TAP_DIST
+ self.__maxDist = maxDist
+ super(TapRecognizer, self).__init__(node, False, 1, initialEvent,
+ possibleHandler, failHandler, detectedHandler)
+
+ def _handleDown(self, event):
+ self._setPossible(event)
+ self.__startTime = player.getFrameTime()
+
+ def _handleMove(self, event):
+ if self.getState() != "IDLE":
+ if (event.contact.distancefromstart >
+ self.__maxDist*player.getPixelsPerMM()):
+ self._setFail(event)
+
+ def _handleUp(self, event):
+ if self.getState() == "POSSIBLE":
+ if (event.contact.distancefromstart >
+ self.__maxDist*player.getPixelsPerMM()):
+ self._setFail(event)
+ else:
+ self._setDetected(event)
+
+ def _onFrame(self):
+ downTime = player.getFrameTime() - self.__startTime
+ if self.getState() == "POSSIBLE":
+ if self.__maxTime and downTime > self.__maxTime:
+ self._setFail(None)
+ super(TapRecognizer, self)._onFrame()
+
+
+class DoubletapRecognizer(Recognizer):
+
+ MAX_DOUBLETAP_TIME = None
+
+ def __init__(self, node, maxTime=None, maxDist=None, initialEvent=None,
+ possibleHandler=None, failHandler=None, detectedHandler=None):
+ if maxTime == None:
+ maxTime = DoubletapRecognizer.MAX_DOUBLETAP_TIME
+ self.__maxTime = maxTime
+ if maxDist == None:
+ maxDist = TapRecognizer.MAX_TAP_DIST
+ self.__maxDist = maxDist
+
+ self.__stateMachine = statemachine.StateMachine("DoubletapRecognizer", "IDLE")
+ self.__stateMachine.addState("IDLE", ("DOWN1",), enterFunc=self.__enterIdle)
+ self.__stateMachine.addState("DOWN1", ("UP1", "IDLE"))
+ self.__stateMachine.addState("UP1", ("DOWN2", "IDLE"))
+ self.__stateMachine.addState("DOWN2", ("IDLE",))
+ #self.__stateMachine.traceChanges(True)
+ self.__frameHandlerID = None
+ super(DoubletapRecognizer, self).__init__(node, False, 1,
+ initialEvent, possibleHandler, failHandler, detectedHandler)
+
+ def abort(self):
+ if self.__stateMachine.state != "IDLE":
+ self.__stateMachine.changeState("IDLE")
+ super(DoubletapRecognizer, self).abort()
+
+ def enable(self, isEnabled):
+ if self.__stateMachine.state != "IDLE":
+ self.__stateMachine.changeState("IDLE")
+ super(DoubletapRecognizer, self).enable(isEnabled)
+
+ def _handleDown(self, event):
+ self.__startTime = player.getFrameTime()
+ if self.__stateMachine.state == "IDLE":
+ self.__frameHandlerID = player.subscribe(player.ON_FRAME, self.__onFrame)
+ self.__stateMachine.changeState("DOWN1")
+ self.__startPos = event.pos
+ self._setPossible(event)
+ elif self.__stateMachine.state == "UP1":
+ if ((event.pos - self.__startPos).getNorm() >
+ self.__maxDist*player.getPixelsPerMM()):
+ self.__stateMachine.changeState("IDLE")
+ self._setFail(event)
+ else:
+ self.__stateMachine.changeState("DOWN2")
+ else:
+ assert(False), self.__stateMachine.state
+
+ def _handleMove(self, event):
+ if self.__stateMachine.state != "IDLE":
+ if ((event.pos - self.__startPos).getNorm() >
+ self.__maxDist*player.getPixelsPerMM()):
+ self.__stateMachine.changeState("IDLE")
+ self._setFail(event)
+
+ def _handleUp(self, event):
+ if self.__stateMachine.state == "DOWN1":
+ self.__startTime = player.getFrameTime()
+ self.__stateMachine.changeState("UP1")
+ elif self.__stateMachine.state == "DOWN2":
+ if ((event.pos - self.__startPos).getNorm() >
+ self.__maxDist*player.getPixelsPerMM()):
+ self._setFail(event)
+ else:
+ self._setDetected(event)
+ self.__stateMachine.changeState("IDLE")
+ elif self.__stateMachine.state == "IDLE":
+ pass
+ else:
+ assert(False), self.__stateMachine.state
+
+ def __onFrame(self):
+ downTime = player.getFrameTime() - self.__startTime
+ if downTime > self.__maxTime:
+ self._setFail(None)
+ self.__stateMachine.changeState("IDLE")
+
+ def __enterIdle(self):
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+
+
+class SwipeRecognizer(Recognizer):
+
+ LEFT = 1
+ RIGHT = 2
+ UP = 3
+ DOWN = 4
+
+ SWIPE_DIRECTION_TOLERANCE = math.pi/8
+ MIN_SWIPE_DIST = 50
+ MAX_SWIPE_CONTACT_DIST = 100
+
+ def __init__(self, node, direction, numContacts=1, initialEvent=None,
+ directionTolerance=SWIPE_DIRECTION_TOLERANCE, minDist=MIN_SWIPE_DIST,
+ maxContactDist=MAX_SWIPE_CONTACT_DIST,
+ possibleHandler=None, failHandler=None, detectedHandler=None):
+
+ self.__numContacts = numContacts
+ self.__angleWanted = self.__angleFromDirection(direction)
+ self.__directionTolerance = directionTolerance
+ self.__minDist = minDist*player.getPixelsPerMM()
+ self.__maxInterContactDist = maxContactDist*player.getPixelsPerMM()
+ super(SwipeRecognizer, self).__init__(node, False, numContacts,
+ initialEvent, possibleHandler=possibleHandler, failHandler=failHandler,
+ detectedHandler=detectedHandler)
+
+ def _handleDown(self, event):
+ if len(self._contacts) == 1:
+ self.__startPos = event.pos
+ else:
+ if (event.pos-self.__startPos).getNorm() > self.__maxInterContactDist:
+ self._setFail(event)
+ return
+ if len(self._contacts) == self.__numContacts:
+ self._setPossible(event)
+
+ def _handleMove(self, event):
+ pass
+
+ def _handleUp(self, event):
+ if self.getState() == "POSSIBLE":
+ if (event.contact.distancefromstart < self.__minDist or
+ not(self.__isValidAngle(event.contact.motionangle))):
+ self._setFail(event)
+ elif len(self._contacts) == 0:
+ self._setDetected(event)
+
+ def __angleFromDirection(self, direction):
+ if direction == SwipeRecognizer.RIGHT:
+ return 0
+ elif direction == SwipeRecognizer.DOWN:
+ return math.pi/2
+ elif direction == SwipeRecognizer.LEFT:
+ return math.pi
+ elif direction == SwipeRecognizer.UP:
+ return 3*math.pi/2
+ else:
+ raise RuntimeError("%s is not a valid direction."%direction)
+
+ def __isValidAngle(self, angle):
+ if angle < 0:
+ angle += 2*math.pi
+ minAngle = self.__angleWanted - self.__directionTolerance
+ maxAngle = self.__angleWanted + self.__directionTolerance
+ if minAngle >= 0:
+ return angle > minAngle and angle < maxAngle
+ else:
+ # Valid range spans 0
+ return angle > minAngle+2*math.pi or angle < maxAngle
+
+
+class HoldRecognizer(Recognizer):
+
+ HOLD_DELAY = None
+
+ def __init__(self, node, delay=None, maxDist=None, initialEvent=None,
+ possibleHandler=None, failHandler=None,
+ detectedHandler=None, stopHandler=None):
+ if delay == None:
+ delay = HoldRecognizer.HOLD_DELAY
+ self.__delay = delay
+ if maxDist == None:
+ maxDist = TapRecognizer.MAX_TAP_DIST
+ self.__maxDist = maxDist
+
+ self.__lastEvent = None
+ super(HoldRecognizer, self).__init__(node, True, 1, initialEvent,
+ possibleHandler, failHandler, detectedHandler, stopHandler)
+
+ def _handleDown(self, event):
+ self.__lastEvent = event
+ self._setPossible(event)
+ self.__startTime = player.getFrameTime()
+
+ def _handleMove(self, event):
+ self.__lastEvent = event
+ if self.getState() == "POSSIBLE":
+ if (event.contact.distancefromstart >
+ self.__maxDist*player.getPixelsPerMM()):
+ self._setFail(event)
+
+ def _handleUp(self, event):
+ self.__lastEvent = event
+ if self.getState() == "POSSIBLE":
+ self._setFail(event)
+ elif self.getState() == "RUNNING":
+ self._setEnd(event)
+
+ def _onFrame(self):
+ downTime = player.getFrameTime() - self.__startTime
+ if self.getState() == "POSSIBLE":
+ if downTime > self.__delay:
+ self._setDetected(self.__lastEvent)
+ super(HoldRecognizer, self)._onFrame()
+
+
+class DragRecognizer(Recognizer):
+
+ ANY_DIRECTION = 0
+ VERTICAL = 1
+ HORIZONTAL = 2
+
+ DIRECTION_TOLERANCE = math.pi/4
+ MIN_DRAG_DIST = None
+ FRICTION = None
+
+ def __init__(self, eventNode, coordSysNode=None, initialEvent=None,
+ direction=ANY_DIRECTION, directionTolerance=DIRECTION_TOLERANCE,
+ friction=None, minDragDist=None,
+ possibleHandler=None, failHandler=None, detectedHandler=None,
+ moveHandler=None, upHandler=None, endHandler=None):
+
+ if coordSysNode != None:
+ self.__coordSysNode = weakref.ref(coordSysNode)
+ else:
+ self.__coordSysNode = weakref.ref(eventNode)
+ self.__direction = direction
+ self.__directionTolerance = directionTolerance
+
+ if minDragDist != None:
+ self.__minDragDist = minDragDist
+ else:
+ if self.__direction == DragRecognizer.ANY_DIRECTION:
+ self.__minDragDist = 0
+ else:
+ self.__minDragDist = DragRecognizer.MIN_DRAG_DIST
+
+ if friction == None:
+ self.__friction = DragRecognizer.FRICTION
+ else:
+ self.__friction = friction
+
+ self.__isSliding = False
+ self.__inertiaHandler = None
+ super(DragRecognizer, self).__init__(eventNode, True, 1,
+ initialEvent, possibleHandler=possibleHandler, failHandler=failHandler,
+ detectedHandler=detectedHandler, endHandler=endHandler)
+ self.subscribe(Recognizer.MOTION, moveHandler)
+ self.subscribe(Recognizer.UP, upHandler)
+
+ def abort(self):
+ if self.__inertiaHandler:
+ self.__inertiaHandler.abort()
+ self.__inertiaHandler = None
+ super(DragRecognizer, self).abort()
+
+ def _handleDown(self, event):
+ if not self._handleCoordSysNodeUnlinked():
+ if self.__inertiaHandler:
+ self.__inertiaHandler.abort()
+ self._setEnd(event)
+ if self.__friction != -1:
+ self.__inertiaHandler = InertiaHandler(self.__friction,
+ self.__onInertiaMove, self.__onInertiaStop)
+ if self.__minDragDist == 0:
+ self._setDetected(event)
+ else:
+ self._setPossible(event)
+ pos = self.__relEventPos(event)
+ self.__dragStartPos = pos
+ self.__lastPos = pos
+
+ def _handleMove(self, event):
+ if not self._handleCoordSysNodeUnlinked():
+ if self.getState() != "IDLE":
+ pos = self.__relEventPos(event)
+ offset = pos - self.__dragStartPos
+ if self.getState() == "RUNNING":
+ self.notifySubscribers(Recognizer.MOTION, [offset]);
+ else:
+ if offset.getNorm() > self.__minDragDist*player.getPixelsPerMM():
+ if self.__angleFits(offset):
+ self._setDetected(event)
+ self.notifySubscribers(Recognizer.MOTION, [offset]);
+ else:
+ self.__fail(event)
+ if self.__inertiaHandler:
+ self.__inertiaHandler.onDrag(Transform(pos - self.__lastPos))
+ self.__lastPos = pos
+
+ def _handleUp(self, event):
+ if not self._handleCoordSysNodeUnlinked():
+ if self.getState() != "IDLE":
+ pos = self.__relEventPos(event)
+ if self.getState() == "RUNNING":
+ self.__offset = pos - self.__dragStartPos
+ self.notifySubscribers(Recognizer.UP, [self.__offset]);
+ if self.__friction != -1:
+ self.__isSliding = True
+ self.__inertiaHandler.onDrag(Transform(pos - self.__lastPos))
+ self.__inertiaHandler.onUp()
+ else:
+ self._setEnd(event)
+ else:
+ self.__fail(event)
+
+ def _handleCoordSysNodeUnlinked(self):
+ if self.__coordSysNode().getParent():
+ return False
+ else:
+ self.abort()
+ return True
+
+ def __fail(self, event):
+ self._setFail(event)
+ if self.__inertiaHandler:
+ self.__inertiaHandler.abort()
+ self.__inertiaHandler = None
+
+ def __onInertiaMove(self, transform):
+ self.__offset += transform.trans
+ self.notifySubscribers(Recognizer.MOTION, [self.__offset]);
+
+ def __onInertiaStop(self):
+ self.__inertiaHandler = None
+ self.__isSliding = False
+ if self.getState() == "POSSIBLE":
+ self._setFail(None)
+ else:
+ self._setEnd(None)
+
+ def __relEventPos(self, event):
+ return self.__coordSysNode().getParent().getRelPos(event.pos)
+
+ def __angleFits(self, offset):
+ angle = offset.getAngle()
+ if angle < 0:
+ angle = -angle
+ if self.__direction == DragRecognizer.VERTICAL:
+ return (angle > math.pi/2-self.__directionTolerance
+ and angle < math.pi/2+self.__directionTolerance)
+ elif self.__direction == DragRecognizer.HORIZONTAL:
+ return (angle < self.__directionTolerance
+ or angle > math.pi-self.__directionTolerance)
+ else:
+ return True
+
+class Mat3x3:
+ # Internal class. Will be removed again.
+
+ def __init__(self, row0=(1,0,0), row1=(0,1,0), row2=(0,0,1)):
+ self.m = [row0, row1, row2]
+
+ @classmethod
+ def translate(cls, t):
+ return Mat3x3([1, 0, t[0]],
+ [0, 1, t[1]])
+
+ @classmethod
+ def rotate(cls, a):
+ return Mat3x3([math.cos(a), -math.sin(a), 0],
+ [math.sin(a), math.cos(a), 0])
+
+ @classmethod
+ def pivotRotate(cls, t, a):
+ rot = Mat3x3.rotate(a)
+ trans = Mat3x3.translate(t)
+ return trans.applyMat(rot.applyMat(trans.inverse()))
+
+ @classmethod
+ def scale(cls, s):
+ return Mat3x3([s[0], 0, 0],
+ [0, s[1], 0])
+
+ @classmethod
+ def fromNode(cls, node):
+ return Mat3x3.translate(node.pos).applyMat(
+ Mat3x3.translate(node.pivot).applyMat(
+ Mat3x3.rotate(node.angle).applyMat(
+ Mat3x3.translate(-node.pivot).applyMat(
+ Mat3x3.scale(node.size)))))
+
+ def setNodeTransform(self, node):
+ v = self.applyVec([1,0,0])
+ rot = avg.Point2D(v[0], v[1]).getAngle()
+ node.angle = rot
+ if self.getScale().x < 9999 and self.getScale().y < 9999:
+ node.size = self.getScale()
+ else:
+ node.size = (0,0)
+ node.pivot = node.size/2
+ v = self.applyVec([0,0,1])
+ node.pos = (avg.Point2D(v[0], v[1]) + (node.pivot).getRotated(node.angle) -
+ node.pivot)
+
+ def getScale(self):
+ v = self.applyVec([1,0,0])
+ xscale = avg.Point2D(v[0], v[1]).getNorm()
+ v = self.applyVec([0,1,0])
+ yscale = avg.Point2D(v[0], v[1]).getNorm()
+ return avg.Point2D(xscale, yscale)
+
+ def __str__(self):
+ return self.m.__str__()
+
+ def applyVec(self, v):
+ m = self.m
+ v1 = []
+ for i in range(3):
+ v1.append(m[i][0]*v[0] + m[i][1]*v[1] + m[i][2]*v[2])
+ return v1
+
+ def applyMat(self, m1):
+ m0 = self.m
+ result = Mat3x3()
+ for i in range(3):
+ v = []
+ for j in range(3):
+ v.append(m0[i][0]*m1.m[0][j] + m0[i][1]*m1.m[1][j] + m0[i][2]*m1.m[2][j])
+ result.m[i] = v
+ return result
+
+ def det(self):
+ m = self.m
+ return float( m[0][0] * (m[2][2]*m[1][1]-m[2][1]*m[1][2])
+ -m[1][0] * (m[2][2]*m[0][1]-m[2][1]*m[0][2])
+ +m[2][0] * (m[1][2]*m[0][1]-m[1][1]*m[0][2]))
+
+ def scalarMult(self, s):
+ m = self.m
+ result = Mat3x3()
+ for i in range(3):
+ v = []
+ for j in range(3):
+ v.append(m[i][j]*s)
+ result.m[i] = v
+ return result
+
+ def inverse(self):
+ m = self.m
+ temp = Mat3x3([ m[2][2]*m[1][1]-m[2][1]*m[1][2], -(m[2][2]*m[0][1]-m[2][1]*m[0][2]), m[1][2]*m[0][1]-m[1][1]*m[0][2] ],
+ [-(m[2][2]*m[1][0]-m[2][0]*m[1][2]), m[2][2]*m[0][0]-m[2][0]*m[0][2] , -(m[1][2]*m[0][0]-m[1][0]*m[0][2])],
+ [ m[2][1]*m[1][0]-m[2][0]*m[1][1], -(m[2][1]*m[0][0]-m[2][0]*m[0][1]), m[1][1]*m[0][0]-m[1][0]*m[0][1] ])
+ return temp.scalarMult(1/self.det())
+
+
+def getCentroid(indexes, pts):
+ c = avg.Point2D(0, 0)
+ for i in indexes:
+ c += pts[i]
+ return c/len(indexes)
+
+def calcKMeans(pts):
+
+ # in: List of points
+ # out: Two lists, each containing indexes into the input list
+ assert(len(pts) > 1)
+ p1 = pts[0]
+ p2 = pts[1]
+ oldP1 = None
+ oldP2 = None
+ j = 0
+ while not(p1 == oldP1 and p2 == oldP2) and j < 50:
+ l1 = []
+ l2 = []
+ # Group points
+ for i, pt in enumerate(pts):
+ dist1 = (pt-p1).getNorm()
+ dist2 = (pt-p2).getNorm()
+ if dist1 < dist2:
+ l1.append(i)
+ else:
+ l2.append(i)
+ oldP1 = p1
+ oldP2 = p2
+ p1 = getCentroid(l1, pts)
+ p2 = getCentroid(l2, pts)
+ j += 1
+ return l1, l2
+
+
+class Transform():
+ def __init__(self, trans, rot=0, scale=1, pivot=(0,0)):
+ self.trans = avg.Point2D(trans)
+ self.rot = rot
+ self.scale = scale
+ self.pivot = avg.Point2D(pivot)
+
+ def moveNode(self, node):
+ transMat = Mat3x3.translate(self.trans)
+ rotMat = Mat3x3.rotate(self.rot)
+ scaleMat = Mat3x3.scale((self.scale, self.scale))
+ pivotMat = Mat3x3.translate(self.pivot)
+ invPivotMat = pivotMat.inverse()
+ startTransform = Mat3x3.fromNode(node)
+ newTransform = pivotMat.applyMat(
+ rotMat.applyMat(
+ scaleMat.applyMat(
+ invPivotMat.applyMat(
+ transMat.applyMat(
+ startTransform)))))
+ newTransform.setNodeTransform(node)
+
+ def __repr__(self):
+ return "Transform"+str((self.trans, self.rot, self.scale, self.pivot))
+
+
+class TransformRecognizer(Recognizer):
+
+ FILTER_MIN_CUTOFF = None
+ FILTER_BETA = None
+
+ def __init__(self, eventNode, coordSysNode=None, initialEvent=None, friction=None,
+ detectedHandler=None, moveHandler=None, upHandler=None, endHandler=None):
+ if coordSysNode != None:
+ self.__coordSysNode = weakref.ref(coordSysNode)
+ else:
+ self.__coordSysNode = weakref.ref(eventNode)
+
+ if friction == None:
+ self.__friction = DragRecognizer.FRICTION
+ else:
+ self.__friction = friction
+
+ self.__baseTransform = Mat3x3()
+ self.__lastPosns = []
+ self.__posns = []
+ self.__inertiaHandler = None
+ self.__filters = {}
+ self.__frameHandlerID = None
+ super(TransformRecognizer, self).__init__(eventNode, True, None,
+ initialEvent, detectedHandler=detectedHandler, endHandler=endHandler)
+ self.subscribe(Recognizer.MOTION, moveHandler)
+ self.subscribe(Recognizer.UP, upHandler)
+
+ def enable(self, isEnabled):
+ if bool(isEnabled) != self.isEnabled() and not(isEnabled):
+ self.__abort()
+ super(TransformRecognizer, self).enable(isEnabled)
+
+ def abort(self):
+ self.__abort()
+ super(TransformRecognizer, self).abort()
+
+ def _handleDown(self, event):
+ numContacts = len(self._contacts)
+ self.__newPhase()
+ if self.__isFiltered():
+ self.__filters[event.contact] = [
+ filter.OneEuroFilter(mincutoff=TransformRecognizer.FILTER_MIN_CUTOFF,
+ beta=TransformRecognizer.FILTER_BETA),
+ filter.OneEuroFilter(mincutoff=TransformRecognizer.FILTER_MIN_CUTOFF,
+ beta=TransformRecognizer.FILTER_BETA)]
+ if numContacts == 1:
+ if self.__inertiaHandler:
+ self.__inertiaHandler.abort()
+ self._setEnd(event)
+ self._setDetected(event)
+ self.__frameHandlerID = player.subscribe(player.ON_FRAME, self.__onFrame)
+ if self.__friction != -1:
+ self.__inertiaHandler = InertiaHandler(self.__friction,
+ self.__onInertiaMove, self.__onInertiaStop)
+
+ def _handleUp(self, event):
+ numContacts = len(self._contacts)
+ if numContacts == 0:
+ contact = event.contact
+ transform = Transform(self.__filteredRelContactPos(contact)
+ - self.__lastPosns[0])
+ self.notifySubscribers(Recognizer.UP, [transform]);
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__frameHandlerID = None
+ if self.__friction != -1:
+ self.__inertiaHandler.onDrag(transform)
+ self.__inertiaHandler.onUp()
+ else:
+ self._setEnd(event)
+ elif numContacts == 1:
+ self.__newPhase()
+ else:
+ self.__newPhase()
+ if self.__isFiltered():
+ del self.__filters[event.contact]
+
+ def _handleNodeGone(self):
+ if self.__coordSysNode and not(self.__coordSysNode()):
+ self.enable(False)
+ return True
+ else:
+ return super(TransformRecognizer, self)._handleNodeGone()
+
+ def __onFrame(self):
+ nodeGone = self._handleNodeGone()
+ if not(nodeGone):
+ self.__move()
+
+ def __move(self):
+ numContacts = len(self._contacts)
+ contactPosns = [self.__filteredRelContactPos(contact)
+ for contact in self._contacts]
+ if numContacts == 1:
+ transform = Transform(contactPosns[0] - self.__lastPosns[0])
+ if self.__friction != -1:
+ self.__inertiaHandler.onDrag(transform)
+ self.notifySubscribers(Recognizer.MOTION, [transform]);
+ self.__lastPosns = contactPosns
+ else:
+ if numContacts == 2:
+ self.__posns = contactPosns
+ else:
+ self.__posns = [getCentroid(self.__clusters[i], contactPosns) for
+ i in range(2)]
+
+ startDelta = self.__lastPosns[1]-self.__lastPosns[0]
+ curDelta = self.__posns[1]-self.__posns[0]
+
+ pivot = (self.__posns[0]+self.__posns[1])/2
+
+ rot = avg.Point2D.angle(curDelta, startDelta)
+
+ if self.__lastPosns[0] == self.__lastPosns[1]:
+ scale = 1
+ else:
+ scale = ((self.__posns[0]-self.__posns[1]).getNorm() /
+ (self.__lastPosns[0]-self.__lastPosns[1]).getNorm())
+
+ trans = ((self.__posns[0]+self.__posns[1])/2 -
+ (self.__lastPosns[0]+self.__lastPosns[1])/2)
+ transform = Transform(trans, rot, scale, pivot)
+ if self.__friction != -1:
+ self.__inertiaHandler.onDrag(transform)
+ self.notifySubscribers(Recognizer.MOTION, [transform]);
+ self.__lastPosns = self.__posns
+
+ def __newPhase(self):
+ self.__lastPosns = []
+ numContacts = len(self._contacts)
+ contactPosns = [self.__relContactPos(contact)
+ for contact in self._contacts]
+ if numContacts == 1:
+ self.__lastPosns.append(contactPosns[0])
+ else:
+ if numContacts == 2:
+ self.__lastPosns = contactPosns
+ else:
+ self.__clusters = calcKMeans(contactPosns)
+ self.__lastPosns = [getCentroid(self.__clusters[i], contactPosns) for
+ i in range(2)]
+
+ def __onInertiaMove(self, transform):
+ self.notifySubscribers(Recognizer.MOTION, [transform]);
+
+ def __onInertiaStop(self):
+ self.__inertiaHandler = None
+ self._setEnd(None)
+
+ def __filteredRelContactPos(self, contact):
+ rawPos = self.__relContactPos(contact)
+ if self.__isFiltered():
+ f = self.__filters[contact]
+ return avg.Point2D(f[0].apply(rawPos.x, player.getFrameTime()),
+ f[1].apply(rawPos.y, player.getFrameTime()))
+ else:
+ return rawPos
+
+ def __relContactPos(self, contact):
+ return self.__coordSysNode().getParent().getRelPos(contact.events[-1].pos)
+
+ def __isFiltered(self):
+ return TransformRecognizer.FILTER_MIN_CUTOFF != None
+
+ def __abort(self):
+ if self.__frameHandlerID:
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__frameHandlerID = None
+ if self.__inertiaHandler:
+ self.__inertiaHandler.abort()
+ self.__inertiaHandler = None
+
+
+class InertiaHandler(object):
+ def __init__(self, friction, moveHandler, stopHandler):
+ self.__friction = friction
+ self.__moveHandler = moveHandler
+ self.__stopHandler = stopHandler
+
+ self.__transVel = avg.Point2D(0, 0)
+ self.__curPivot = avg.Point2D(0, 0)
+ self.__angVel = 0
+ self.__sizeVel = avg.Point2D(0, 0)
+ self.__frameHandlerID = player.subscribe(player.ON_FRAME, self.__onDragFrame)
+
+ def abort(self):
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__stopHandler = None
+ self.__moveHandler = None
+
+ def onDrag(self, transform):
+ frameDuration = player.getFrameDuration()
+ if frameDuration > 0:
+ self.__transVel += 0.1*transform.trans/frameDuration
+ if transform.pivot != avg.Point2D(0,0):
+ self.__curPivot = transform.pivot
+ if transform.rot > math.pi:
+ transform.rot -= 2*math.pi
+ if frameDuration > 0:
+ self.__angVel += 0.1*transform.rot/frameDuration
+
+ def onUp(self):
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__frameHandlerID = player.subscribe(player.ON_FRAME, self.__onInertiaFrame)
+ self.__onInertiaFrame()
+
+ def __onDragFrame(self):
+ self.__transVel *= 0.9
+ self.__angVel *= 0.9
+
+ def __onInertiaFrame(self):
+ transNorm = self.__transVel.getNorm()
+ if transNorm - self.__friction > 0:
+ direction = self.__transVel.getNormalized()
+ self.__transVel = direction * (transNorm-self.__friction)
+ curTrans = self.__transVel * player.getFrameDuration()
+ else:
+ curTrans = avg.Point2D(0, 0)
+
+ if self.__angVel != 0:
+ angSign = self.__angVel/math.fabs(self.__angVel)
+ self.__angVel = self.__angVel - angSign*self.__friction/200
+ newAngSign = self.__angVel/math.fabs(self.__angVel)
+ if newAngSign != angSign:
+ self.__angVel = 0
+ curAng = self.__angVel * player.getFrameDuration()
+ self.__curPivot += curTrans
+
+ if transNorm - self.__friction > 0 or self.__angVel != 0:
+ if self.__moveHandler:
+ self.__moveHandler(Transform(curTrans, curAng, 1, self.__curPivot))
+ else:
+ self.__stop()
+
+ def __stop(self):
+ player.unsubscribe(player.ON_FRAME, self.__frameHandlerID)
+ self.__stopHandler()
+ self.__stopHandler = None
+ self.__moveHandler = None
+
+
+def initConfig():
+ def getFloatOption(name):
+ return float(player.getConfigOption("gesture", name))
+
+ TapRecognizer.MAX_TAP_DIST = getFloatOption("maxtapdist")
+ DoubletapRecognizer.MAX_DOUBLETAP_TIME = getFloatOption("maxdoubletaptime")
+ SwipeRecognizer.MIN_SWIPE_DIST = getFloatOption("minswipedist")
+ SwipeRecognizer.SWIPE_DIRECTION_TOLERANCE = getFloatOption("swipedirectiontolerance")
+ SwipeRecognizer.MAX_SWIPE_CONTACT_DIST = getFloatOption("maxswipecontactdist")
+ HoldRecognizer.HOLD_DELAY = getFloatOption("holddelay")
+ DragRecognizer.MIN_DRAG_DIST = getFloatOption("mindragdist")
+ DragRecognizer.FRICTION = getFloatOption("friction")
+ TransformRecognizer.FILTER_MIN_CUTOFF = getFloatOption("filtermincutoff")
+ if TransformRecognizer.FILTER_MIN_CUTOFF == -1:
+ TransformRecognizer.FILTER_MIN_CUTOFF = None
+ TransformRecognizer.FILTER_BETA = getFloatOption("filterbeta")
+
+
+initConfig()
diff --git a/src/python/graph.py b/src/python/graph.py
new file mode 100644
index 0000000..9be9d6d
--- /dev/null
+++ b/src/python/graph.py
@@ -0,0 +1,229 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+import math
+import time
+
+from libavg import avg, player, Point2D
+
+
+class Graph(avg.DivNode):
+ def __init__(self, title='', getValue=None, parent=None, **kwargs):
+ super(Graph, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self._getValue = getValue
+ self._xSkip = 2
+ self._curUsage = 0
+ self.sensitive = False
+
+ avg.RectNode(parent=self, strokewidth=0, fillopacity=0.6, fillcolor="FFFFFF",
+ size=self.size)
+ self._textNode0 = avg.WordsNode(parent=self, x=10, y=self.size.y - 22,
+ color="000080")
+ self._textNode1 = avg.WordsNode(parent=self, x=10, y=self.size.y - 39,
+ color="000080")
+ self._maxLineNode = avg.PolyLineNode(parent=self, color="880000")
+ self._lineNode = avg.PolyLineNode(parent=self, color="008000")
+ self.__graphText = avg.WordsNode(parent=self, x=10, y=0, color="000080")
+ self.__graphText.text = title
+ self._setup()
+
+ def _setup(self):
+ raise RuntimeError('Please overload _setup() function')
+
+
+class AveragingGraph(Graph):
+
+ def __init__(self, title='', getValue=None, parent=None, **kwargs):
+ super(AveragingGraph, self).__init__(title, getValue, parent, **kwargs)
+ self.registerInstance(self, None)
+
+ def unlink(self, kill):
+ player.clearInterval(self.__interval)
+ self.__interval = None
+ super(AveragingGraph, self).unlink(kill)
+
+ def _setup(self):
+ self.__interval = player.setInterval(1000, self._nextMemSample)
+ self.__numSamples = 0
+ self._usage = [0]
+ self._maxUsage = [0]
+ self._minutesUsage = [0]
+ self._minutesMaxUsage = [0]
+ self._nextMemSample()
+
+ def _nextMemSample(self):
+ curUsage = self._getValue()
+ self._usage.append(curUsage)
+ maxUsage = self._maxUsage[-1]
+
+ if curUsage > maxUsage:
+ maxUsage = curUsage
+ lastMaxChangeTime = time.time()
+ self._textNode1.text = ("Last increase in maximum: "
+ + time.strftime("%d.%m.%Y %H:%M:%S",
+ time.localtime(lastMaxChangeTime)))
+ self._maxUsage.append(maxUsage)
+ self.__numSamples += 1
+
+ if self.__numSamples % 60 == 0:
+ lastMinuteAverage = sum(self._usage[-60:]) / 60
+ self._minutesUsage.append(lastMinuteAverage)
+ self._minutesMaxUsage.append(maxUsage)
+
+ if self.__numSamples < 60 * 60:
+ self._plotLine(self._usage, self._lineNode, maxUsage)
+ self._plotLine(self._maxUsage, self._maxLineNode, maxUsage)
+ else:
+ self._plotLine(self._minutesUsage, self._lineNode, maxUsage)
+ self._plotLine(self._minutesMaxUsage, self._maxLineNode, maxUsage)
+
+ self._textNode0.text = ("Max. memory usage: %(size).2f MB" %
+ {"size": maxUsage / (1024 * 1024.0)})
+
+ if self.__numSamples % 3600 == 0:
+ del self._usage[0:3600]
+ del self._maxUsage[0:3599]
+ if self.__numSamples == 604800:
+ self.__numSamples == 0
+
+ def _plotLine(self, data, node, maxy):
+ yfactor = (self.size.y - 10.0) / float(maxy)
+ xfactor = (self.size.x - 10.0) / float(len(data) - 1)
+ node.pos = [(pos[0] * xfactor + 10, (maxy - pos[1]) * yfactor + 10.0)
+ for pos in enumerate(data)]
+
+
+class SlidingGraph(Graph):
+ def __init__(self, title='', getValue=None, limit=120.0, parent=None, **kwargs):
+ super(SlidingGraph, self).__init__(title, getValue, parent, **kwargs)
+ self.registerInstance(self, None)
+ self._limitValue = float(limit)
+
+ def _setup(self):
+ self.__frameHandlerID = player.subscribe(avg.Player.ON_FRAME,
+ self._nextFrameTimeSample)
+ self._numSamples = 0
+ self._lastCurUsage = 0
+ self._maxFrameTime = 0
+ self._values = []
+
+ def _nextFrameTimeSample(self):
+ val = self._frameTimeSample()
+ self._appendValue(val)
+ self._numSamples += 1
+
+ def _appendValue(self, value):
+ maxValue = min(self._limitValue, value)
+ y = self.height - (self.height * (maxValue / self._limitValue))
+ y = max(0, y)
+ numValues = int(self.width / self._xSkip)
+ self._values = (self._values + [y])[-numValues:]
+ self._plotGraph()
+
+ def _frameTimeSample(self):
+ frameTime = self._getValue()
+ diff = frameTime - self._lastCurUsage
+ if self._numSamples < 2:
+ self._maxFrameTime = 0
+ if diff > self._maxFrameTime:
+ lastMaxChangeTime = time.time()
+ self._maxFrameTime = diff
+ self._textNode0.text = ("Max FrameTime: %.f" % self._maxFrameTime + " ms" +
+ " Time: " + time.strftime("%d.%m.%Y %H:%M:%S",
+ time.localtime(lastMaxChangeTime)))
+
+ self._lastCurUsage = frameTime
+ self._textNode1.text = ("Current FrameTime: %.f" % diff + " ms")
+ return diff
+
+ def _plotGraph(self):
+ self._lineNode.pos = self._getCoords()
+
+ def _getCoords(self):
+ return zip(xrange(0, len(self._values) * self._xSkip, self._xSkip), self._values)
+
+
+class BinBar(avg.DivNode):
+ def __init__(self, label, parent=None, **kwargs):
+ super(BinBar, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ avg.WordsNode(text=label, fontsize=8, alignment='center',
+ pos=(self.size.x / 2, self.size.y - 12), parent=self)
+
+ self._vbar = avg.RectNode(opacity=0, fillopacity=0.4,
+ fillcolor='ff0000', parent=self)
+
+ self.update(0)
+
+ def update(self, value):
+ value = min(max(value, 0), 1)
+
+ height = (self.size.y - 15) * value
+ self._vbar.size = (self.size.x - 2, height)
+ self._vbar.pos = (1, self.size.y - height - 12)
+
+
+class BinsGraph(avg.DivNode):
+ def __init__(self, binsThresholds, parent=None, **kwargs):
+ super(BinsGraph, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ avg.RectNode(size=self.size, parent=self)
+ colWidth = self.size.x / len(binsThresholds)
+ self._binBars = [BinBar(str(int(thr)),
+ pos=(idx * colWidth, 0),
+ size=(colWidth, self.size.y),
+ parent=self)
+ for idx, thr in enumerate(binsThresholds)]
+
+ def update(self, values):
+ s = sum(values)
+
+ for binBar, value in zip(self._binBars, values):
+ normValue = float(value) / s
+ logValue = math.log10(normValue * 9 + 1) if normValue != 0 else 0
+ binBar.update(logValue)
+
+
+class SlidingBinnedGraph(SlidingGraph):
+ def __init__(self, title='', getValue=None, limit=120.0, binsThresholds=[],
+ parent=None, **kwargs):
+ super(SlidingBinnedGraph, self).__init__(title, getValue, limit, parent, **kwargs)
+ if not all([isinstance(x, (int, float)) for x in binsThresholds]):
+ raise RuntimeError('Bins thresholds must be provided as list of numbers')
+
+ self._bins = [0] * len(binsThresholds)
+ self._binsThresholds = binsThresholds
+ self._binsGraph = BinsGraph(binsThresholds=binsThresholds,
+ pos=(self.size.x - 120, 5), size=(90, self.size.y - 10),
+ parent=self)
+
+ def _appendValue(self, value):
+ for i in xrange(len(self._binsThresholds) - 1, -1, -1):
+ if value >= self._binsThresholds[i]:
+ self._bins[i] += 1
+ break
+
+ if sum(self._bins) % 100 == 0:
+ self._binsGraph.update(self._bins)
+
+ super(SlidingBinnedGraph, self)._appendValue(value)
diff --git a/src/python/mathutil.py b/src/python/mathutil.py
new file mode 100644
index 0000000..b27758e
--- /dev/null
+++ b/src/python/mathutil.py
@@ -0,0 +1,168 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is Martin Heistermann <mh at sponc dot de>
+#
+
+# TODO: Some of this stuff is duplicated - either in Point2D or in MathHelper.h/.cpp.
+# Clean that up.
+
+import math
+from libavg import Point2D
+
+def getAngle(p1, p2):
+ vec = p2 - p1
+ res = math.atan2(vec.y, vec.x)
+ if res < 0:
+ res += math.pi * 2
+ return res
+
+def getDistance (p, q):
+ return math.sqrt((p.x-q.x)**2 + (p.y-q.y)**2)
+
+def getDistSquared (p, q):
+ return (p.x-q.x)**2 + (p.y-q.y)**2
+
+def getScaleToSize ((width, height), (max_width, max_height)):
+ if width < max_width:
+ height = height * (float(max_width) / width)
+ width = max_width
+ elif height > max_height:
+ width = width * (float(max_height) / height)
+ height = max_height
+ return getScaledDim((width, height), (max_width, max_height))
+
+def getScaledDim (size, max = None, min = None):
+ width, height = size
+ if width == 0 or height == 0:
+ return size
+
+ if max:
+ max = Point2D(max)
+ assert (max.x > 0 and max.y > 0)
+ if width > max.x:
+ height = height * (max.x / width)
+ width = max.x
+ if height > max.y:
+ width = width * (max.y / height)
+ height = max.y
+
+ if min:
+ min = Point2D(min)
+ assert (min.x > 0 and min.y > 0)
+ if width < min.x:
+ height = height * (min.x / width)
+ width = min.x
+ if height < min.y:
+ width = width * (min.y / height)
+ height = min.y
+
+ return Point2D(width, height)
+
+
+class EquationNotSolvable (Exception):
+ pass
+class EquationSingular (Exception):
+ pass
+
+def gauss_jordan(m, eps = 1.0/(10**10)):
+ """Puts given matrix (2D array) into the Reduced Row Echelon Form.
+ Returns True if successful, False if 'm' is singular.
+ NOTE: make sure all the matrix items support fractions! Int matrix will NOT work!
+ Written by Jarno Elonen in April 2005, released into Public Domain
+ http://elonen.iki.fi/code/misc-notes/affine-fit/index.html"""
+ (h, w) = (len(m), len(m[0]))
+ for y in range(0,h):
+ maxrow = y
+ for y2 in range(y+1, h): # Find max pivot
+ if abs(m[y2][y]) > abs(m[maxrow][y]):
+ maxrow = y2
+ (m[y], m[maxrow]) = (m[maxrow], m[y])
+ if abs(m[y][y]) <= eps: # Singular?
+ raise EquationSingular
+ for y2 in range(y+1, h): # Eliminate column y
+ c = m[y2][y] / m[y][y]
+ for x in range(y, w):
+ m[y2][x] -= m[y][x] * c
+ for y in range(h-1, 0-1, -1): # Backsubstitute
+ c = m[y][y]
+ for y2 in range(0,y):
+ for x in range(w-1, y-1, -1):
+ m[y2][x] -= m[y][x] * m[y2][y] / c
+ m[y][y] /= c
+ for x in range(h, w): # Normalize row y
+ m[y][x] /= c
+ return m
+
+
+def solveEquationMatrix(_matrix, eps = 1.0/(10**10)):
+ matrix=[]
+ for coefficients, res in _matrix:
+ newrow = map(float, coefficients + (res,))
+ matrix.append(newrow)
+ matrix = gauss_jordan (matrix)
+ res=[]
+ for col in xrange(len(matrix[0])-1):
+ rows = filter(lambda row: row[col] >= eps, matrix)
+ if len(rows)!=1:
+ raise EquationNotSolvable
+ res.append (rows[0][-1])
+
+ return res
+
+
+def getOffsetForMovedPivot(oldPivot, newPivot, angle):
+ oldPos = Point2D(0,0).getRotated(angle, oldPivot)
+ newPos = Point2D(0,0).getRotated(angle, newPivot)
+ return oldPos - newPos
+
+def isNaN(x):
+ return (not(x<=0) and not(x>=0))
+
+def sgn (x):
+ if x<0:
+ return -1
+ elif x==0:
+ return 0
+ else:
+ return 1
+
+class MovingAverage:
+ """
+ Moving average implementation.
+ Example:
+ ma = MovingAverage(20)
+ print ma(2)
+ print ma(3)
+ print ma(10)
+ """
+ def __init__(self, points):
+ self.__points = points
+ self.__values = []
+
+ def __appendValue(self, value):
+ self.__values = (self.__values + [value])[-self.__points:]
+
+ def __getAverage(self):
+ sum = reduce(lambda a,b:a+b, self.__values)
+ return float(sum) / len(self.__values)
+
+ def __call__(self, value):
+ self.__appendValue(value)
+ return self.__getAverage()
diff --git a/src/python/methodref.py b/src/python/methodref.py
new file mode 100644
index 0000000..f59ed24
--- /dev/null
+++ b/src/python/methodref.py
@@ -0,0 +1,69 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+import weakref, new
+
+class methodref(object):
+ # From Python Cookbook
+ """ Wraps any callable, most importantly a bound method, in a way that allows a bound
+ method's object to be GC'ed, while providing the same interface as a normal weak
+ reference."""
+ def __init__(self, fn):
+ try:
+ # Try getting object, function and class
+ o, f, c = fn.im_self, fn.im_func, fn.im_class
+ except AttributeError:
+ # It's not a bound method
+ self._obj = None
+ self._func = fn
+ self._clas = None
+ if fn:
+ self.__name__ = fn.__name__
+ else:
+ self.__name__ = None
+ else:
+ # Bound method
+ if o is None: # ... actually UN-bound
+ self._obj = None
+ self.__name__ = f.__name__
+ else:
+ self._obj = weakref.ref(o)
+ self.__name__ = fn.im_class.__name__ + "." + fn.__name__
+ self._func = f
+ self._clas = c
+
+ def isSameFunc(self, func):
+ if self._obj is None:
+ return func == self._func
+ elif self._obj() is None:
+ return func is None
+ else:
+ try:
+ o, f, c = func.im_self, func.im_func, func.im_class
+ except AttributeError:
+ return False
+ else:
+ return (o == self._obj() and f == self._func and c == self._clas)
+
+ def __call__(self):
+ if self._obj is None:
+ return self._func
+ elif self._obj() is None:
+ return None
+ return new.instancemethod(self._func, self._obj(), self._clas)
diff --git a/src/python/mtemu.py b/src/python/mtemu.py
new file mode 100644
index 0000000..3bd348f
--- /dev/null
+++ b/src/python/mtemu.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+# Original author of this file is Sebastian Maulbeck
+# <sm (at) archimedes-solutions (dot) de>
+
+'''
+Multitouch emulation helper, supporting pinch gestures
+'''
+
+from libavg import avg, Point2D, player
+
+
+class MTemu(object):
+ MOUSE_STATE_UP = 'MOUSE_STATE_UP'
+ MOUSE_STATE_DOWN = 'MOUSE_STATE_DOWN'
+
+ def __init__(self):
+ self.__mouseState = self.MOUSE_STATE_UP
+ self.__cursorID = 0
+ self.__dualTouch = False
+ self.__secondTouch = False
+ self.__source = avg.Event.TOUCH
+
+ self.__oldEventHook = player.getEventHook()
+ player.setEventHook(self.__onEvent)
+
+ root = player.getRootNode()
+ self.__caption = avg.WordsNode(pos=(root.size.x - 15, root.size.y - 20),
+ alignment = 'right',
+ color='DDDDDD',
+ sensitive=False,
+ fontsize=14,
+ parent=root)
+ self.__updateCaption()
+
+ def deinit(self):
+ player.setEventHook(self.__oldEventHook)
+ self.__caption.unlink()
+ if self.__mouseState == self.MOUSE_STATE_DOWN:
+ self.__releaseTouch(self.__cursorID)
+ if self.__secondTouch:
+ self.__releaseTouch(self.__cursorID+1)
+
+ def toggleSource(self):
+ '''
+ Switch between avg.Event.TOUCH and avg.Event.TRACK - source
+ '''
+ self.__clearSourceState()
+ self.__source = (avg.Event.TOUCH if self.__source == avg.Event.TRACK
+ else avg.Event.TRACK)
+ self.__updateCaption()
+
+ def toggleDualTouch(self):
+ self.__dualTouch = not(self.__dualTouch)
+ self.__clearDualtouchState()
+
+ def enableDualTouch(self):
+ self.__dualTouch = True
+ self.__clearDualtouchState()
+
+ def disableDualTouch(self):
+ self.__dualTouch = False
+ self.__clearDualtouchState()
+
+ def __clearSourceState(self):
+ if self.__mouseState == self.MOUSE_STATE_DOWN:
+ self.__releaseTouch(self.__cursorID)
+ if self.__secondTouch:
+ self.__releaseTouch(self.__cursorID+1)
+ self.__mouseState = self.MOUSE_STATE_UP
+ self.__secondTouch = False
+
+ def __clearDualtouchState(self):
+ if self.__mouseState == self.MOUSE_STATE_DOWN:
+ if self.__secondTouch:
+ self.__releaseTouch(self.__cursorID+1)
+ else:
+ self.__sendFakeTouch(self.__cursorID+1, Point2D(0,0),
+ avg.Event.CURSOR_DOWN, mirror=True)
+ self.__secondTouch = not(self.__secondTouch)
+
+ def __updateCaption(self):
+ self.__caption.text = 'Multitouch emulation (%s source)' % self.__source
+
+ def __onEvent(self, event):
+ if event.source == avg.Event.MOUSE:
+ if event.type == avg.Event.CURSOR_DOWN:
+ self.__onMouseDown(event)
+ elif event.type == avg.Event.CURSOR_MOTION:
+ self.__onMouseMotion(event)
+ elif event.type == avg.Event.CURSOR_UP:
+ self.__onMouseUp(event)
+ return True
+ else:
+ return False
+
+ def __onMouseDown(self, event):
+ self._initialPos = event.pos
+ if self.__mouseState == self.MOUSE_STATE_UP and event.button == 1:
+ self.__sendFakeTouch(self.__cursorID, event.pos, event.type)
+ if self.__dualTouch and not self.__secondTouch:
+ self.__sendFakeTouch(self.__cursorID+1, event.pos, event.type,
+ True)
+ self.__secondTouch = True
+ self.__mouseState = self.MOUSE_STATE_DOWN
+
+ def __onMouseMotion(self, event):
+ if self.__mouseState == self.MOUSE_STATE_DOWN:
+ self.__sendFakeTouch(self.__cursorID, event.pos, event.type)
+ if self.__dualTouch and self.__secondTouch:
+ self.__sendFakeTouch(self.__cursorID+1, event.pos,
+ event.type, True)
+
+ def __onMouseUp(self, event):
+ if self.__mouseState == self.MOUSE_STATE_DOWN and event.button == 1:
+ self.__sendFakeTouch(self.__cursorID, event.pos, event.type)
+ if self.__dualTouch and self.__secondTouch:
+ self.__sendFakeTouch(self.__cursorID+1, event.pos,
+ event.type, True)
+ self.__secondTouch = False
+ self.__mouseState = self.MOUSE_STATE_UP
+ self.__cursorID += 2 #Even for left uneven for right touch
+
+ def __sendFakeTouch(self, cursorID, pos, touchType, mirror=False):
+ offset = Point2D(0,0)
+ if self.__dualTouch:
+ offset = Point2D(40, 0)
+ if mirror:
+ pos = 2*(self._initialPos)-pos
+ offset = -offset
+ player.getTestHelper().fakeTouchEvent(cursorID,
+ touchType, self.__source, self.__clampPos(pos+offset))
+
+ def __releaseTouch(self, cursorID):
+ self.__sendFakeTouch(cursorID, Point2D(0,0), avg.Event.CURSOR_UP)
+
+ def __clampPos(self, pos):
+ if pos[0] < 0:
+ pos[0] = 0
+ if pos[1] < 0:
+ pos[1] = 0
+ if pos[0] >= player.getRootNode().size[0]:
+ pos[0] = player.getRootNode().size[0]-1
+ if pos[1] >= player.getRootNode().size[1]:
+ pos[1] = player.getRootNode().size[1]-1
+ return pos
+
diff --git a/src/python/parsecamargs.py b/src/python/parsecamargs.py
new file mode 100644
index 0000000..7515b2f
--- /dev/null
+++ b/src/python/parsecamargs.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+from libavg import avg
+
+validPixFmt = list();
+for formatItem in avg.getSupportedPixelFormats():
+ validPixFmt.append(formatItem);
+validDrivers = ('firewire', 'video4linux', 'directshow')
+
+def addOptions(parser):
+ parser.add_option("-t", "--driver", action="store", dest="driver",
+ choices=validDrivers,
+ help="camera drivers (one of: %s)" %', '.join(validDrivers))
+ parser.add_option("-d", "--device", action = "store", dest = "device", default = "",
+ help = "camera device identifier (may be GUID or device path)")
+ parser.add_option("-u", "--unit", action="store", dest="unit", default="-1",
+ type="int", help="unit number")
+ parser.add_option("-w", "--width", dest="width", default="640", type="int",
+ help="capture width in pixels")
+ parser.add_option("-e", "--height", dest="height", default="480", type="int",
+ help="capture height in pixels")
+ parser.add_option("-p", "--pixformat", dest="pixelFormat", default="R8G8B8",
+ choices=validPixFmt,
+ help="camera frame pixel format (one of: %s)" %', '.join(validPixFmt))
+ parser.add_option("-f", "--framerate", dest="framerate", default="15", type="float",
+ help="capture frame rate")
+ parser.add_option("-8", "--fw800", dest="fw800", action="store_true", default=False,
+ help="set firewire bus speed to s800 (if applicable)")
diff --git a/src/python/persist.py b/src/python/persist.py
new file mode 100644
index 0000000..1357609
--- /dev/null
+++ b/src/python/persist.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is OXullo Interecans <x at brainrapers dot org>
+
+
+import os
+import time
+import cPickle as pickle
+
+import libavg
+
+
+class Persist(object):
+ def __init__(self, storeFile, initialData, validator=lambda v: True,
+ autoCommit=False):
+ self.__storeFile = storeFile
+
+ if hasattr(initialData, '__call__'):
+ initialData = initialData()
+ elif initialData is None:
+ initialData = dict()
+
+ if os.path.exists(self.__storeFile):
+ if not os.path.isfile:
+ raise RuntimeError('%s dump file is not a plain file' % self)
+ elif not os.access(self.__storeFile, os.R_OK | os.W_OK):
+ raise RuntimeError('%s dump file'
+ 'cannot be accessed with r/w permissions' % self)
+
+ try:
+ f = open(self.__storeFile)
+ except IOError:
+ libavg.logger.debug('Initializing %s' % self)
+ self.data = initialData
+ self.commit()
+ else:
+ try:
+ self.data = pickle.load(f)
+ except:
+ f.close()
+ libavg.logger.warning('Persist %s is corrupted or unreadable, '
+ 'reinitializing' % self)
+ self.data = initialData
+ self.commit()
+ else:
+ f.close()
+ if not validator(self.data):
+ libavg.logger.warning('Sanity check failed for %s, '
+ 'reinitializing' % self)
+ self.data = initialData
+ self.commit()
+ else:
+ libavg.logger.debug('%s successfully loaded' % self)
+
+ if autoCommit:
+ import atexit
+ atexit.register(self.commit)
+
+ def __repr__(self):
+ return '<%s %s>' % (self.__class__.__name__, self.__storeFile)
+
+ @property
+ def storeFile(self):
+ return self.__storeFile
+
+ def commit(self):
+ tempFile = self.__storeFile + '.tmp.' + str(int(time.time() * 1000))
+
+ try:
+ with open(tempFile, 'wb') as f:
+ pickle.dump(self.data, f)
+ except Exception, e:
+ libavg.logger.warning('Cannot save %s (%s)' % (self.__storeFile, str(e)))
+ return False
+ else:
+ if os.path.exists(self.__storeFile):
+ try:
+ os.remove(self.__storeFile)
+ except Exception, e:
+ libavg.logger.warning('Cannot overwrite dump file '
+ '%s (%s)' % (self, str(e)))
+ return False
+ try:
+ os.rename(tempFile, self.__storeFile)
+ except Exception, e:
+ libavg.logger.warning('Cannot save %s (%s)' % (self, str(e)))
+ os.remove(tempFile)
+ return False
+ else:
+ libavg.logger.debug('%s saved' % self)
+ return True
+
+
+class UserPersistentData(Persist):
+ def __init__(self, appName, fileName, *args, **kargs):
+ basePath = os.path.join(self._getUserDataPath(), appName)
+ fullPath = os.path.join(basePath, '%s.pkl' % fileName)
+
+ try:
+ os.makedirs(basePath)
+ except OSError, e:
+ import errno
+ if e.errno != errno.EEXIST:
+ raise
+
+ super(UserPersistentData, self).__init__(fullPath, *args, **kargs)
+
+ def _getUserDataPath(self):
+ if os.name == 'posix':
+ path = os.path.join(os.environ['HOME'], '.avg')
+ elif os.name == 'nt':
+ path = os.path.join(os.environ['APPDATA'], 'Avg')
+ else:
+ raise RuntimeError('Unsupported system %s' % os.name)
+
+ return path
+
+
+if __name__ == '__main__':
+ testFile = './testfile.pkl'
+ initialData = {'initial': True}
+ p = Persist(testFile, initialData)
+ p.commit()
+ p.data['initial'] = False
+ p.commit()
+
+ p = Persist(testFile, initialData)
+ print not p.data['initial']
+
+ os.unlink(testFile)
+
+ p = UserPersistentData('myapp', 'hiscore', initialData)
+ p.data['initial'] = False
+ p.commit()
+
+ print p
+
diff --git a/src/python/statemachine.py b/src/python/statemachine.py
new file mode 100644
index 0000000..993ef9b
--- /dev/null
+++ b/src/python/statemachine.py
@@ -0,0 +1,149 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+from methodref import methodref
+
+import subprocess
+import os
+
+class State(object):
+ def __init__(self, transitions, enterFunc, leaveFunc):
+ self.transitions = {}
+ for destState, transfunc in transitions.items():
+ ref = methodref(transfunc)
+ self.transitions[destState] = ref
+ self.enterFunc = methodref(enterFunc)
+ self.leaveFunc = methodref(leaveFunc)
+
+class StateMachine(object):
+ def __init__(self, name, startState):
+ self.__states = {}
+ self.__name = name
+ self.__startState = startState
+ self.__curState = startState
+ self.__trace = False
+ self.__initDone = False
+
+ def addState(self, state, transitions, enterFunc=None, leaveFunc=None):
+ if self.__initDone:
+ raise RuntimeError(
+ "StateMachine: Can't add new states after calling changeState")
+ if self.__states.has_key(state):
+ raise RuntimeError("StateMachine: Duplicate state " + state + ".")
+
+ if isinstance(transitions, (list, tuple)):
+ transitions = dict.fromkeys(transitions)
+ self.__states[state] = State(transitions, enterFunc, leaveFunc)
+
+ def changeState(self, newState):
+ if not(self.__initDone):
+ self.__initDone = True
+ self.__doSanityCheck()
+
+ if self.__trace:
+ print self.__name, ":", self.__curState, "-->", newState
+
+ if not(newState in self.__states):
+ raise RuntimeError('StateMachine: Attempt to change to nonexistent state '+
+ newState+'.')
+ assert(self.__curState in self.__states)
+ state = self.__states[self.__curState]
+ if newState in state.transitions:
+ if state.leaveFunc() != None:
+ state.leaveFunc()()
+ transitionFunc = state.transitions[newState]()
+ if transitionFunc != None:
+ try:
+ transitionFunc(self.__curState, newState)
+ except TypeError:
+ transitionFunc()
+ self.__curState = newState
+ enterFunc = self.__states[self.__curState].enterFunc()
+ if enterFunc != None:
+ enterFunc()
+ else:
+ raise RuntimeError('StateMachine: State change from '+self.__curState+' to '+
+ newState+' not allowed.')
+
+ def traceChanges(self, trace):
+ self.__trace = trace
+
+ @property
+ def state(self):
+ return self.__curState
+
+ def dump(self):
+ for oldStateName, state in self.__states.iteritems():
+ print oldStateName, ("(enter: " + self.__getNiceFuncName(state.enterFunc)
+ + ", leave: " + self.__getNiceFuncName(state.leaveFunc) + "):")
+ for newState, func in state.transitions.iteritems():
+ print " -->", newState, ":", self.__getNiceFuncName(func)
+ print "Current state:", self.__curState
+
+ def makeDiagram(self, fName, showMethods=False):
+ def writeState(stateName, state):
+ label = stateName
+ if showMethods:
+ if state.enterFunc.__name__ is not(None):
+ label += ('<br/><font point-size="10">entry/'
+ + state.enterFunc.__name__ + '</font>')
+ if state.leaveFunc.__name__ is not(None):
+ label += ('<br/><font point-size="10">exit/'
+ + state.leaveFunc.__name__ + '</font>')
+ dotFile.write(' "'+stateName+'" [label=<'+label+'>];\n')
+
+ def writeTransition(origState, destState, func):
+ dotFile.write(' "'+origState+'" -> "'+destState+'"')
+ if showMethods and func and func.__name__ is not(None):
+ dotFile.write(' [label="/'+func.__name__+'", fontsize=10]')
+ dotFile.write(";\n")
+
+
+ dotFile = open("tmp.dot", "w")
+ dotFile.write('digraph "'+self.__name+'" {\n')
+ dotFile.write(' node [fontsize=12, shape=box, style=rounded];\n')
+ dotFile.write(' startstate [shape=point, height=0.2, width=0.2, label=""];\n')
+ dotFile.write(' { rank=source; "startstate" };\n')
+ writeTransition("startstate", self.__startState, None)
+ for stateName, state in self.__states.iteritems():
+ writeState(stateName, state)
+ for destState, func in state.transitions.iteritems():
+ writeTransition(stateName, destState, func)
+ dotFile.write(' "'+self.__curState+'" [style="rounded,bold"];\n')
+ dotFile.write('}\n')
+ dotFile.close()
+ try:
+ subprocess.call(["dot", "tmp.dot", "-Tpng", "-o"+fName])
+ except OSError:
+ raise RuntimeError("dot executable not found. graphviz needs to be installed for StateMachine.makeDiagram to work.")
+ os.remove("tmp.dot")
+
+ def __getNiceFuncName(self, f):
+ if f.__name__ is not(None):
+ return f.__name__
+ else:
+ return "None"
+
+ def __doSanityCheck(self):
+ for stateName, state in self.__states.iteritems():
+ for transitionName in state.transitions.iterkeys():
+ if not(self.__states.has_key(transitionName)):
+ raise RuntimeError("StateMachine: transition " + stateName + " -> " +
+ transitionName + " has an unknown destination state.")
diff --git a/src/python/textarea.py b/src/python/textarea.py
new file mode 100644
index 0000000..8b9bcf1
--- /dev/null
+++ b/src/python/textarea.py
@@ -0,0 +1,833 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this module is Marco Fagiolini <mfx at archi-me-des dot de>
+#
+
+"""
+Single/Multi-Line editable text field widget for libavg
+
+textarea module provides two classes:
+
+1. TextArea
+ This is the implementation of the widget. Every instantiated TextArea
+ represents an editable text field, which can be set up with several styles
+ and behaviors.
+
+2. FocusContext
+ This helps to easily route the events that comes from keyboards to an
+ appropriate TextArea instance, cycling focuses and dispatching events on
+ the selected field.
+
+"""
+
+g_FocusContext = None
+g_LastKeyEvent = None
+g_activityCallback = None
+g_LastKeyRepeated = 0
+g_RepeatDelay = 0.2
+g_CharDelay = 0.1
+
+KEYCODE_TAB = 9
+KEYCODE_LINEFEED = 13
+KEYCODE_SHTAB = 25
+KEYCODE_FORMFEED = 12
+KEYCODE_CRS_UP = 63232
+KEYCODE_CRS_DOWN = 63233
+KEYCODE_CRS_LEFT = 63234
+KEYCODE_CRS_RIGHT = 63235
+KEYCODES_BACKSPACE = (8,127)
+KEYCODES_DEL = 63272
+
+CURSOR_PADDING_PCT = 15
+CURSOR_WIDTH_PCT = 4
+CURSOR_SPACING_PCT = 4
+CURSOR_FLASHING_DELAY = 600
+CURSOR_FLASH_AFTER_INACTIVITY = 200
+
+DEFAULT_BLUR_OPACITY = 0.3
+
+import time
+
+from libavg import avg, player, gesture
+from avg import Point2D
+
+
+class FocusContext(object):
+ """
+ This class helps to group TextArea elements
+
+ TextArea elements that belong to the same FocusContext cycle focus among
+ themselves. There can be several FocusContextes but only one active at once
+ ( using the global function setActiveFocusContext() )
+ """
+ def __init__(self):
+ self.__elements = []
+ self.__isActive = False
+
+ def isActive(self):
+ """
+ Test if this FocusContext is currently active
+ """
+ return self.__isActive
+
+ def register(self, taElement):
+ """
+ Register a floating textarea on this FocusContext
+
+ @param taElement: a TextArea instance
+ """
+ self.__elements.append(taElement)
+
+ def getFocused(self):
+ """
+ Query the TextArea element that currently has focus
+
+ @return: TextArea instance or None
+ """
+ for ob in self.__elements:
+ if ob.hasFocus():
+ return ob
+ return None
+
+ def keyCharPressed(self, kchar):
+ """
+ Inject an utf-8 encoded characted into the flow
+
+ Shift a character (Unicode keycode) into the active (w/focus) TextArea
+ @type kchar: string
+ @param kchar: a single character (if more than one, the following are ignored)
+ """
+ uch = unicode(kchar, 'utf-8')
+ self.keyUCodePressed(ord(uch[0]))
+
+ def keyUCodePressed(self, keycode):
+ """
+ Inject an Unicode code point into the flow
+
+ Shift a character (Unicode keycode) into the active (w/focus) TextArea
+ @type keycode: int
+ @param keycode: unicode code point of the character
+ """
+ # TAB key cycles focus through textareas
+ if keycode == KEYCODE_TAB:
+ self.cycleFocus()
+ return
+ # Shift-TAB key cycles focus through textareas backwards
+ if keycode == KEYCODE_SHTAB:
+ self.cycleFocus(True)
+ return
+
+ for ob in self.__elements:
+ if ob.hasFocus():
+ ob.onKeyDown(keycode)
+
+ def backspace(self):
+ """
+ Emulate a backspace character keypress
+ """
+ self.keyUCodePressed(KEYCODES_BACKSPACE[0])
+
+ def delete(self):
+ """
+ Emulate a delete character keypress
+ """
+ self.keyUCodePressed(KEYCODE_DEL)
+
+ def clear(self):
+ """
+ Clear the active textarea, emulating the press of FF character
+ """
+ self.keyUCodePressed(KEYCODE_FORMFEED)
+
+ def resetFocuses(self):
+ """
+ Blur every TextArea registered within this FocusContext
+ """
+ for ob in self.__elements:
+ ob.clearFocus()
+
+ def cycleFocus(self, backwards=False):
+ """
+ Force a focus cycle among instantiated textareas
+
+ TAB/Sh-TAB keypress is what is translated in a focus cycle.
+ @param backwards: as default, the method cycles following the order
+ that has been followed during the registration of TextArea
+ instances. Setting this to True, the order is inverted.
+ """
+
+ els = []
+ els.extend(self.__elements)
+
+ if len(els) == 0:
+ return
+
+ if backwards:
+ els.reverse()
+
+ elected = 0
+ for ob in els:
+ if not ob.hasFocus():
+ elected = elected + 1
+ else:
+ break
+
+ # elects the first if no ta are in focus or if the
+ # last one has it
+ if elected in (len(els), len(els)-1):
+ elected = 0
+ else:
+ elected = elected + 1
+
+ for ob in els:
+ ob.setFocus(False)
+
+ els[elected].setFocus(True)
+
+ def getRegistered(self):
+ """
+ Returns a list of TextArea currently registered within this FocusContext
+ @return: a list of registered TextArea instances
+ """
+ return self.__elements
+
+ def _switchActive(self, active):
+ if active:
+ self.resetFocuses()
+ self.cycleFocus()
+ else:
+ self.resetFocuses()
+
+ self.__isActive = active
+
+
+class TextArea(avg.DivNode):
+ """
+ TextArea class is a libavg widget to create editable text fields
+
+ TextArea is an extended <words> node that reacts to user input (mouse/touch for
+ focus, keyboard for text input). Can be set as a single line or span to multiple
+ lines.
+ """
+ def __init__(self, focusContext=None, disableMouseFocus=False,
+ moveCoursorOnTouch=True, textBackgroundNode=None, loupeBackgroundNode=None,
+ parent=None, **kwargs):
+ """
+ @param parent: parent of the node
+ @param focusContext: FocusContext object which directs focus for TextArea elements
+ @param disableMouseFocus: boolean, prevents that mouse can set focus for
+ this instance
+ @param moveCoursorOnTouch: boolean, activate the coursor motion on touch events
+ """
+ super(TextArea, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__focusContext = focusContext
+ self.__blurOpacity = DEFAULT_BLUR_OPACITY
+ self.__border = 0
+ self.__data = []
+ self.__cursorPosition = 0
+
+ textNode = avg.WordsNode(rawtextmode=True)
+
+ if textBackgroundNode != None:
+ self.appendChild(textBackgroundNode)
+
+ if not disableMouseFocus:
+ self.setEventHandler(avg.Event.CURSOR_UP, avg.Event.MOUSE, self.__onClick)
+ self.setEventHandler(avg.Event.CURSOR_UP, avg.Event.TOUCH, self.__onClick)
+
+ self.appendChild(textNode)
+
+ cursorContainer = avg.DivNode()
+ cursorNode = avg.LineNode(color='000000')
+ self.appendChild(cursorContainer)
+ cursorContainer.appendChild(cursorNode)
+ self.__flashingCursor = False
+
+ self.__cursorContainer = cursorContainer
+ self.__cursorNode = cursorNode
+ self.__textNode = textNode
+
+ self.__loupe = None
+
+ if focusContext is not None:
+ focusContext.register(self)
+ self.setFocus(False)
+ else:
+ self.setFocus(True)
+
+ player.setInterval(CURSOR_FLASHING_DELAY, self.__tickFlashCursor)
+
+ self.__lastActivity = 0
+
+ if moveCoursorOnTouch:
+ self.__recognizer = gesture.DragRecognizer(eventNode=self, friction=-1,
+ moveHandler=self.__moveHandler,
+ detectedHandler=self.__detectedHandler,
+ upHandler=self.__upHandler)
+ self.__loupeZoomFactor = 0.5
+ self.__loupe = avg.DivNode(parent=self, crop=True)
+
+ if loupeBackgroundNode != None:
+ self.__loupe.appendChild(loupeBackgroundNode)
+ self.__loupe.size = loupeBackgroundNode.size
+ else:
+ self.__loupe.size = (50,50)
+ avg.RectNode(fillopacity=1, fillcolor="f5f5f5", color="ffffff",
+ size=self.__loupe.size, parent=self.__loupe)
+ self.__loupeOffset = (self.__loupe.size[0]/2.0, self.__loupe.size[1]+20)
+ self.__loupe.unlink()
+ self.__zoomedImage = avg.DivNode(parent=self.__loupe)
+ self.__loupeTextNode = avg.WordsNode(rawtextmode=True,
+ parent=self.__zoomedImage)
+
+ self.__loupeCursorContainer = avg.DivNode(parent=self.__zoomedImage)
+ self.__loupeCursorNode = avg.LineNode(color='000000',
+ parent=self.__loupeCursorContainer)
+ self.setStyle()
+
+ def clearText(self):
+ """
+ Clears the text
+ """
+ self.setText(u'')
+
+ def setText(self, uString):
+ """
+ Set the text on the TextArea
+
+ @param uString: an unicode string (or an utf-8 encoded string)
+ """
+ if not isinstance(uString, unicode):
+ uString = unicode(uString, 'utf-8')
+
+ self.__data = []
+ for c in uString:
+ self.__data.append(c)
+
+ self.__cursorPosition = len(self.__data)
+ self.__update()
+
+ def getText(self):
+ """
+ Get the text stored and displayed on the TextArea
+ """
+ return self.__getUnicodeFromData()
+
+ def setStyle(self, font='sans', fontsize=12, alignment='left', variant='Regular',
+ color='000000', multiline=True, cursorWidth=None, border=(0,0),
+ blurOpacity=DEFAULT_BLUR_OPACITY, flashingCursor=False, cursorColor='000000',
+ lineSpacing=0, letterSpacing=0):
+ """
+ Set TextArea's graphical appearance
+ @param font: font face
+ @param fontsize: font size in pixels
+ @param alignment: one among 'left', 'right', 'center'
+ @param variant: font variant (eg: 'bold')
+ @param color: RGB hex for text color
+ @param multiline: boolean, whether TextArea has to wrap (undefinitely)
+ or stop at full width
+ @param cursorWidth: int, width of the cursor in pixels
+ @param border: amount of offsetting pixels that words node will have from image
+ extents
+ @param blurOpacity: opacity that textarea gets when goes to blur state
+ @param flashingCursor: whether the cursor should flash or not
+ @param cursorColor: RGB hex for cursor color
+ @param lineSpacing: linespacing property of words node
+ @param letterSpacing: letterspacing property of words node
+ """
+ self.__textNode.fontstyle = avg.FontStyle(font=font, fontsize=fontsize,
+ alignment=alignment, variant=variant, linespacing=lineSpacing,
+ letterspacing=letterSpacing)
+ self.__textNode.color = color
+ self.__isMultiline = multiline
+ self.__border = border
+ self.__maxLength = -1
+ self.__blurOpacity = blurOpacity
+
+ if multiline:
+ self.__textNode.width = int(self.width) - self.__border[0] * 2
+ self.__textNode.wrapmode = 'wordchar'
+ else:
+ self.__textNode.width = 0
+
+ self.__textNode.x = self.__border[0]
+ self.__textNode.y = self.__border[1]
+
+ tempNode = avg.WordsNode(text=u'W', font=font, fontsize=int(fontsize),
+ variant=variant)
+ self.__textNode.realFontSize = tempNode.getGlyphSize(0)
+ del tempNode
+ self.__textNode.alignmentOffset = Point2D(0,0)
+
+ if alignment != "left":
+ offset = Point2D(self.size.x / 2,0)
+ if alignment == "right":
+ offset = Point2D(self.size.x,0)
+ self.__textNode.pos += offset
+ self.__textNode.alignmentOffset = offset
+ self.__cursorContainer.pos = offset
+
+ self.__cursorNode.color = cursorColor
+ if cursorWidth is not None:
+ self.__cursorNode.strokewidth = cursorWidth
+ else:
+ w = float(fontsize) * CURSOR_WIDTH_PCT / 100.0
+ if w < 1:
+ w = 1
+ self.__cursorNode.strokewidth = w
+ x = self.__cursorNode.strokewidth / 2.0
+ self.__cursorNode.pos1 = Point2D(x, self.__cursorNode.pos1.y)
+ self.__cursorNode.pos2 = Point2D(x, self.__cursorNode.pos2.y)
+
+ self.__flashingCursor = flashingCursor
+ if not flashingCursor:
+ self.__cursorContainer.opacity = 1
+
+ if self.__loupe:
+ zoomfactor = (1.0 + self.__loupeZoomFactor)
+ self.__loupeTextNode.fontstyle = self.__textNode.fontstyle
+ self.__loupeTextNode.fontsize = int(fontsize) * zoomfactor
+ self.__loupeTextNode.color = color
+ if multiline:
+ self.__loupeTextNode.width = self.__textNode.width * zoomfactor
+ self.__loupeTextNode.wrapmode = 'wordchar'
+ else:
+ self.__loupeTextNode.width = 0
+
+ self.__loupeTextNode.x = self.__border[0] * 2
+ self.__loupeTextNode.y = self.__border[1] * 2
+
+ self.__loupeTextNode.realFontSize = self.__textNode.realFontSize * zoomfactor
+
+ if alignment != "left":
+ self.__loupeTextNode.pos = self.__textNode.pos * zoomfactor
+ self.__loupeTextNode.alignmentOffset = self.__textNode.alignmentOffset * \
+ zoomfactor
+ self.__loupeCursorContainer.pos = self.__cursorContainer.pos * zoomfactor
+
+ self.__loupeCursorNode.color = cursorColor
+ if cursorWidth is not None:
+ self.__loupeCursorNode.strokewidth = cursorWidth * zoomfactor
+ else:
+ w = float(self.__loupeTextNode.fontsize) * CURSOR_WIDTH_PCT / 100.0
+ if w < 1:
+ w = 1
+ self.__loupeCursorNode.strokewidth = w * zoomfactor
+ x = self.__loupeCursorNode.strokewidth / 2.0
+ self.__loupeCursorNode.pos1 = Point2D(x, self.__loupeCursorNode.pos1.y)
+ self.__loupeCursorNode.pos2 = Point2D(x, self.__loupeCursorNode.pos2.y)
+
+ if not flashingCursor:
+ self.__loupeCursorContainer.opacity = 1
+ self.__updateCursors()
+
+ def setMaxLength(self, maxlen):
+ """
+ Set character limit of the input
+
+ @param maxlen: max number of character allowed
+ """
+ self.__maxLength = maxlen
+
+ def clearFocus(self):
+ """
+ Compact form to blur the TextArea
+ """
+ self.opacity = self.__blurOpacity
+ self.__hasFocus = False
+
+ def setFocus(self, hasFocus):
+ """
+ Force the focus (or blur) of this TextArea
+
+ @param hasFocus: boolean
+ """
+ if self.__focusContext is not None:
+ self.__focusContext.resetFocuses()
+
+ if hasFocus:
+ self.opacity = 1
+ self.__cursorContainer.opacity = 1
+ else:
+ self.clearFocus()
+ self.__cursorContainer.opacity = 0
+
+ self.__hasFocus = hasFocus
+
+ def hasFocus(self):
+ """
+ Query the focus status for this TextArea
+ """
+ return self.__hasFocus
+
+ def showCursor(self, show):
+ if show:
+ avg.fadeIn(self.__cursorNode, 200)
+ if self.__loupe:
+ avg.fadeIn(self.__loupeCursorNode, 200)
+ else:
+ avg.fadeOut(self.__cursorNode, 200)
+ if self.__loupe:
+ avg.fadeOut(self.__loupeCursorNode, 200)
+
+ def onKeyDown(self, keycode):
+ """
+ Inject a keycode into TextArea flow
+
+ Used mainly by FocusContext. It can be used directly, but the best option
+ is always to use a FocusContext helper, which exposes convenience method for
+ injection.
+ @param keycode: characted to insert
+ @type keycode: int (SDL reference)
+ """
+ # Ensure that the cursor is shown
+ if self.__flashingCursor:
+ self.__cursorContainer.opacity = 1
+
+ if keycode in KEYCODES_BACKSPACE:
+ self.__removeChar(left=True)
+ self.__updateLastActivity()
+ self.__updateCursors()
+ elif keycode == KEYCODES_DEL:
+ self.__removeChar(left=False)
+ self.__updateLastActivity()
+ self.__updateCursors()
+ # NP/FF clears text
+ elif keycode == KEYCODE_FORMFEED:
+ self.clearText()
+ elif keycode in (KEYCODE_CRS_UP, KEYCODE_CRS_DOWN, KEYCODE_CRS_LEFT,
+ KEYCODE_CRS_RIGHT):
+ if keycode == KEYCODE_CRS_LEFT and self.__cursorPosition > 0:
+ self.__cursorPosition -= 1
+ self.__update()
+ elif (keycode == KEYCODE_CRS_RIGHT and
+ self.__cursorPosition < len(self.__data)):
+ self.__cursorPosition += 1
+ self.__update()
+ elif keycode == KEYCODE_CRS_UP and self.__cursorPosition != 0:
+ self.__cursorPosition = 0
+ self.__update()
+ elif (keycode == KEYCODE_CRS_DOWN and
+ self.__cursorPosition != len(self.__data)):
+ self.__cursorPosition = len(self.__data)
+ self.__update()
+ # add linefeed only on multiline textareas
+ elif keycode == KEYCODE_LINEFEED and self.__isMultiline:
+ self.__appendUChar('\n')
+ # avoid shift-tab, return, zero, delete
+ elif keycode not in (KEYCODE_LINEFEED, 0, 25, 63272):
+ self.__appendKeycode(keycode)
+ self.__updateLastActivity()
+ self.__updateCursors()
+
+ def __onClick(self, e):
+ if self.__focusContext is not None:
+ if self.__focusContext.isActive():
+ self.setFocus(True)
+ else:
+ self.setFocus(True)
+
+ def __getUnicodeFromData(self):
+ return u''.join(self.__data)
+
+ def __appendKeycode(self, keycode):
+ self.__appendUChar(unichr(keycode))
+
+ def __appendUChar(self, uchar):
+ # if maximum number of char is specified, honour the limit
+ if self.__maxLength > -1 and len(self.__data) > self.__maxLength:
+ return
+
+ # Boundary control
+ if len(self.__data) > 0:
+ maxCharDim = self.__textNode.fontsize
+ lastCharPos = self.__textNode.getGlyphPos(len(self.__data) - 1)
+ if self.__isMultiline:
+ if lastCharPos[1] + maxCharDim*2 > self.height - self.__border[1]*2:
+ if lastCharPos[0] + maxCharDim*1.5 > self.width - self.__border[0]*2:
+ return
+ if ord(uchar) == 10:
+ return
+ else:
+ if lastCharPos[0] + maxCharDim*1.5 > self.width - self.__border[0]*2:
+ return
+
+ self.__data.insert(self.__cursorPosition, uchar)
+ self.__cursorPosition += 1
+ self.__update()
+
+ def __removeChar(self, left=True):
+ if left and self.__cursorPosition > 0:
+ self.__cursorPosition -= 1
+ del self.__data[self.__cursorPosition]
+ self.__update()
+ elif not left and self.__cursorPosition < len(self.__data):
+ del self.__data[self.__cursorPosition]
+ self.__update()
+
+ def __update(self):
+ self.__textNode.text = self.__getUnicodeFromData()
+ if self.__loupe:
+ self.__loupeTextNode.text = self.__getUnicodeFromData()
+ self.__updateCursors()
+
+ def __updateCursors(self):
+ self.__updateCursor(self.__cursorNode, self.__cursorContainer, self.__textNode)
+ if self.__loupe:
+ self.__updateCursor(self.__loupeCursorNode, self.__loupeCursorContainer,
+ self.__loupeTextNode)
+
+ def __updateCursor(self, cursorNode, cursorContainer, textNode):
+ if self.__cursorPosition == 0:
+ lastCharPos = (0,0)
+ lastCharExtents = (0,0)
+ else:
+ lastCharPos = textNode.getGlyphPos(self.__cursorPosition - 1)
+ lastCharExtents = textNode.getGlyphSize(self.__cursorPosition - 1)
+
+ if self.__data[self.__cursorPosition - 1] == '\n':
+ lastCharPos = (0, lastCharPos[1] + lastCharExtents[1])
+ lastCharExtents = (0, lastCharExtents[1])
+
+ xPos = cursorNode.pos2.x
+ cursorNode.pos2 = Point2D(xPos, textNode.realFontSize.y * \
+ (1 - CURSOR_PADDING_PCT/100.0))
+
+ if textNode.alignment != "left":
+ if len(self.__data) > 0:
+ lineWidth = textNode.getLineExtents(self.__selectTextLine(lastCharPos,
+ textNode))
+ else:
+ lineWidth = Point2D(0,0)
+ if textNode.alignment == "center":
+ lineWidth *= 0.5
+ cursorContainer.x = textNode.alignmentOffset.x - lineWidth.x + \
+ lastCharPos[0] + lastCharExtents[0] + self.__border[0]
+ else:
+ cursorContainer.x = lastCharPos[0] + lastCharExtents[0] + self.__border[0]
+ cursorContainer.y = (lastCharPos[1] +
+ cursorNode.pos2.y * CURSOR_PADDING_PCT/200.0 + self.__border[1])
+
+ def __updateLastActivity(self):
+ self.__lastActivity = time.time()
+
+ def __tickFlashCursor(self):
+ if (self.__flashingCursor and
+ self.__hasFocus and
+ time.time() - self.__lastActivity > CURSOR_FLASH_AFTER_INACTIVITY/1000.0):
+ if self.__cursorContainer.opacity == 0:
+ self.__cursorContainer.opacity = 1
+ if self.__loupe:
+ self.__loupeCursorContainer.opacity = 1
+ else:
+ self.__cursorContainer.opacity = 0
+ if self.__loupe:
+ self.__loupeCursorContainer.opacity = 0
+ elif self.__hasFocus:
+ self.__cursorContainer.opacity = 1
+ if self.__loupe:
+ self.__loupeCursorContainer.opacity = 1
+
+ def __moveHandler(self, offset):
+ self.__addLoupe()
+ event = player.getCurrentEvent()
+ eventPos = self.getRelPos(event.pos)
+ if ( (eventPos[0] >= -1 and eventPos[0] <= self.size[0]) and
+ (eventPos[1] >= 0 and eventPos[1] <= self.size[1]) ):
+ self.__updateCursorPosition(event)
+ else:
+ self.__upHandler(None)
+
+ def __detectedHandler(self):
+ event = player.getCurrentEvent()
+ self.__updateCursorPosition(event)
+ self.__timerID = player.setTimeout(1000, self.__addLoupe)
+
+ def __addLoupe(self):
+ if not self.__loupe.getParent():
+ self.appendChild(self.__loupe)
+
+ def __upHandler (self, offset):
+ player.clearInterval(self.__timerID)
+ if self.__loupe.getParent():
+ self.__loupe.unlink()
+
+ def __selectTextLine(self, pos, textNode):
+ for line in range(textNode.getNumLines()):
+ curLine = textNode.getLineExtents(line)
+ minMaxHight = (curLine[1] * line,curLine[1] * (line + 1) )
+ if pos[1] >= minMaxHight[0] and pos[1] < minMaxHight[1]:
+ return line
+ return 0
+
+ def __updateCursorPosition(self, event):
+ eventPos = self.__textNode.getRelPos(event.pos)
+ if len(self.__data) > 0:
+ lineWidth = self.__textNode.getLineExtents(self.__selectTextLine(eventPos,
+ self.__textNode))
+ else:
+ lineWidth = Point2D(0,0)
+ if self.__textNode.alignment != "left":
+ if self.__textNode.alignment == "center":
+ eventPos = Point2D(eventPos.x + lineWidth.x / 2, eventPos.y)
+ else:
+ eventPos = Point2D(eventPos.x + lineWidth.x, eventPos.y)
+ length = len(self.__data)
+ if length > 0:
+ index = self.__textNode.getCharIndexFromPos(eventPos) # click on letter
+ if index == None: # click behind line
+ realLines = self.__textNode.getNumLines() - 1
+ for line in range(realLines + 1):
+ curLine = self.__textNode.getLineExtents(line)
+ minMaxHight = (curLine[1] * line,curLine[1] * (line + 1) )
+ if eventPos[1] >= minMaxHight[0] and eventPos[1] < minMaxHight[1]:
+ if curLine[0] != 0: # line with letters
+ correction = 1
+ if self.__textNode.alignment != "left":
+ if eventPos[0] < 0:
+ targetLine = (1, curLine[1] * line)
+ correction = 0
+ else:
+ targetLine = (curLine[0] - 1, curLine[1] * line)
+ else:
+ targetLine = (curLine[0] - 1, curLine[1] * line)
+ index = (self.__textNode.getCharIndexFromPos(targetLine)
+ + correction)
+ else: # empty line
+ count = 0
+ for char in range(length-1):
+ if count < line:
+ if self.__textNode.text[char] == "\n":
+ count += 1
+ else:
+ index = char
+ break
+ break
+ if index == None: # click under text
+ curLine = self.__textNode.getLineExtents(realLines)
+ curLine *= realLines
+ index = self.__textNode.getCharIndexFromPos( (eventPos[0],curLine[1]) )
+ if index == None:
+ index = length
+ self.__cursorPosition = index
+
+ self.__update()
+ self.__updateLoupe(event)
+
+ def __updateLoupe(self, event):
+ # setzt es mittig ueber das orginal
+# self.__zoomedImage.pos = - self.getRelPos(event.pos) + self.__loupe.size / 2.0
+ # add zoomfactor position
+# self.__zoomedImage.pos = - self.getRelPos(event.pos) + self.__loupe.size / 2.0 -\
+# ( 0.0,(self.__textNode.fontsize * self.__loupeZoomFactor))
+ # add scrolling | without zoom positioning
+
+ self.__zoomedImage.pos = - self.getRelPos(event.pos) + self.__loupe.size / 2.0 - \
+ self.getRelPos(event.pos)* self.__loupeZoomFactor + Point2D(0,5)
+ self.__loupe.pos = self.getRelPos(event.pos) - self.__loupeOffset
+##################################
+# MODULE FUNCTIONS
+
+def init(g_avg, catchKeyboard=True, repeatDelay=0.2, charDelay=0.1):
+ """
+ Initialization routine for the module
+
+ This method should be called immediately after avg file
+ load (Player.loadFile())
+ @param g_avg: avg package
+ @param catchKeyboard: boolean, if true events from keyboard are catched
+ @param repeatDelay: wait time (seconds) before starting to repeat a key which
+ is held down
+ @param charDelay: delay among character repetition (of an steadily pressed key)
+ """
+ global avg, g_RepeatDelay, g_CharDelay
+ avg = g_avg
+ g_RepeatDelay = repeatDelay
+ g_CharDelay = charDelay
+
+ player.subscribe(player.ON_FRAME, _onFrame)
+
+ if catchKeyboard:
+ player.subscribe(avg.Player.KEY_DOWN, _onKeyDown)
+ player.subscribe(avg.Player.KEY_UP, _onKeyUp)
+
+def setActiveFocusContext(focusContext):
+ """
+ Tell the module what FocusContext is presently active
+
+ Only one FocusContext at once can be set 'active' and therefore
+ prepared to receive the flow of user events from keyboard.
+ @param focusContext: set the active focusContext. If initialization has been
+ made with 'catchKeyboard' == True, the new active focusContext will receive
+ the flow of events from keyboard.
+ """
+ global g_FocusContext
+
+ if g_FocusContext is not None:
+ g_FocusContext._switchActive(False)
+
+ g_FocusContext = focusContext
+ g_FocusContext._switchActive(True)
+
+def setActivityCallback(pyfunc):
+ """
+ Set a callback that is called at every keyboard's keypress
+
+ If a callback of user interaction is needed (eg: resetting idle timeout)
+ just pass a function to this method, which is going to be called at each
+ user intervention (keydown, keyup).
+ Active focusContext will be passed as argument
+ """
+ global g_activityCallback
+ g_activityCallback = pyfunc
+
+
+def _onFrame():
+ global g_LastKeyEvent, g_LastKeyRepeated, g_CharDelay
+ if (g_LastKeyEvent is not None and
+ time.time() - g_LastKeyRepeated > g_CharDelay and
+ g_FocusContext is not None):
+ g_FocusContext.keyUCodePressed(g_LastKeyEvent.unicode)
+ g_LastKeyRepeated = time.time()
+
+def _onKeyDown(e):
+ global g_LastKeyEvent, g_LastKeyRepeated, g_RepeatDelay, g_activityCallback
+
+ if e.unicode == 0:
+ return
+
+ g_LastKeyEvent = e
+ g_LastKeyRepeated = time.time() + g_RepeatDelay
+
+ if g_FocusContext is not None:
+ g_FocusContext.keyUCodePressed(e.unicode)
+
+ if g_activityCallback is not None:
+ g_activityCallback(g_FocusContext)
+
+def _onKeyUp(e):
+ global g_LastKeyEvent
+
+ g_LastKeyEvent = None
diff --git a/src/python/utils.py b/src/python/utils.py
new file mode 100644
index 0000000..86f76c0
--- /dev/null
+++ b/src/python/utils.py
@@ -0,0 +1,63 @@
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is Martin Heistermann <mh at sponc dot de>
+#
+
+import os
+
+from libavg import avg, mathutil, player
+
+
+def getMediaDir(_file_=None, subdir='media'):
+ """call with _file_=__file__"""
+ if _file_ == None:
+ _file_ = __file__
+ myDir = os.path.dirname(_file_)
+ mediaDir = os.path.join(myDir, subdir)
+ return os.path.abspath(mediaDir)
+
+def getMediaDirFromNode(node, path=''):
+ '''
+ Recursively build the mediadir path, starting from the given node.
+ '''
+ if node.getParent():
+ if type(node) in (avg.DivNode, avg.AVGNode):
+ return getMediaDirFromNode(node.getParent(), os.path.join(node.mediadir, path))
+ else:
+ return getMediaDirFromNode(node.getParent(), path)
+ else:
+ return path
+
+def createImagePreviewNode(maxSize, absHref):
+ node = player.createNode('image', {'href': absHref})
+ node.size = mathutil.getScaledDim(node.size, max = maxSize)
+ return node
+
+def initFXCache(numFXNodes):
+ nodes = []
+ mediadir = os.path.join(os.path.dirname(__file__), 'data')
+ for i in range(numFXNodes):
+ node = avg.ImageNode(href=mediadir+"/black.png",
+ parent=player.getRootNode())
+ node.setEffect(avg.NullFXNode())
+ nodes.append(node)
+ for node in nodes:
+ node.unlink(True)
+
diff --git a/src/python/widget/Makefile.am b/src/python/widget/Makefile.am
new file mode 100644
index 0000000..ad04474
--- /dev/null
+++ b/src/python/widget/Makefile.am
@@ -0,0 +1,3 @@
+pkgwidgetdir = $(pkgpyexecdir)/widget
+pkgwidget_PYTHON = __init__.py button.py keyboard.py scrollarea.py slider.py base.py \
+ skin.py mediacontrol.py
diff --git a/src/python/widget/__init__.py b/src/python/widget/__init__.py
new file mode 100644
index 0000000..c94febb
--- /dev/null
+++ b/src/python/widget/__init__.py
@@ -0,0 +1,7 @@
+from base import SwitchNode, HStretchNode, VStretchNode, HVStretchNode, Orientation
+from button import Button, BmpButton, TextButton, ToggleButton, BmpToggleButton, CheckBox
+from keyboard import Keyboard
+from scrollarea import ScrollPane, ScrollArea
+from skin import Skin
+from slider import Slider, ScrollBar, ScrollBarTrack, ScrollBarThumb, SliderThumb, ProgressBar
+from mediacontrol import TimeSlider, MediaControl
diff --git a/src/python/widget/base.py b/src/python/widget/base.py
new file mode 100644
index 0000000..7a898e6
--- /dev/null
+++ b/src/python/widget/base.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+from libavg import avg, player
+
+class Orientation():
+ VERTICAL = 0
+ HORIZONTAL = 1
+
+
+def bmpFromSrc(src):
+ if isinstance(src, basestring):
+ return avg.Bitmap(src)
+ elif isinstance(src, avg.Bitmap):
+ return src
+ else:
+ raise RuntimeError("src must be a string or a Bitmap.")
+
+class _StretchNodeBase(avg.DivNode):
+
+ def __init__(self, src=None, parent=None, **kwargs):
+ super(_StretchNodeBase, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ if isinstance(src, avg.Bitmap):
+ self._bmp = src
+ else:
+ self._bmp = bmpFromSrc(src)
+
+ self.subscribe(self.SIZE_CHANGED, self._positionNodes)
+
+ def _initNodes(self):
+ self._setSizeFromBmp(self._bmp)
+ self._positionNodes(self.size)
+
+ if player.isPlaying():
+ self._renderImages()
+ else:
+ player.subscribe(avg.Player.PLAYBACK_START, self._renderImages)
+
+ def _setSizeFromBmp(self, bmp):
+ size = bmp.getSize()
+ if self.width == 0:
+ self.width = size.x
+ if self.height == 0:
+ self.height = size.y
+
+ def _checkExtents(self, endsExtent, minExtent):
+ if endsExtent < 0:
+ raise RuntimeError(
+ "Illegal value for endsExtent: %i. Must be >= 0"%endsExtent)
+ elif endsExtent == 0:
+ # 1 has same effect as 0 - we just create one-pixel wide start and end images.
+ endsExtent = 1
+
+ if minExtent == -1:
+ minExtent = endsExtent*2+1
+ else:
+ minExtent = minExtent
+ return (endsExtent, minExtent)
+
+ def _renderImage(self, srcBmp, node, pos, size):
+ canvas = player.createCanvas(id="stretch_canvas", size=size)
+ img = avg.ImageNode(pos=pos, parent=canvas.getRootNode())
+ img.setBitmap(srcBmp)
+ canvas.render()
+ node.setBitmap(canvas.screenshot())
+ player.deleteCanvas("stretch_canvas")
+
+
+class HStretchNode(_StretchNodeBase):
+
+ def __init__(self, endsExtent, minExtent=-1, **kwargs):
+ super(HStretchNode, self).__init__(**kwargs)
+
+ (self.__endsExtent, self.__minExtent) = self._checkExtents(endsExtent, minExtent)
+
+ self.__startImg = avg.ImageNode(parent=self)
+ self.__centerImg = avg.ImageNode(parent=self)
+ self.__endImg = avg.ImageNode(parent=self)
+
+ self._initNodes()
+
+ def _positionNodes(self, newSize):
+ if newSize.x < self.__minExtent:
+ self.width = self.__minExtent
+ else:
+ self.__centerImg.x = self.__endsExtent
+ self.__centerImg.width = newSize.x - self.__endsExtent*2
+ self.__endImg.x = newSize.x - self.__endsExtent
+
+ def _renderImages(self):
+ height = self._bmp.getSize().y
+ self._renderImage(self._bmp, self.__startImg, (0,0), (self.__endsExtent, height))
+ self._renderImage(self._bmp, self.__centerImg,
+ (-self.__endsExtent,0), (1, height))
+ endOffset = self._bmp.getSize().x - self.__endsExtent
+ self._renderImage(self._bmp, self.__endImg,
+ (-endOffset,0), (self.__endsExtent, height))
+
+
+class VStretchNode(_StretchNodeBase):
+
+ def __init__(self, endsExtent, minExtent=-1, **kwargs):
+ super(VStretchNode, self).__init__(**kwargs)
+
+ (self.__endsExtent, self.__minExtent) = self._checkExtents(endsExtent, minExtent)
+
+ self.__startImg = avg.ImageNode(parent=self)
+ self.__centerImg = avg.ImageNode(parent=self)
+ self.__endImg = avg.ImageNode(parent=self)
+
+ self._initNodes()
+
+ def _positionNodes(self, newSize):
+ if newSize.y < self.__minExtent:
+ self.height = self.__minExtent
+ else:
+ self.__centerImg.y = self.__endsExtent
+ self.__centerImg.height = newSize.y - self.__endsExtent*2
+ self.__endImg.y = newSize.y - self.__endsExtent
+
+ def _renderImages(self):
+ width = self._bmp.getSize().x
+ self._renderImage(self._bmp, self.__startImg, (0,0), (width, self.__endsExtent))
+ self._renderImage(self._bmp, self.__centerImg,
+ (0,-self.__endsExtent), (width, 1))
+ endOffset = self._bmp.getSize().y - self.__endsExtent
+ self._renderImage(self._bmp, self.__endImg, (0,-endOffset),
+ (width, self.__endsExtent))
+
+
+class HVStretchNode(_StretchNodeBase):
+
+ def __init__(self, endsExtent, minExtent=(-1,-1), **kwargs):
+ super(HVStretchNode, self).__init__(**kwargs)
+
+ (hEndsExtent, hMinExtent) = self._checkExtents(endsExtent[0], minExtent[0])
+ (vEndsExtent, vMinExtent) = self._checkExtents(endsExtent[1], minExtent[1])
+ self.__endsExtent = avg.Point2D(hEndsExtent, vEndsExtent)
+ self.__minExtent = avg.Point2D(hMinExtent, vMinExtent)
+
+ self.__createNodes()
+
+ self._initNodes()
+
+ def __calcNodePositions(self, newSize):
+ xPosns = (0, self.__endsExtent[0], newSize.x-self.__endsExtent[0], newSize.x)
+ yPosns = (0, self.__endsExtent[1], newSize.y-self.__endsExtent[1], newSize.y)
+
+ self.__nodePosns = []
+ for y in range(4):
+ curRow = []
+ for x in range(4):
+ curRow.append(avg.Point2D(xPosns[x], yPosns[y]))
+ self.__nodePosns.append(curRow)
+
+ def __createNodes(self):
+ self.__nodes = []
+ for y in range(3):
+ curRow = []
+ for x in range(3):
+ node = avg.ImageNode(parent=self)
+ curRow.append(node)
+ self.__nodes.append(curRow)
+
+ def _positionNodes(self, newSize):
+ newSize = avg.Point2D(
+ max(self.__minExtent.x, newSize.x),
+ max(self.__minExtent.y, newSize.y))
+
+ self.__calcNodePositions(newSize)
+
+ for y in range(3):
+ for x in range(3):
+ pos = self.__nodePosns[y][x]
+ size = self.__nodePosns[y+1][x+1] - self.__nodePosns[y][x]
+ node = self.__nodes[y][x]
+ node.pos = pos
+ node.size = size
+
+ def _renderImages(self):
+ bmpSize = self._bmp.getSize()
+ xPosns = (0, self.__endsExtent[0], bmpSize.x-self.__endsExtent[0], bmpSize.x)
+ yPosns = (0, self.__endsExtent[1], bmpSize.y-self.__endsExtent[1], bmpSize.y)
+ for y in range(3):
+ for x in range(3):
+ node = self.__nodes[y][x]
+ pos = avg.Point2D(xPosns[x], yPosns[y])
+ size = avg.Point2D(xPosns[x+1], yPosns[y+1]) - pos
+ if x == 1:
+ size.x = 1
+ if y == 1:
+ size.y = 1
+ self._renderImage(self._bmp, node, -pos, size)
+
+
+class SwitchNode(avg.DivNode):
+
+ def __init__(self, nodeMap=None, visibleid=None, parent=None, **kwargs):
+ super(SwitchNode, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__nodeMap = None
+ if nodeMap:
+ self.setNodeMap(nodeMap)
+ if visibleid:
+ self.setVisibleID(visibleid)
+
+ self.subscribe(self.SIZE_CHANGED, self.__setChildSizes)
+
+ def setNodeMap(self, nodeMap):
+ if self.__nodeMap is not None:
+ raise RuntimeError("SwitchNode.nodeMap can only be set once.")
+ self.__nodeMap = nodeMap
+ for node in self.__nodeMap.itervalues():
+ if node:
+ # Only insert child if it hasn't been inserted yet.
+ try:
+ self.indexOf(node)
+ except RuntimeError:
+ self.appendChild(node)
+ if self.size != (0,0):
+ size = self.size
+ else:
+ key = list(self.__nodeMap.keys())[0]
+ size = self.__nodeMap[key].size
+ self.size = size
+
+ def getVisibleID(self):
+ return self.__visibleid
+
+ def setVisibleID(self, visibleid):
+ if not (visibleid in self.__nodeMap):
+ raise RuntimeError("'%s' is not a registered id." % visibleid)
+ self.__visibleid = visibleid
+ for node in self.__nodeMap.itervalues():
+ node.active = False
+ self.__nodeMap[visibleid].active = True
+
+ visibleid = property(getVisibleID, setVisibleID)
+
+ def __setChildSizes(self, newSize):
+ if self.__nodeMap:
+ for node in self.__nodeMap.itervalues():
+ if node:
+ node.size = newSize
+ # Hack to support min. size in SwitchNodes containing StretchNodes
+ if node.size > newSize:
+ self.size = node.size
+ return
diff --git a/src/python/widget/button.py b/src/python/widget/button.py
new file mode 100644
index 0000000..f938e46
--- /dev/null
+++ b/src/python/widget/button.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this file is Henrik Thoms
+
+from libavg import avg, statemachine, player, gesture
+
+from base import SwitchNode, HVStretchNode
+from . import skin
+
+class _ButtonBase(avg.DivNode):
+
+ PRESSED = avg.Publisher.genMessageID()
+ RELEASED = avg.Publisher.genMessageID()
+
+ def __init__(self, parent=None, **kwargs):
+ super(_ButtonBase, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ self.publish(self.PRESSED)
+ self.publish(self.RELEASED)
+
+ def _setActiveArea(self, upNode, activeAreaNode, fatFingerEnlarge):
+ if fatFingerEnlarge:
+ if activeAreaNode:
+ raise(RuntimeError(
+ "Button: Can't specify both fatFingerEnlarge and activeAreaNode"))
+ size = upNode.size
+ minSize = 20*player.getPixelsPerMM()
+ size = avg.Point2D(max(minSize, size.x), max(minSize, size.y))
+ activeAreaNode = avg.RectNode(size=size, opacity=0, parent=self)
+ #Here we need to store a 'hard' reference to the active area node
+ #because the tap recognizer won't keep it
+ self.__activeAreaNode = activeAreaNode
+ else:
+ if activeAreaNode == None:
+ activeAreaNode = self
+ else:
+ self.appendChild(activeAreaNode)
+
+ self._tapRecognizer = gesture.TapRecognizer(activeAreaNode,
+ possibleHandler=self._onDown,
+ detectedHandler=self._onTap,
+ failHandler=self._onTapFail)
+
+
+class Button(_ButtonBase):
+
+ CLICKED = avg.Publisher.genMessageID()
+
+ def __init__(self, upNode, downNode, disabledNode=None, activeAreaNode=None,
+ enabled=True, fatFingerEnlarge=False, **kwargs):
+ super(Button, self).__init__(**kwargs)
+
+ if disabledNode == None:
+ disabledNode = upNode
+
+ nodeMap = {
+ "UP": upNode,
+ "DOWN": downNode,
+ "DISABLED": disabledNode
+ }
+ self.__switchNode = SwitchNode(nodeMap=nodeMap, visibleid="UP", parent=self)
+ self.publish(self.CLICKED)
+
+ self.__stateMachine = statemachine.StateMachine("Button", "UP")
+ self.__stateMachine.addState("UP", ("DOWN", "DISABLED"),
+ enterFunc=self._enterUp, leaveFunc=self._leaveUp)
+ self.__stateMachine.addState("DOWN", ("UP", "DISABLED"),
+ enterFunc=self._enterDown, leaveFunc=self._leaveDown)
+ self.__stateMachine.addState("DISABLED", ("UP",),
+ enterFunc=self._enterDisabled, leaveFunc=self._leaveDisabled)
+
+ self._setActiveArea(upNode, activeAreaNode, fatFingerEnlarge)
+
+ if not(enabled):
+ self.setEnabled(False)
+
+ def getEnabled(self):
+ return self.__stateMachine.state != "DISABLED"
+
+ def setEnabled(self, enabled):
+ if enabled:
+ if self.__stateMachine.state == "DISABLED":
+ self.__stateMachine.changeState("UP")
+ else:
+ if self.__stateMachine.state != "DISABLED":
+ self.__stateMachine.changeState("DISABLED")
+
+ enabled = property(getEnabled, setEnabled)
+
+ def _getState(self):
+ return self.__stateMachine.state
+
+ def _onDown(self):
+ self.__stateMachine.changeState("DOWN")
+ self.notifySubscribers(self.PRESSED, [])
+
+ def _onTap(self):
+ self.__stateMachine.changeState("UP")
+ self.notifySubscribers(self.CLICKED, [])
+ self.notifySubscribers(self.RELEASED, [])
+
+ def _onTapFail(self):
+ self.__stateMachine.changeState("UP")
+ self.notifySubscribers(self.RELEASED, [])
+
+ def _enterUp(self):
+ self.__setActiveNode()
+
+ def _leaveUp(self):
+ pass
+
+ def _enterDown(self):
+ self.__setActiveNode()
+
+ def _leaveDown(self):
+ pass
+
+ def _enterDisabled(self):
+ self.__setActiveNode()
+ self._tapRecognizer.enable(False)
+
+ def _leaveDisabled(self):
+ self._tapRecognizer.enable(True)
+
+ def __setActiveNode(self):
+ self.__switchNode.visibleid = self.__stateMachine.state
+
+
+class BmpButton(Button):
+
+ def __init__(self, upSrc, downSrc, disabledSrc=None, **kwargs):
+ upNode = avg.ImageNode(href=upSrc)
+ downNode = avg.ImageNode(href=downSrc)
+ if disabledSrc != None:
+ disabledNode = avg.ImageNode(href=disabledSrc)
+ else:
+ disabledNode = None
+ super(BmpButton, self).__init__(upNode=upNode, downNode=downNode,
+ disabledNode=disabledNode, **kwargs)
+
+
+class TextButton(Button):
+
+ def __init__(self, text, skinObj=skin.Skin.default, **kwargs):
+ size = avg.Point2D(kwargs["size"])
+ cfg = skinObj.defaultTextButtonCfg
+
+ self.wordsNodes = []
+
+ upNode = self.__createStateNode(size, cfg, "upBmp", text, "font")
+ downNode = self.__createStateNode(size, cfg, "downBmp", text, "downFont")
+ if "disabledBmp" in cfg:
+ disabledNode = self.__createStateNode(size, cfg, "disabledBmp", text,
+ "disabledFont")
+ else:
+ disabledNode = None
+
+ super(TextButton, self).__init__(upNode=upNode, downNode=downNode,
+ disabledNode=disabledNode, **kwargs)
+
+ def __createStateNode(self, size, cfg, bmpName, text, fontStyleName):
+ stateNode = avg.DivNode(size=size)
+ endsExtent = eval(cfg["endsExtent"], {}, {})
+ HVStretchNode(size=size, src=cfg[bmpName], endsExtent=endsExtent,
+ parent=stateNode)
+ words = avg.WordsNode(text=text, fontstyle=cfg[fontStyleName], parent=stateNode)
+ words.pos = (round((size.x-words.size.x)/2), round((size.y-words.size.y)/2))
+ self.wordsNodes.append(words)
+ return stateNode
+
+ def getText(self):
+ return self.wordsNodes[0].text
+
+ def setText(self, text):
+ for node in self.wordsNodes:
+ node.text = text
+ node.pos = (self.size-node.size)/2
+
+ text = property(getText, setText)
+
+
+class ToggleButton(_ButtonBase):
+
+ TOGGLED = avg.Publisher.genMessageID()
+
+ def __init__(self, uncheckedUpNode, uncheckedDownNode, checkedUpNode, checkedDownNode,
+ uncheckedDisabledNode=None, checkedDisabledNode=None, activeAreaNode=None,
+ enabled=True, fatFingerEnlarge=False, checked=False, **kwargs):
+ super(ToggleButton, self).__init__(**kwargs)
+ nodeMap = {
+ "UNCHECKED_UP": uncheckedUpNode,
+ "UNCHECKED_DOWN": uncheckedDownNode,
+ "CHECKED_UP": checkedUpNode,
+ "CHECKED_DOWN": checkedDownNode,
+ "UNCHECKED_DISABLED": uncheckedDisabledNode,
+ "CHECKED_DISABLED": checkedDisabledNode,
+ }
+ if uncheckedDisabledNode == None:
+ nodeMap["UNCHECKED_DISABLED"] = uncheckedUpNode
+ if checkedDisabledNode == None:
+ nodeMap["CHECKED_DISABLED"] = checkedUpNode
+ self.__switchNode = SwitchNode(nodeMap=nodeMap, visibleid="UNCHECKED_UP",
+ parent=self)
+
+ self.publish(ToggleButton.TOGGLED)
+
+ self.__stateMachine = statemachine.StateMachine("ToggleButton", "UNCHECKED_UP")
+ self.__stateMachine.addState("UNCHECKED_UP", ("UNCHECKED_DOWN",
+ "UNCHECKED_DISABLED"), enterFunc=self._enterUncheckedUp,
+ leaveFunc=self._leaveUncheckedUp)
+ self.__stateMachine.addState("UNCHECKED_DOWN", ("UNCHECKED_UP",
+ "UNCHECKED_DISABLED", "CHECKED_UP"), enterFunc=self._enterUncheckedDown,
+ leaveFunc=self._leaveUncheckedDown)
+ self.__stateMachine.addState("CHECKED_UP", ("CHECKED_DOWN", "CHECKED_DISABLED"),
+ enterFunc=self._enterCheckedUp, leaveFunc=self._leaveCheckedUp)
+ self.__stateMachine.addState("CHECKED_DOWN", ("CHECKED_UP", "UNCHECKED_UP",
+ "CHECKED_DISABLED"), enterFunc=self._enterCheckedDown,
+ leaveFunc=self._leaveCheckedDown)
+ self.__stateMachine.addState("UNCHECKED_DISABLED", ("UNCHECKED_UP",),
+ enterFunc=self._enterUncheckedDisabled,
+ leaveFunc=self._leaveUncheckedDisabled)
+ self.__stateMachine.addState("CHECKED_DISABLED", ("CHECKED_UP", ),
+ enterFunc=self._enterCheckedDisabled,
+ leaveFunc=self._leaveCheckedDisabled)
+
+ self._setActiveArea(uncheckedUpNode, activeAreaNode, fatFingerEnlarge)
+
+ if not enabled:
+ self.__stateMachine.changeState("UNCHECKED_DISABLED")
+ if checked:
+ self.setChecked(True)
+
+ def getEnabled(self):
+ return (self.__stateMachine.state != "CHECKED_DISABLED" and
+ self.__stateMachine.state != "UNCHECKED_DISABLED")
+
+ def setEnabled(self, enabled):
+ if enabled:
+ if self.__stateMachine.state == "CHECKED_DISABLED":
+ self.__stateMachine.changeState("CHECKED_UP")
+ elif self.__stateMachine.state == "UNCHECKED_DISABLED":
+ self.__stateMachine.changeState("UNCHECKED_UP")
+ else:
+ if (self.__stateMachine.state == "CHECKED_UP" or
+ self.__stateMachine.state == "CHECKED_DOWN") :
+ self.__stateMachine.changeState("CHECKED_DISABLED")
+ elif (self.__stateMachine.state == "UNCHECKED_UP" or
+ self.__stateMachine.state == "UNCHECKED_DOWN") :
+ self.__stateMachine.changeState("UNCHECKED_DISABLED")
+
+ enabled = property(getEnabled, setEnabled)
+
+ def getChecked(self):
+ return (self.__stateMachine.state != "UNCHECKED_UP" and
+ self.__stateMachine.state != "UNCHECKED_DOWN" and
+ self.__stateMachine.state != "UNCHECKED_DISABLED")
+
+ def setChecked(self, checked):
+ oldEnabled = self.getEnabled()
+ if checked:
+ if self.__stateMachine.state == "UNCHECKED_DISABLED":
+ self.__stateMachine.changeState("UNCHECKED_UP")
+ if self.__stateMachine.state == "UNCHECKED_UP":
+ self.__stateMachine.changeState("UNCHECKED_DOWN")
+ if self.__stateMachine.state != "CHECKED_UP":
+ self.__stateMachine.changeState("CHECKED_UP")
+ if not oldEnabled:
+ self.__stateMachine.changeState("CHECKED_DISABLED")
+ else:
+ if self.__stateMachine.state == "CHECKED_DISABLED":
+ self.__stateMachine.changeState("CHECKED_UP")
+ if self.__stateMachine.state == "CHECKED_UP":
+ self.__stateMachine.changeState("CHECKED_DOWN")
+ if self.__stateMachine.state != "UNCHECKED_UP":
+ self.__stateMachine.changeState("UNCHECKED_UP")
+ if not oldEnabled:
+ self.__stateMachine.changeState("UNCHECKED_DISABLED")
+
+ checked = property(getChecked, setChecked)
+
+ def _getState(self):
+ return self.__stateMachine.state
+
+ def _enterUncheckedUp(self):
+ self.__setActiveNode()
+
+ def _leaveUncheckedUp(self):
+ pass
+
+ def _enterUncheckedDown(self):
+ self.__setActiveNode()
+
+ def _leaveUncheckedDown(self):
+ pass
+
+ def _enterCheckedUp(self):
+ self.__setActiveNode()
+
+ def _leaveCheckedUp(self):
+ pass
+
+ def _enterCheckedDown(self):
+ self.__setActiveNode()
+
+ def _leaveCheckedDown(self):
+ pass
+
+ def _enterUncheckedDisabled(self):
+ self.__setActiveNode()
+ self._tapRecognizer.enable(False)
+
+ def _leaveUncheckedDisabled(self):
+ self._tapRecognizer.enable(True)
+
+ def _enterCheckedDisabled(self):
+ self.__setActiveNode()
+ self._tapRecognizer.enable(False)
+
+ def _leaveCheckedDisabled(self):
+ self._tapRecognizer.enable(True)
+
+ def _onDown(self):
+ if self.__stateMachine.state == "UNCHECKED_UP":
+ self.__stateMachine.changeState("UNCHECKED_DOWN")
+ elif self.__stateMachine.state == "CHECKED_UP":
+ self.__stateMachine.changeState("CHECKED_DOWN")
+ self.notifySubscribers(self.PRESSED, [])
+
+ def _onTap(self):
+ if self.__stateMachine.state == "UNCHECKED_DOWN":
+ self.__stateMachine.changeState("CHECKED_UP")
+ self.notifySubscribers(ToggleButton.TOGGLED, [True])
+ elif self.__stateMachine.state == "CHECKED_DOWN":
+ self.__stateMachine.changeState("UNCHECKED_UP")
+ self.notifySubscribers(ToggleButton.TOGGLED, [False])
+ self.notifySubscribers(self.RELEASED, [])
+
+ def _onTapFail(self):
+ if self.__stateMachine.state == "UNCHECKED_DOWN":
+ self.__stateMachine.changeState("UNCHECKED_UP")
+ elif self.__stateMachine.state == "CHECKED_DOWN":
+ self.__stateMachine.changeState("CHECKED_UP")
+ self.notifySubscribers(self.RELEASED, [])
+
+ def __setActiveNode(self):
+ self.__switchNode.visibleid = self.__stateMachine.state
+
+
+class CheckBox(ToggleButton):
+
+ def __init__(self, text="", skinObj=skin.Skin.default, **kwargs):
+ self.cfg = skinObj.defaultCheckBoxCfg
+
+ uncheckedUpNode = self.__createImageNode(self.cfg["uncheckedUpBmp"])
+ uncheckedDownNode = self.__createImageNode(self.cfg["uncheckedDownBmp"])
+ uncheckedDisabledNode = self.__createImageNode(self.cfg["uncheckedDisabledBmp"])
+ checkedUpNode = self.__createImageNode(self.cfg["checkedUpBmp"])
+ checkedDownNode = self.__createImageNode(self.cfg["checkedDownBmp"])
+ checkedDisabledNode = self.__createImageNode(self.cfg["checkedDisabledBmp"])
+
+ super(CheckBox, self).__init__(uncheckedUpNode=uncheckedUpNode,
+ uncheckedDownNode=uncheckedDownNode,
+ uncheckedDisabledNode=uncheckedDisabledNode,
+ checkedUpNode=checkedUpNode,
+ checkedDownNode=checkedDownNode,
+ checkedDisabledNode=checkedDisabledNode,
+ **kwargs)
+ self.textNode = avg.WordsNode(pos=(20,0), text=text, fontstyle=self.cfg["font"],
+ parent=self)
+
+ def _enterUncheckedUp(self):
+ self.textNode.fontstyle = self.cfg["font"]
+ super(CheckBox, self)._enterUncheckedUp()
+
+ def _enterUncheckedDown(self):
+ self.textNode.fontstyle = self.cfg["downFont"]
+ super(CheckBox, self)._enterUncheckedDown()
+
+ def _enterCheckedUp(self):
+ self.textNode.fontstyle = self.cfg["font"]
+ super(CheckBox, self)._enterCheckedUp()
+
+ def _enterCheckedDown(self):
+ self.textNode.fontstyle = self.cfg["downFont"]
+ super(CheckBox, self)._enterCheckedDown()
+
+ def _enterUncheckedDisabled(self):
+ self.textNode.fontstyle = self.cfg["disabledFont"]
+ super(CheckBox, self)._enterUncheckedDisabled()
+
+ def _enterCheckedDisabled(self):
+ self.textNode.fontstyle = self.cfg["disabledFont"]
+ super(CheckBox, self)._enterCheckedDisabled()
+
+ def __createImageNode(self, bmp):
+ node = avg.ImageNode()
+ node.setBitmap(bmp)
+ return node
+
+
+class BmpToggleButton(ToggleButton):
+ def __init__(self, uncheckedUpSrc, uncheckedDownSrc, checkedUpSrc, checkedDownSrc,
+ uncheckedDisabledSrc=None, checkedDisabledSrc=None, **kwargs):
+ uncheckedUpNode = avg.ImageNode(href=uncheckedUpSrc)
+ uncheckedDownNode = avg.ImageNode(href=uncheckedDownSrc)
+ checkedUpNode = avg.ImageNode(href=checkedUpSrc)
+ checkedDownNode = avg.ImageNode(href=checkedDownSrc)
+
+ if uncheckedDisabledSrc != None:
+ uncheckedDisabledNode = avg.ImageNode(href=uncheckedDisabledSrc)
+ else:
+ uncheckedDisabledNode = None
+ if checkedDisabledSrc != None:
+ checkedDisabledNode = avg.ImageNode(href=checkedDisabledSrc)
+ else:
+ checkedDisabledNode = None
+
+ super(BmpToggleButton, self).__init__(uncheckedUpNode=uncheckedUpNode,
+ uncheckedDownNode=uncheckedDownNode,
+ checkedUpNode=checkedUpNode,
+ checkedDownNode=checkedDownNode,
+ uncheckedDisabledNode=uncheckedDisabledNode,
+ checkedDisabledNode=checkedDisabledNode,
+ **kwargs)
+
diff --git a/src/python/widget/keyboard.py b/src/python/widget/keyboard.py
new file mode 100644
index 0000000..cca0afc
--- /dev/null
+++ b/src/python/widget/keyboard.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+# Original author of this module: Thomas Schott <scotty at c-base dot org>
+#
+
+import os.path
+
+from libavg import avg, player
+
+FEEDBACK_ZOOM_FACTOR = 1.0
+
+# XXX Merge with base.bmpFromSrc()
+def _bmpFromSrc(node, src):
+ if isinstance(src, basestring):
+ if os.path.isabs(src):
+ effectiveSrc = src
+ else:
+ effectiveSrc = node.getParent().getEffectiveMediaDir() + src
+ return avg.Bitmap(effectiveSrc)
+ elif isinstance(src, avg.Bitmap):
+ return src
+ elif src is None:
+ return None
+ else:
+ raise RuntimeError("src must be a string or a Bitmap.")
+
+class Key(avg.DivNode):
+ # KeyDef is (keyCode, pos, size, isCommand=False)
+ def __init__(self, keyDef, downBmp, feedbackBmp, sticky=False, parent=None,
+ **kwargs):
+ self.__keyCode = keyDef[0]
+ if not(isinstance(self.__keyCode, tuple)):
+ self.__keyCode = (self.__keyCode,)
+ kwargs['pos'] = avg.Point2D(keyDef[1])
+ kwargs['size'] = avg.Point2D(keyDef[2])
+ if len(keyDef) == 4:
+ self.__isCommand = keyDef[3]
+ else:
+ self.__isCommand = False
+ super(Key, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__sticky = sticky
+ self.__stickyIsDown = False
+ self.__cursorID = None
+ if downBmp:
+ if player.isPlaying():
+ self.__createImages(downBmp, feedbackBmp)
+ else:
+ player.subscribe(avg.Player.PLAYBACK_START,
+ lambda: self.__createImages(downBmp, feedbackBmp))
+
+ def reset(self):
+ if self.__sticky:
+ self.__image.opacity = 0.0
+ self.__stickyIsDown = False
+
+ def isCommand(self):
+ return self.__isCommand
+
+ def getCode(self):
+ return self.__keyCode
+
+ def isStickyDown(self):
+ return self.__sticky and self.__stickyIsDown
+
+ def onDown(self, event):
+ if self.__cursorID:
+ return
+ self.__cursorID = event.cursorid
+ self.__image.opacity = 1.0
+
+ def onUp(self, event):
+ if not self.__cursorID == event.cursorid:
+ return
+ if self.__sticky:
+ self.__stickyIsDown = not(self.__stickyIsDown)
+ if not self.__stickyIsDown:
+ self.__image.opacity = 0.0
+ else:
+ self.__image.opacity = 0.0
+ self.__cursorID = None
+
+ def onOut(self, event):
+ if not self.__cursorID == event.cursorid:
+ return
+ if not(self.__sticky) or (not self.__stickyIsDown):
+ self.__cursorID = None
+ self.__image.opacity = 0.0
+
+ def showFeedback(self, show):
+ if show:
+ self.__feedbackImage.opacity = 0.95
+ else:
+ self.__feedbackImage.opacity = 0.0
+
+ def __createImages(self, downBmp, feedbackBmp):
+ self.__image = avg.ImageNode(parent=self, opacity=0.0)
+ self.__createImage(self.__image, downBmp, 1)
+
+ self.__feedbackImage = avg.ImageNode(parent=self, opacity=0.0)
+ if feedbackBmp and not(self.__isCommand):
+ self.__createImage(self.__feedbackImage, feedbackBmp, 2)
+ self.__feedbackImage.pos = (-self.size.x/2, -self.size.y/3 - \
+ self.__feedbackImage.size.y)
+
+ def __createImage(self, node, bmp, sizeFactor):
+ canvas = player.createCanvas(id="keycanvas", size=self.size*sizeFactor)
+ canvasImage = avg.ImageNode(pos=-self.pos*sizeFactor, parent=canvas.getRootNode())
+ canvasImage.setBitmap(bmp)
+ canvas.render()
+ node.setBitmap(canvas.screenshot())
+ player.deleteCanvas('keycanvas')
+
+
+class Keyboard(avg.DivNode):
+
+ DOWN = avg.Publisher.genMessageID()
+ UP = avg.Publisher.genMessageID()
+ CHAR = avg.Publisher.genMessageID()
+
+ def __init__(self, bgSrc, downSrc, keyDefs, shiftKeyCode, altGrKeyCode=None,
+ stickyShift=False, feedbackSrc=None, parent=None, **kwargs):
+ super(Keyboard, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.__shiftKeyCode = shiftKeyCode
+ self.__shiftDownCounter = 0
+ self.__stickyShift = stickyShift
+ self.__altGrKeyCode = altGrKeyCode
+ self.__altGrKeyCounter = 0
+ if not(self.__shiftKeyCode) and self.__altGrKeyCode:
+ raise RuntimeError(
+ "Keyboard: If there is an altgr key, there must also be a shift key.")
+ self.__codesPerKey = 1
+ if self.__shiftKeyCode:
+ self.__codesPerKey = 2
+ if self.__altGrKeyCode:
+ self.__codesPerKey = 3
+
+ self.__keys = []
+ if bgSrc:
+ bgNode = avg.ImageNode(parent=self)
+ bgNode.setBitmap(_bmpFromSrc(self, bgSrc))
+ for kd in keyDefs:
+ downBmp = _bmpFromSrc(self, downSrc)
+ feedbackBmp = _bmpFromSrc(self, feedbackSrc)
+ if isinstance(kd[0], tuple):
+ while len(kd[0]) < self.__codesPerKey:
+ kd[0] += (kd[0][0],)
+ key = Key(kd, downBmp, feedbackBmp, parent=self)
+ else:
+ sticky =(self.__stickyShift and
+ (self.__shiftKeyCode == kd[0] or self.__altGrKeyCode == kd[0]))
+ key = Key(kd, downBmp, feedbackBmp, sticky=sticky, parent=self)
+ self.__keys.append(key)
+ self.subscribe(avg.Node.CURSOR_DOWN, self.__onCursorDown)
+ self.__curKeys = {}
+ self.__feedbackKey = None
+
+ self.publish(Keyboard.DOWN)
+ self.publish(Keyboard.UP)
+ self.publish(Keyboard.CHAR)
+
+ @classmethod
+ def makeRowKeyDefs(cls, startPos, keySize, spacing, keyStr, shiftKeyStr,
+ altGrKeyStr=None):
+ keyDefs = []
+ curPos = avg.Point2D(startPos)
+ offset = keySize[0]+spacing
+ if (len(shiftKeyStr) != len(keyStr) or
+ (altGrKeyStr and len(altGrKeyStr) != len(keyStr))):
+ raise RuntimeError("makeRowKeyDefs string lengths must be identical.")
+
+ for i in xrange(len(keyStr)):
+ if altGrKeyStr:
+ codes = (keyStr[i], shiftKeyStr[i], altGrKeyStr[i])
+ else:
+ codes = (keyStr[i], shiftKeyStr[i])
+ keyDefs.append([codes, curPos, avg.Point2D(keySize), False])
+ curPos = (curPos[0]+offset, curPos[1])
+ return keyDefs
+
+ def reset(self):
+ for key in self.__keys:
+ key.reset()
+ self.__shiftDownCounter = 0
+ self.__altGrKeyCounter = 0
+
+ def __onCursorDown(self, event):
+ curKey = self.__findKey(event.pos)
+ self.__keyDown(curKey, event)
+ event.contact.subscribe(avg.Contact.CURSOR_MOTION, self.__onCursorMotion)
+ event.contact.subscribe(avg.Contact.CURSOR_UP, self.__onCursorUp)
+
+ def __onCursorMotion(self, event):
+ newKey = self.__findKey(event.pos)
+ oldKey = self.__curKeys[event.contact]
+ if newKey != oldKey:
+ if oldKey:
+ oldKey.onOut(event)
+ self.notifySubscribers(Keyboard.UP, [oldKey.getCode()[0]])
+ if oldKey.isCommand():
+ self.__onCommandKeyUp(oldKey)
+ self.__keyDown(newKey, event)
+
+ def __onCursorUp(self, event):
+ self.__onCursorMotion(event)
+ key = self.__curKeys[event.contact]
+ if key:
+ key.onUp(event)
+ self.notifySubscribers(Keyboard.UP, [key.getCode()[0]])
+ if key.isCommand():
+ self.__onCommandKeyUp(key)
+ else:
+ self.__onCharKeyUp(key.getCode())
+ self.__switchFeedbackKey(None)
+ del self.__curKeys[event.contact]
+
+ def __findKey(self, pos):
+ for key in self.__keys:
+ localPos = key.getRelPos(pos)
+ if self.__isInside(localPos, key):
+ return key
+ return None
+
+ def __isInside(self, pos, node):
+ return (pos.x >= 0 and pos.y >= 0 and
+ pos.x <= node.size.x and pos.y <= node.size.y)
+
+ def __switchFeedbackKey(self, newKey):
+ if self.__feedbackKey:
+ self.__feedbackKey.showFeedback(False)
+ self.__feedbackKey = newKey
+ if self.__feedbackKey:
+ self.__feedbackKey.showFeedback(True)
+
+ def __keyDown(self, key, event):
+ self.__switchFeedbackKey(key)
+ self.__curKeys[event.contact] = key
+ if key:
+ key.onDown(event)
+ if key.isCommand():
+ self.__onCommandKeyDown(key)
+ else:
+ self.__onCharKeyDown(key.getCode())
+
+ def __getCharKeyCode(self, keyCodes):
+ if self.__shiftDownCounter:
+ return keyCodes[1]
+ elif self.__altGrKeyCounter:
+ return keyCodes[2]
+ else:
+ return keyCodes[0]
+
+ def __onCharKeyDown(self, keyCodes):
+ self.notifySubscribers(Keyboard.DOWN, [keyCodes[0]])
+
+ def __onCharKeyUp(self, keyCodes):
+ self.notifySubscribers(Keyboard.CHAR, [self.__getCharKeyCode(keyCodes)])
+
+ def __onCommandKeyDown(self, key):
+ keyCode = key.getCode()[0]
+ if not(key.isStickyDown()):
+ if keyCode == self.__shiftKeyCode:
+ self.__shiftDownCounter += 1
+ if keyCode == self.__altGrKeyCode:
+ self.__altGrKeyCounter += 1
+ self.notifySubscribers(Keyboard.DOWN, [keyCode])
+
+ def __onCommandKeyUp(self, key):
+ keyCode = key.getCode()[0]
+ if not(key.isStickyDown()):
+ if keyCode == self.__shiftKeyCode:
+ if self.__shiftDownCounter > 0:
+ self.__shiftDownCounter -= 1
+ else:
+ avg.logger.warning('Keyboard: ShiftDownCounter=0 on [%s] up'
+ %self.__shiftKeyCode)
+ elif keyCode == self.__altGrKeyCode:
+ if self.__altGrKeyCounter > 0:
+ self.__altGrKeyCounter -= 1
diff --git a/src/python/widget/mediacontrol.py b/src/python/widget/mediacontrol.py
new file mode 100644
index 0000000..3e740d0
--- /dev/null
+++ b/src/python/widget/mediacontrol.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+from libavg import avg
+from . import slider, button, skin
+
+class TimeSlider(slider.Slider):
+
+ def __init__(self, orientation=slider.Orientation.HORIZONTAL,
+ skinObj=skin.Skin.default, **kwargs):
+ self.__progressThumb = None
+ super(TimeSlider, self).__init__(skinObj=skinObj, orientation=orientation,
+ **kwargs)
+
+ if self._orientation == slider.Orientation.HORIZONTAL:
+ progressCfg = skinObj.defaultProgressBarCfg["horizontal"]
+ else:
+ progressCfg = skinObj.defaultProgressBarCfg["vertical"]
+
+ thumbUpBmp = progressCfg["thumbUpBmp"]
+ thumbDisabledBmp = skin.getBmpFromCfg(progressCfg, "thumbDisabledBmp",
+ "thumbUpBmp")
+ endsExtent = progressCfg["thumbEndsExtent"]
+ self.__progressThumb = slider.ScrollBarThumb(orientation=orientation,
+ upBmp=thumbUpBmp, downBmp=thumbUpBmp, disabledBmp=thumbDisabledBmp,
+ endsExtent=endsExtent)
+ self.insertChildAfter(self.__progressThumb, self._trackNode)
+ self._positionNodes()
+
+ def _positionNodes(self, newSliderPos=None):
+ super(TimeSlider, self)._positionNodes(newSliderPos)
+
+ if self.__progressThumb:
+ if self._orientation == slider.Orientation.HORIZONTAL:
+ self.__progressThumb.width = self._thumbNode.x+self._thumbNode.width/2
+ else:
+ self.__progressThumb.height = self._thumbNode.y+self._thumbNode.height/2
+
+
+class MediaControl(avg.DivNode):
+
+ PLAY_CLICKED = avg.Publisher.genMessageID()
+ PAUSE_CLICKED = avg.Publisher.genMessageID()
+ SEEK_PRESSED = avg.Publisher.genMessageID()
+ SEEK_MOTION = avg.Publisher.genMessageID()
+ SEEK_RELEASED = avg.Publisher.genMessageID()
+
+ def __init__(self, skinObj=skin.Skin.default, duration=1000, time=0, parent=None,
+ **kwargs):
+ super(MediaControl, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ cfg = skinObj.defaultMediaControlCfg
+
+ # subscribe to button & slider changes
+ self._playButton = button.ToggleButton(
+ uncheckedUpNode=self.__createImageNode(cfg, "playUpBmp"),
+ uncheckedDownNode=self.__createImageNode(cfg, "playDownBmp"),
+ uncheckedDisabledNode=self.__createImageNode(cfg, "playDisabledBmp",
+ "playUpBmp"),
+ checkedUpNode=self.__createImageNode(cfg, "pauseUpBmp"),
+ checkedDownNode=self.__createImageNode(cfg, "pauseDownBmp"),
+ checkedDisabledNode=self.__createImageNode(cfg, "pauseDisabledBmp",
+ "pauseUpBmp"),
+ parent=self)
+ self._playButton.subscribe(button.ToggleButton.TOGGLED, self.__onTogglePlay)
+
+ sliderWidth = self.width + cfg["barRight"] - cfg["barPos"][0]
+ self._timeSlider = TimeSlider(skinObj=skinObj, pos=cfg["barPos"],
+ width=sliderWidth, parent=self)
+ self._timeSlider.subscribe(TimeSlider.PRESSED, self.__onSliderPressed)
+ self._timeSlider.subscribe(TimeSlider.RELEASED, self.__onSliderReleased)
+ self._timeSlider.subscribe(TimeSlider.THUMB_POS_CHANGED, self.__onSliderMotion)
+
+ self._timeNode = avg.WordsNode(pos=cfg["timePos"], fontstyle=cfg["font"],
+ color="FFFFFF", parent=self)
+ timeLeftPos = (self.width+cfg["timeLeftPos"][0], cfg["timeLeftPos"][1])
+ self._timeLeftNode = avg.WordsNode(pos=timeLeftPos, fontstyle=cfg["font"],
+ color="FFFFFF", parent=self)
+
+ self.setDuration(duration)
+ self.setTime(time)
+
+ self.publish(MediaControl.PLAY_CLICKED)
+ self.publish(MediaControl.PAUSE_CLICKED)
+ self.publish(MediaControl.SEEK_PRESSED)
+ self.publish(MediaControl.SEEK_MOTION)
+ self.publish(MediaControl.SEEK_RELEASED)
+
+ def play(self):
+ self._playButton.checked = True
+
+ def pause(self):
+ self._playButton.checked = False
+
+ def getDuration(self):
+ return self._timeSlider.range[1]
+
+ def setDuration(self, duration):
+ self._timeSlider.range = (0, duration-100)
+ self.__updateText()
+ duration = property(getDuration, setDuration)
+
+ def getTime(self):
+ return self._timeSlider.thumbPos
+
+ def setTime(self, curTime):
+ self._timeSlider.thumbPos = curTime
+ self.__updateText()
+ time = property(getTime, setTime)
+
+ def __onTogglePlay(self, play):
+ if play:
+ self.notifySubscribers(MediaControl.PLAY_CLICKED, [])
+ else:
+ self.notifySubscribers(MediaControl.PAUSE_CLICKED, [])
+
+ def __onSliderPressed(self):
+ self.notifySubscribers(MediaControl.SEEK_PRESSED, [])
+
+ def __onSliderReleased(self):
+ self.notifySubscribers(MediaControl.SEEK_RELEASED, [])
+
+ def __onSliderMotion(self, curTime):
+ self.__updateText()
+ self.notifySubscribers(MediaControl.SEEK_MOTION, [curTime])
+
+ def __updateText(self):
+ self._timeNode.text = self.__msToMinSec(self._timeSlider.thumbPos)
+ self._timeLeftNode.text = "-"+self.__msToMinSec(
+ (self._timeSlider.range[1]-self._timeSlider.thumbPos))
+
+ def __createImageNode(self, cfg, src, defaultSrc=None):
+ bmp = skin.getBmpFromCfg(cfg, src, defaultSrc)
+ node = avg.ImageNode()
+ node.setBitmap(bmp)
+ return node
+
+ def __msToMinSec(self, ms):
+ ms += 500
+ minutes, ms = divmod(ms, 60000)
+ seconds, ms = divmod(ms, 1000)
+ return "%d:%02d"%(minutes, seconds)
+
diff --git a/src/python/widget/scrollarea.py b/src/python/widget/scrollarea.py
new file mode 100644
index 0000000..d22877f
--- /dev/null
+++ b/src/python/widget/scrollarea.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+#
+
+from libavg import avg, gesture
+from . import slider
+from base import HVStretchNode, Orientation
+from . import skin
+
+class ScrollPane(avg.DivNode):
+
+ def __init__(self, contentNode, parent=None, **kwargs):
+
+ super(ScrollPane, self).__init__(crop=True, **kwargs)
+ self.registerInstance(self, parent)
+
+ self.appendChild(contentNode)
+ self._contentNode = contentNode
+
+ def setContentPos(self, pos):
+
+ def constrain(pos, limit):
+ if limit > 0:
+ # Content larger than container
+ if pos > limit:
+ pos = limit
+ elif pos < 0:
+ pos = 0
+ else:
+ # Content smaller than container
+ if pos > 0:
+ pos = 0
+ elif pos < limit:
+ pos = limit
+ return pos
+
+ maxPos = self.getMaxContentPos()
+ pos = avg.Point2D(pos)
+ pos.x = constrain(pos.x, maxPos.x)
+ pos.y = constrain(pos.y, maxPos.y)
+ self._contentNode.pos = -pos
+
+ def getContentPos(self):
+ return -self._contentNode.pos
+ contentpos = property(getContentPos, setContentPos)
+
+ def getContentSize(self):
+ return self._contentNode.size
+
+ def setContentSize(self, size):
+ self._contentNode.size = size
+ self.setContentPos(-self._contentNode.pos) # Recheck constraints.
+ contentsize = property(getContentSize, setContentSize)
+
+ def getMaxContentPos(self):
+ maxPos = avg.Point2D(self._contentNode.size - self.size)
+ if maxPos.x < 0:
+ maxPos.x = 0
+ if maxPos.y < 0:
+ maxPos.y = 0
+ return maxPos
+
+
+class ScrollArea(avg.DivNode):
+
+ PRESSED = avg.Publisher.genMessageID()
+ RELEASED = avg.Publisher.genMessageID()
+ CONTENT_POS_CHANGED = avg.Publisher.genMessageID()
+
+ def __init__(self, contentNode, size, skinObj=skin.Skin.default, enabled=True,
+ scrollBars=(Orientation.HORIZONTAL, Orientation.VERTICAL),
+ parent=None, **kwargs):
+
+ super(ScrollArea, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ self.cfg = skinObj.defaultScrollAreaCfg
+
+ self.publish(self.PRESSED)
+ self.publish(self.RELEASED)
+ self.publish(self.CONTENT_POS_CHANGED)
+
+ self.__scrollPane = ScrollPane(contentNode=contentNode, parent=self)
+
+ if "borderBmp" in self.cfg:
+ endsExtent = self.cfg["borderEndsExtent"]
+ self._borderNode = HVStretchNode(src=self.cfg["borderBmp"],
+ endsExtent=endsExtent, sensitive=False, parent=self)
+ else:
+ self._borderNode = None
+
+ sensitiveScrollBars = self.cfg["sensitiveScrollBars"]
+
+ if Orientation.HORIZONTAL in scrollBars:
+ self._hScrollBar = slider.ScrollBar(sensitive=sensitiveScrollBars,
+ parent=self, skinObj=skinObj)
+ self._hScrollBar.subscribe(slider.Slider.THUMB_POS_CHANGED,
+ self.__onHThumbMove)
+ else:
+ self._hScrollBar = None
+
+ if Orientation.VERTICAL in scrollBars:
+ self._vScrollBar = slider.ScrollBar(orientation=Orientation.VERTICAL,
+ sensitive=sensitiveScrollBars, parent=self, skinObj=skinObj)
+ self._vScrollBar.subscribe(slider.Slider.THUMB_POS_CHANGED,
+ self.__onVThumbMove)
+ else:
+ self._vScrollBar = None
+
+ self.subscribe(self.SIZE_CHANGED, self.__positionNodes)
+ self.size = size
+
+ self.__enabled = True
+ if not(enabled):
+ self.setEnabled(False)
+
+ self.recognizer = gesture.DragRecognizer(
+ eventNode=self.__scrollPane,
+ detectedHandler=self.__onDragStart,
+ moveHandler=self.__onDragMove,
+ upHandler=self.__onDragUp,
+ friction=self.cfg["friction"]
+ )
+
+ def getContentSize(self):
+ return self.__scrollPane._contentNode.size
+
+ def setContentSize(self, size):
+ self.__scrollPane.contentsize = size
+ self.__positionNodes(self.size)
+ contentsize = property(getContentSize, setContentSize)
+
+ def getContentPos(self):
+ return self.__scrollPane.contentpos
+
+ def setContentPos(self, pos):
+ self.__scrollPane.contentpos = pos
+ self.__positionNodes(self.size)
+ self.__positionThumbs(avg.Point2D(pos))
+ self.notifySubscribers(self.CONTENT_POS_CHANGED, [self.__scrollPane.contentpos])
+ contentpos = property(getContentPos, setContentPos)
+
+ def getEnabled(self):
+ return self.__enabled
+
+ def setEnabled(self, enabled):
+ if enabled and not(self.__enabled):
+ self.recognizer.enable(True)
+ elif not(enabled) and self.__enabled:
+ self.recognizer.enable(False)
+
+ if self._vScrollBar:
+ self._vScrollBar.enabled = enabled
+ if self._hScrollBar:
+ self._hScrollBar.enabled = enabled
+ self.__enabled = enabled
+ enabled = property(getEnabled, setEnabled)
+
+ def __onHThumbMove(self, thumbPos):
+ self.__scrollPane.contentpos = (thumbPos, self.__scrollPane.contentpos.y)
+ self.notifySubscribers(self.CONTENT_POS_CHANGED, [self.__scrollPane.contentpos])
+
+ def __onVThumbMove(self, thumbPos):
+ self.__scrollPane.contentpos = (self.__scrollPane.contentpos.x, thumbPos)
+ self.notifySubscribers(self.CONTENT_POS_CHANGED, [self.__scrollPane.contentpos])
+
+ def __onDragStart(self):
+ self.__dragStartPos = self.__scrollPane.contentpos
+ self.notifySubscribers(self.PRESSED, [])
+
+ def __onDragMove(self, offset):
+ contentpos = self.__dragStartPos - offset
+ self.__scrollPane.contentpos = contentpos
+ self.__positionThumbs(contentpos)
+ self.notifySubscribers(self.CONTENT_POS_CHANGED, [self.__scrollPane.contentpos])
+
+ def __onDragUp(self, offset):
+ self.__onDragMove(offset)
+ self.notifySubscribers(self.RELEASED, [])
+
+ def __positionNodes(self, size):
+ paneSize = size
+ if self._borderNode:
+ self._borderNode.size = size
+
+ margins = self.cfg["margins"]
+ if self._hScrollBar:
+ paneSize -= (0, margins[0]+margins[2])
+ if self._vScrollBar:
+ paneSize -= (margins[1]+margins[3], 0)
+ self.__scrollPane.pos = (margins[0], margins[1])
+ self.__scrollPane.size = paneSize
+
+
+ if self._hScrollBar:
+ self._hScrollBar.pos = (0, size.y-self._hScrollBar.height)
+ self._hScrollBar.width = self.__scrollPane.width
+
+ if self.__scrollPane.contentsize.x <= self.__scrollPane.width:
+ self._hScrollBar.range = (0, self.__scrollPane.width)
+ self._hScrollBar.enabled = False
+ else:
+ self._hScrollBar.range = (0, self.__scrollPane.contentsize.x)
+ self._hScrollBar.enabled = True
+ self._hScrollBar.thumbExtent = self.__scrollPane.width
+
+ if self._vScrollBar:
+ self._vScrollBar.pos = (size.x-self._vScrollBar.width, 0)
+ self._vScrollBar.height = self.__scrollPane.height
+
+ if self.__scrollPane.contentsize.y <= self.__scrollPane.height:
+ self._vScrollBar.range = (0, self.__scrollPane.height)
+ self._vScrollBar.enabled = False
+ else:
+ self._vScrollBar.range = (0, self.__scrollPane.contentsize.y)
+ self._vScrollBar.enabled = True
+ self._vScrollBar.thumbExtent = self.__scrollPane.height
+
+ def __positionThumbs(self, contentPos):
+ if self._hScrollBar:
+ self._hScrollBar.thumbPos = contentPos.x
+ if self._vScrollBar:
+ self._vScrollBar.thumbPos = contentPos.y
+
diff --git a/src/python/widget/skin.py b/src/python/widget/skin.py
new file mode 100644
index 0000000..9490d40
--- /dev/null
+++ b/src/python/widget/skin.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+from libavg import avg
+
+import os, copy
+import xml.etree.ElementTree as ET
+
+class Skin:
+
+ default = None
+
+ def __init__(self, skinXmlFName, mediaDir=""):
+ global defaultMediaDir
+ self.__mediaDir = defaultMediaDir if mediaDir == "" else mediaDir
+ schemaFName = defaultMediaDir+"skin.xsd"
+ schemaString = open(schemaFName, "r").read()
+ skinPath = os.path.join(self.__mediaDir, skinXmlFName)
+ xmlString = open(skinPath, "r").read()
+ avg.validateXml(xmlString, schemaString, skinXmlFName, schemaFName)
+
+ xmlRoot = ET.fromstring(xmlString)
+
+ self.fonts = {}
+ for fontNode in xmlRoot.findall("fontdef"):
+ fontid, attrs = self.__splitAttrs(fontNode)
+ if "baseid" in attrs:
+ self.fonts[fontid] = copy.copy(self.fonts[attrs["baseid"]])
+ font = self.fonts[fontid]
+ del attrs["baseid"]
+ for (key, value) in attrs.iteritems():
+ setattr(font, key, value)
+ else:
+ kwargs = self.__extractArgs(attrs,
+ ("fontsize", "letterspacing", "linespacing"))
+ self.fonts[fontid] = avg.FontStyle(**kwargs)
+
+ self.textButtonCfg, self.defaultTextButtonCfg = self.__parseElement(
+ xmlRoot, "textbutton",
+ bmpArgNames={"upSrc": "upBmp", "downSrc": "downBmp",
+ "disabledSrc": "disabledBmp"},
+ fontArgNames=("font", "downFont", "disabledFont"))
+
+ self.checkBoxCfg, self.defaultCheckBoxCfg = self.__parseElement(
+ xmlRoot, "checkbox",
+ bmpArgNames={"uncheckedUpSrc":"uncheckedUpBmp",
+ "uncheckedDownSrc":"uncheckedDownBmp",
+ "uncheckedDisabledSrc":"uncheckedDisabledBmp",
+ "checkedUpSrc":"checkedUpBmp",
+ "checkedDownSrc":"checkedDownBmp",
+ "checkedDisabledSrc":"checkedDisabledBmp"},
+ fontArgNames=("font", "downFont", "disabledFont"))
+
+ self.sliderCfg, self.defaultSliderCfg = self.__initSliders(xmlRoot, "slider")
+ self.scrollBarCfg, self.defaultScrollBarCfg = self.__initSliders(
+ xmlRoot, "scrollbar")
+ self.progressBarCfg, self.defaultProgressBarCfg = self.__initSliders(
+ xmlRoot, "progressbar")
+
+ self.scrollAreaCfg, self.defaultScrollAreaCfg = self.__parseElement(
+ xmlRoot, "scrollarea",
+ pyArgNames=("friction","borderEndsExtent","margins",
+ "sensitiveScrollBars"),
+ bmpArgNames={"borderSrc":"borderBmp"})
+
+ self.mediaControlCfg, self.defaultMediaControlCfg = self.__parseElement(
+ xmlRoot, "mediacontrol",
+ bmpArgNames={"playUpSrc":"playUpBmp",
+ "playDownSrc":"playDownBmp",
+ "playDisabledSrc":"playDisabledBmp",
+ "pauseUpSrc":"pauseUpBmp",
+ "pauseDownSrc":"pauseDownBmp",
+ "pauseDisabledSrc":"pauseDisabledBmp"},
+ pyArgNames=("timePos", "timeLeftPos", "barPos", "barRight"),
+ fontArgNames=("font"))
+
+ def __parseElement(self, xmlRoot, elementName, pyArgNames=(), bmpArgNames={},
+ fontArgNames=()):
+ cfgMap = {}
+ defaultCfg = None
+ for node in xmlRoot.findall(elementName):
+ nodeid, attrs = self.__splitAttrs(node)
+ kwargs = self.__extractArgs(attrs, pyArgNames=pyArgNames,
+ bmpArgNames=bmpArgNames, fontArgNames=fontArgNames)
+ cfgMap[nodeid] = kwargs
+ if defaultCfg == None or nodeid == None:
+ defaultCfg = kwargs
+ return cfgMap, defaultCfg
+
+ def __splitAttrs(self, xmlNode):
+ attrs = xmlNode.attrib
+ if "id" in attrs:
+ nodeID = attrs["id"]
+ del attrs["id"]
+ else:
+ nodeID = None
+ return nodeID, attrs
+
+ def __extractArgs(self, attrs, pyArgNames=(), bmpArgNames={}, fontArgNames=()):
+ kwargs = {}
+ for (key, value) in attrs.iteritems():
+ if key in pyArgNames:
+ kwargs[key] = eval(value)
+ elif key in bmpArgNames.iterkeys():
+ argkey = bmpArgNames[key]
+ kwargs[argkey] = avg.Bitmap(os.path.join(self.__mediaDir, value))
+ elif key in fontArgNames:
+ kwargs[key] = self.fonts[value]
+ else:
+ kwargs[key] = value
+ return kwargs
+
+ def __initSliders(self, xmlRoot, typeName):
+ sliderCfg = {}
+ defaultSliderCfg = None
+ for sliderXmlNode in xmlRoot.findall(typeName):
+ (nodeID, bogus) = self.__splitAttrs(sliderXmlNode)
+ sliderCfg[nodeID] = {}
+ if defaultSliderCfg == None or nodeID == None:
+ defaultSliderCfg = sliderCfg[nodeID]
+ for xmlNode in sliderXmlNode.findall("*"):
+ # Loop through orientations (horiz, vert)
+ bogus, attrs = self.__splitAttrs(xmlNode)
+ kwargs = self.__extractArgs(attrs,
+ pyArgNames=("trackEndsExtent", "thumbEndsExtent"),
+ bmpArgNames={"trackSrc": "trackBmp",
+ "trackDisabledSrc": "trackDisabledBmp",
+ "thumbUpSrc": "thumbUpBmp",
+ "thumbDownSrc": "thumbDownBmp",
+ "thumbDisabledSrc": "thumbDisabledBmp"})
+ sliderCfg[nodeID][xmlNode.tag] = kwargs
+
+ return (sliderCfg, defaultSliderCfg)
+
+
+def getBmpFromCfg(cfg, bmpName, defaultName=None):
+ if bmpName in cfg:
+ return cfg[bmpName]
+ else:
+ return cfg[defaultName]
+
+
+defaultMediaDir = os.path.join(os.path.dirname(__file__), "..", 'data/')
+Skin.default = Skin("SimpleSkin.xml", "")
diff --git a/src/python/widget/slider.py b/src/python/widget/slider.py
new file mode 100644
index 0000000..1e63f0a
--- /dev/null
+++ b/src/python/widget/slider.py
@@ -0,0 +1,371 @@
+# -*- coding: utf-8 -*-
+# libavg - Media Playback Engine.
+# Copyright (C) 2003-2014 Ulrich von Zadow
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# Current versions can be found at www.libavg.de
+
+from libavg import avg, gesture
+from base import SwitchNode, HStretchNode, VStretchNode, Orientation
+from . import skin
+
+import math
+
+
+class ScrollBarTrack(SwitchNode):
+
+ def __init__(self, bmp, endsExtent, disabledBmp, orientation=Orientation.HORIZONTAL,
+ **kwargs):
+
+ super(ScrollBarTrack, self).__init__(nodeMap=None, **kwargs)
+ if orientation == Orientation.HORIZONTAL:
+ StretchNode = HStretchNode
+ else:
+ StretchNode = VStretchNode
+ self.__enabledNode = StretchNode(src=bmp, endsExtent=endsExtent, parent=self)
+ self.__disabledNode = StretchNode(src=disabledBmp, endsExtent=endsExtent,
+ parent=self)
+
+ self.setNodeMap({
+ "ENABLED": self.__enabledNode,
+ "DISABLED": self.__disabledNode
+ })
+ self.visibleid = "ENABLED"
+
+
+class ScrollBarThumb(SwitchNode):
+
+ def __init__(self, upBmp, downBmp, disabledBmp, endsExtent,
+ orientation=Orientation.HORIZONTAL, minExtent=-1,
+ **kwargs):
+
+ super(ScrollBarThumb, self).__init__(nodeMap=None, **kwargs)
+
+ if orientation == Orientation.HORIZONTAL:
+ StretchNode = HStretchNode
+ else:
+ StretchNode = VStretchNode
+ self.__upNode = StretchNode(src=upBmp, endsExtent=endsExtent, minExtent=minExtent)
+ self.__downNode = StretchNode(src=downBmp, endsExtent=endsExtent,
+ minExtent=minExtent)
+ self.__disabledNode = StretchNode(src=disabledBmp, endsExtent=endsExtent,
+ minExtent=minExtent)
+
+ self.setNodeMap({
+ "UP": self.__upNode,
+ "DOWN": self.__downNode,
+ "DISABLED": self.__disabledNode
+ })
+ self.visibleid = "UP"
+
+
+class SliderThumb(SwitchNode):
+
+ def __init__(self, upBmp, downBmp, disabledBmp, **kwargs):
+ upNode = avg.ImageNode()
+ upNode.setBitmap(upBmp)
+ downNode = avg.ImageNode()
+ downNode.setBitmap(downBmp)
+ disabledNode = avg.ImageNode()
+ disabledNode.setBitmap(disabledBmp)
+ nodeMap = {"UP": upNode, "DOWN": downNode, "DISABLED": disabledNode}
+ super(SliderThumb, self).__init__(nodeMap=nodeMap, visibleid="UP", **kwargs)
+
+
+class ProgressBar(avg.DivNode):
+
+ def __init__(self, orientation, skinObj=skin.Skin.default, height=0, width=0,
+ range=(0.,1.), value=0.0, parent=None, **kwargs):
+ super(ProgressBar, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+ if orientation == Orientation.HORIZONTAL:
+ cfg = skinObj.defaultProgressBarCfg["horizontal"]
+ else:
+ cfg = skinObj.defaultProgressBarCfg["vertical"]
+
+ self._orientation = orientation
+
+ trackBmp = cfg["trackBmp"]
+ self._trackNode = ScrollBarTrack(bmp=trackBmp, disabledBmp=trackBmp,
+ endsExtent=cfg["trackEndsExtent"], orientation=self._orientation)
+ self.appendChild(self._trackNode)
+
+ thumbUpBmp = cfg["thumbUpBmp"]
+ endsExtent=cfg["thumbEndsExtent"]
+
+ self.__thumbNode = ScrollBarThumb(orientation=self._orientation,
+ upBmp=thumbUpBmp, downBmp=thumbUpBmp, disabledBmp=thumbUpBmp,
+ endsExtent=endsExtent)
+ self.appendChild(self.__thumbNode)
+
+ self.__range = range
+ self.__value = value
+
+ if orientation == Orientation.HORIZONTAL:
+ self.size = (width, trackBmp.getSize().y)
+ else:
+ self.size = (trackBmp.getSize().x, height)
+ self._positionNodes()
+
+ def getRange(self):
+ return self.__range
+
+ def setRange(self, range):
+ self.__range = (float(range[0]), float(range[1]))
+ self._positionNodes()
+ range = property(getRange, setRange)
+
+ def getValue(self):
+ return self.__value
+
+ def setValue(self, value):
+ self._positionNodes(value)
+ value = property(getValue, setValue)
+
+ def _positionNodes(self, newValue=None):
+ if newValue is not None:
+ self.__value = float(newValue)
+ if self.__value < self.__range[0]:
+ self.__value = self.__range[0]
+ if self.__value > self.__range[1]:
+ self.__value = self.__range[1]
+ self._trackNode.size = self.size
+
+ effectiveRange = math.fabs(self.__range[1] - self.__range[0])
+ normValue = ((self.__value-self.__range[0])/effectiveRange)
+ if self._orientation == Orientation.HORIZONTAL:
+ self.__thumbNode.width = normValue*self.size.x
+ else:
+ self.__thumbNode.height = normValue*self.size.y
+
+
+class SliderBase(avg.DivNode):
+
+ THUMB_POS_CHANGED = avg.Publisher.genMessageID()
+ PRESSED = avg.Publisher.genMessageID()
+ RELEASED = avg.Publisher.genMessageID()
+
+ def __init__(self, orientation, cfg, enabled=True, height=0, width=0, range=(0.,1.),
+ thumbPos=0.0, parent=None, **kwargs):
+ super(SliderBase, self).__init__(**kwargs)
+ self.registerInstance(self, parent)
+
+ self.publish(SliderBase.THUMB_POS_CHANGED)
+ self.publish(SliderBase.PRESSED)
+ self.publish(SliderBase.RELEASED)
+
+ self._orientation = orientation
+
+ trackBmp = cfg["trackBmp"]
+ trackDisabledBmp = cfg["trackDisabledBmp"]
+ self._trackNode = ScrollBarTrack(bmp=trackBmp, disabledBmp=trackDisabledBmp,
+ endsExtent=cfg["trackEndsExtent"], orientation=self._orientation)
+ self.appendChild(self._trackNode)
+
+ self._initThumb(cfg)
+
+ self._range = range
+ self._thumbPos = thumbPos
+
+ self.subscribe(self.SIZE_CHANGED, lambda newSize: self._positionNodes())
+ if orientation == Orientation.HORIZONTAL:
+ self.size = (width, trackBmp.getSize().y)
+ else:
+ self.size = (trackBmp.getSize().x, height)
+ if not(enabled):
+ self.setEnabled(False)
+
+ self.__recognizer = gesture.DragRecognizer(self._thumbNode, friction=-1,
+ detectedHandler=self.__onDragStart, moveHandler=self.__onDrag,
+ upHandler=self.__onUp)
+
+ def getRange(self):
+ return self._range
+
+ def setRange(self, range):
+ self._range = (float(range[0]), float(range[1]))
+ self._positionNodes()
+
+ # range[1] > range[0]: Reversed scrollbar.
+ range = property(getRange, setRange)
+
+ def getThumbPos(self):
+ return self._thumbPos
+
+ def setThumbPos(self, thumbPos):
+ self._positionNodes(thumbPos)
+
+ thumbPos = property(getThumbPos, setThumbPos)
+
+ def getEnabled(self):
+ return self._trackNode.visibleid != "DISABLED"
+
+ def setEnabled(self, enabled):
+ if enabled:
+ if self._trackNode.visibleid == "DISABLED":
+ self._trackNode.visibleid = "ENABLED"
+ self._thumbNode.visibleid = "UP"
+ self.__recognizer.enable(True)
+ else:
+ if self._trackNode.visibleid != "DISABLED":
+ self._trackNode.visibleid = "DISABLED"
+ self._thumbNode.visibleid = "DISABLED"
+ self.__recognizer.enable(False)
+
+ enabled = property(getEnabled, setEnabled)
+
+ def _positionNodes(self, newSliderPos=None):
+ if newSliderPos is not None:
+ self._thumbPos = float(newSliderPos)
+ self._trackNode.size = self.size
+
+ self._constrainSliderPos()
+
+ pixelRange = self._getScrollRangeInPixels()
+ if self._getSliderRange() == 0:
+ thumbPixelPos = 0
+ else:
+ thumbPixelPos = (((self._thumbPos-self._range[0])/self._getSliderRange())*
+ pixelRange)
+ if self._orientation == Orientation.HORIZONTAL:
+ self._thumbNode.x = thumbPixelPos
+ else:
+ self._thumbNode.y = thumbPixelPos
+
+ def __onDragStart(self):
+ self._thumbNode.visibleid = "DOWN"
+ self.__dragStartPos = self._thumbPos
+ self.notifySubscribers(Slider.PRESSED, [])
+
+ def __onDrag(self, offset):
+ pixelRange = self._getScrollRangeInPixels()
+ if pixelRange == 0:
+ normalizedOffset = 0
+ else:
+ if self._orientation == Orientation.HORIZONTAL:
+ normalizedOffset = offset.x/pixelRange
+ else:
+ normalizedOffset = offset.y/pixelRange
+ oldThumbPos = self._thumbPos
+ self._positionNodes(self.__dragStartPos + normalizedOffset*self._getSliderRange())
+ if self._thumbPos != oldThumbPos:
+ self.notifySubscribers(Slider.THUMB_POS_CHANGED, [self._thumbPos])
+
+ def __onUp(self, offset):
+ self.__onDrag(offset)
+ self._thumbNode.visibleid = "UP"
+ self.notifySubscribers(Slider.RELEASED, [])
+
+
+class Slider(SliderBase):
+
+ def __init__(self, orientation=Orientation.HORIZONTAL, skinObj=skin.Skin.default,
+ **kwargs):
+ if orientation == Orientation.HORIZONTAL:
+ cfg = skinObj.defaultSliderCfg["horizontal"]
+ else:
+ cfg = skinObj.defaultSliderCfg["vertical"]
+ super(Slider, self).__init__(orientation, cfg, **kwargs)
+
+ def _initThumb(self, cfg):
+ thumbUpBmp = cfg["thumbUpBmp"]
+ thumbDownBmp = skin.getBmpFromCfg(cfg, "thumbDownBmp", "thumbUpBmp")
+ thumbDisabledBmp = skin.getBmpFromCfg(cfg, "thumbDisabledBmp", "thumbUpBmp")
+ self._thumbNode = SliderThumb(upBmp=thumbUpBmp, downBmp=thumbDownBmp,
+ disabledBmp=thumbDisabledBmp)
+ self.appendChild(self._thumbNode)
+
+ def _getScrollRangeInPixels(self):
+ if self._orientation == Orientation.HORIZONTAL:
+ return self.size.x - self._thumbNode.size.x
+ else:
+ return self.size.y - self._thumbNode.size.y
+
+ def _getSliderRange(self):
+ return self._range[1] - self._range[0]
+
+ def _constrainSliderPos(self):
+ rangeMin = min(self._range[0], self._range[1])
+ rangeMax = max(self._range[0], self._range[1])
+ self._thumbPos = max(rangeMin, self._thumbPos)
+ self._thumbPos = min(rangeMax, self._thumbPos)
+
+
+class ScrollBar(SliderBase):
+
+ def __init__(self, orientation=Orientation.HORIZONTAL, skinObj=skin.Skin.default,
+ thumbExtent=0.1, **kwargs):
+ self.__thumbExtent = thumbExtent
+ if orientation == Orientation.HORIZONTAL:
+ cfg = skinObj.defaultScrollBarCfg["horizontal"]
+ else:
+ cfg = skinObj.defaultScrollBarCfg["vertical"]
+ super(ScrollBar, self).__init__(orientation=orientation, cfg=cfg, **kwargs)
+
+ def _initThumb(self, cfg):
+ thumbUpBmp = cfg["thumbUpBmp"]
+ thumbDownBmp = skin.getBmpFromCfg(cfg, "thumbDownBmp", "thumbUpBmp")
+ thumbDisabledBmp = skin.getBmpFromCfg(cfg, "thumbDisabledBmp", "thumbUpBmp")
+ endsExtent=cfg["thumbEndsExtent"]
+
+ self._thumbNode = ScrollBarThumb(orientation=self._orientation,
+ upBmp=thumbUpBmp, downBmp=thumbDownBmp,
+ disabledBmp=thumbDisabledBmp, endsExtent=endsExtent)
+ self.appendChild(self._thumbNode)
+
+ def getThumbExtent(self):
+ return self.__thumbExtent
+
+ def setThumbExtent(self, thumbExtent):
+ self.__thumbExtent = float(thumbExtent)
+ self._positionNodes()
+
+ thumbExtent = property(getThumbExtent, setThumbExtent)
+
+ def _getScrollRangeInPixels(self):
+ if self._orientation == Orientation.HORIZONTAL:
+ return self.size.x - self._thumbNode.width
+ else:
+ return self.size.y - self._thumbNode.height
+
+ def _positionNodes(self, newSliderPos=None):
+ effectiveRange = math.fabs(self._range[1] - self._range[0])
+ if self._orientation == Orientation.HORIZONTAL:
+ thumbExtent = (self.__thumbExtent/effectiveRange)*self.size.x
+ self._thumbNode.width = thumbExtent
+ else:
+ thumbExtent = (self.__thumbExtent/effectiveRange)*self.size.y
+ self._thumbNode.height = thumbExtent
+ super(ScrollBar, self)._positionNodes(newSliderPos)
+ if self._range[1] < self._range[0]:
+ # Reversed (upside-down) scrollbar
+ if self._orientation == Orientation.HORIZONTAL:
+ self._thumbNode.x -= thumbExtent
+ else:
+ self._thumbNode.y -= thumbExtent
+
+ def _getSliderRange(self):
+ if self._range[1] > self._range[0]:
+ return self._range[1] - self._range[0] - self.__thumbExtent
+ else:
+ return self._range[1] - self._range[0] + self.__thumbExtent
+
+ def _constrainSliderPos(self):
+ rangeMin = min(self._range[0], self._range[1])
+ rangeMax = max(self._range[0], self._range[1])
+ self._thumbPos = max(rangeMin, self._thumbPos)
+ self._thumbPos = min(rangeMax-self.__thumbExtent, self._thumbPos)
+