diff options
Diffstat (limited to 'src/python/textarea.py')
-rw-r--r-- | src/python/textarea.py | 833 |
1 files changed, 833 insertions, 0 deletions
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 |