summaryrefslogtreecommitdiff
path: root/src/python/textarea.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/python/textarea.py')
-rw-r--r--src/python/textarea.py833
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