summaryrefslogtreecommitdiff
path: root/src/python/textarea.py
blob: 8b9bcf130e4662fa967450b0d085c4a7d1d2833c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
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