diff options
author | Dimitri John Ledkov <xnox@ubuntu.com> | 2014-06-24 20:05:13 +0100 |
---|---|---|
committer | Dimitri John Ledkov <xnox@ubuntu.com> | 2014-06-24 20:05:13 +0100 |
commit | dd22bd15f6ed3e5eb5c77ab427029be50fe20148 (patch) | |
tree | d9491ee40d80688b7f5b1f20504f022686827a57 /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')
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 = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<", +} + +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 Binary files differnew file mode 100644 index 0000000..4bc1a37 --- /dev/null +++ b/src/python/data/CamImgBorder.png diff --git a/src/python/data/Feedback.png b/src/python/data/Feedback.png Binary files differnew file mode 100644 index 0000000..3c71f9c --- /dev/null +++ b/src/python/data/Feedback.png 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 Binary files differnew file mode 100644 index 0000000..9f5dcda --- /dev/null +++ b/src/python/data/TouchFeedback.png diff --git a/src/python/data/black.png b/src/python/data/black.png Binary files differnew file mode 100644 index 0000000..23bd512 --- /dev/null +++ b/src/python/data/black.png diff --git a/src/python/data/border.png b/src/python/data/border.png Binary files differnew file mode 100644 index 0000000..447a3e6 --- /dev/null +++ b/src/python/data/border.png diff --git a/src/python/data/button_bg_down.png b/src/python/data/button_bg_down.png Binary files differnew file mode 100644 index 0000000..c44179b --- /dev/null +++ b/src/python/data/button_bg_down.png diff --git a/src/python/data/button_bg_up.png b/src/python/data/button_bg_up.png Binary files differnew file mode 100644 index 0000000..475c9c9 --- /dev/null +++ b/src/python/data/button_bg_up.png diff --git a/src/python/data/checkbox_checked_disabled.png b/src/python/data/checkbox_checked_disabled.png Binary files differnew file mode 100644 index 0000000..da58829 --- /dev/null +++ b/src/python/data/checkbox_checked_disabled.png diff --git a/src/python/data/checkbox_checked_down.png b/src/python/data/checkbox_checked_down.png Binary files differnew file mode 100644 index 0000000..4fbbd83 --- /dev/null +++ b/src/python/data/checkbox_checked_down.png diff --git a/src/python/data/checkbox_checked_up.png b/src/python/data/checkbox_checked_up.png Binary files differnew file mode 100644 index 0000000..ca901f4 --- /dev/null +++ b/src/python/data/checkbox_checked_up.png diff --git a/src/python/data/checkbox_unchecked_disabled.png b/src/python/data/checkbox_unchecked_disabled.png Binary files differnew file mode 100644 index 0000000..e8c2116 --- /dev/null +++ b/src/python/data/checkbox_unchecked_disabled.png diff --git a/src/python/data/checkbox_unchecked_down.png b/src/python/data/checkbox_unchecked_down.png Binary files differnew file mode 100644 index 0000000..69f8282 --- /dev/null +++ b/src/python/data/checkbox_unchecked_down.png diff --git a/src/python/data/checkbox_unchecked_up.png b/src/python/data/checkbox_unchecked_up.png Binary files differnew file mode 100644 index 0000000..e354492 --- /dev/null +++ b/src/python/data/checkbox_unchecked_up.png diff --git a/src/python/data/crosshair.png b/src/python/data/crosshair.png Binary files differnew file mode 100644 index 0000000..be025b1 --- /dev/null +++ b/src/python/data/crosshair.png diff --git a/src/python/data/mpeg1-48x48-sound.avi b/src/python/data/mpeg1-48x48-sound.avi Binary files differnew file mode 100644 index 0000000..be415db --- /dev/null +++ b/src/python/data/mpeg1-48x48-sound.avi diff --git a/src/python/data/mpeg1-48x48.mov b/src/python/data/mpeg1-48x48.mov Binary files differnew file mode 100644 index 0000000..16ab499 --- /dev/null +++ b/src/python/data/mpeg1-48x48.mov diff --git a/src/python/data/pause_button_down.png b/src/python/data/pause_button_down.png Binary files differnew file mode 100644 index 0000000..c06b34a --- /dev/null +++ b/src/python/data/pause_button_down.png diff --git a/src/python/data/pause_button_up.png b/src/python/data/pause_button_up.png Binary files differnew file mode 100644 index 0000000..1a923b9 --- /dev/null +++ b/src/python/data/pause_button_up.png diff --git a/src/python/data/play_button_down.png b/src/python/data/play_button_down.png Binary files differnew file mode 100644 index 0000000..07b167b --- /dev/null +++ b/src/python/data/play_button_down.png diff --git a/src/python/data/play_button_up.png b/src/python/data/play_button_up.png Binary files differnew file mode 100644 index 0000000..e16ec9e --- /dev/null +++ b/src/python/data/play_button_up.png diff --git a/src/python/data/rgb24alpha-64x64.png b/src/python/data/rgb24alpha-64x64.png Binary files differnew file mode 100644 index 0000000..41b69c3 --- /dev/null +++ b/src/python/data/rgb24alpha-64x64.png diff --git a/src/python/data/scrollarea_border.png b/src/python/data/scrollarea_border.png Binary files differnew file mode 100644 index 0000000..2e95f57 --- /dev/null +++ b/src/python/data/scrollarea_border.png diff --git a/src/python/data/scrollbar_horiz_thumb_down.png b/src/python/data/scrollbar_horiz_thumb_down.png Binary files differnew file mode 100644 index 0000000..6dc6fd1 --- /dev/null +++ b/src/python/data/scrollbar_horiz_thumb_down.png diff --git a/src/python/data/scrollbar_horiz_thumb_up.png b/src/python/data/scrollbar_horiz_thumb_up.png Binary files differnew file mode 100644 index 0000000..02748f2 --- /dev/null +++ b/src/python/data/scrollbar_horiz_thumb_up.png diff --git a/src/python/data/scrollbar_horiz_track.png b/src/python/data/scrollbar_horiz_track.png Binary files differnew file mode 100644 index 0000000..052002a --- /dev/null +++ b/src/python/data/scrollbar_horiz_track.png diff --git a/src/python/data/scrollbar_horiz_track_disabled.png b/src/python/data/scrollbar_horiz_track_disabled.png Binary files differnew file mode 100644 index 0000000..7ac86fe --- /dev/null +++ b/src/python/data/scrollbar_horiz_track_disabled.png diff --git a/src/python/data/scrollbar_vert_thumb_down.png b/src/python/data/scrollbar_vert_thumb_down.png Binary files differnew file mode 100644 index 0000000..c7b09d1 --- /dev/null +++ b/src/python/data/scrollbar_vert_thumb_down.png diff --git a/src/python/data/scrollbar_vert_thumb_up.png b/src/python/data/scrollbar_vert_thumb_up.png Binary files differnew file mode 100644 index 0000000..f6c2f88 --- /dev/null +++ b/src/python/data/scrollbar_vert_thumb_up.png diff --git a/src/python/data/scrollbar_vert_track.png b/src/python/data/scrollbar_vert_track.png Binary files differnew file mode 100644 index 0000000..58af284 --- /dev/null +++ b/src/python/data/scrollbar_vert_track.png diff --git a/src/python/data/scrollbar_vert_track_disabled.png b/src/python/data/scrollbar_vert_track_disabled.png Binary files differnew file mode 100644 index 0000000..695b112 --- /dev/null +++ b/src/python/data/scrollbar_vert_track_disabled.png 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 Binary files differnew file mode 100644 index 0000000..6e233a5 --- /dev/null +++ b/src/python/data/slider_horiz_track.png diff --git a/src/python/data/slider_horiz_track_disabled.png b/src/python/data/slider_horiz_track_disabled.png Binary files differnew file mode 100644 index 0000000..01c880d --- /dev/null +++ b/src/python/data/slider_horiz_track_disabled.png diff --git a/src/python/data/slider_thumb_down.png b/src/python/data/slider_thumb_down.png Binary files differnew file mode 100644 index 0000000..1f2acc2 --- /dev/null +++ b/src/python/data/slider_thumb_down.png diff --git a/src/python/data/slider_thumb_up.png b/src/python/data/slider_thumb_up.png Binary files differnew file mode 100644 index 0000000..885506d --- /dev/null +++ b/src/python/data/slider_thumb_up.png diff --git a/src/python/data/slider_vert_track.png b/src/python/data/slider_vert_track.png Binary files differnew file mode 100644 index 0000000..51bac37 --- /dev/null +++ b/src/python/data/slider_vert_track.png diff --git a/src/python/data/slider_vert_track_disabled.png b/src/python/data/slider_vert_track_disabled.png Binary files differnew file mode 100644 index 0000000..bc8d7b6 --- /dev/null +++ b/src/python/data/slider_vert_track_disabled.png 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) + |