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 Single/Multi-Line editable text field widget for libavg
28
29 textarea module provides two classes:
30
31 1. TextArea
32 This is the implementation of the widget. Every instantiated TextArea
33 represents an editable text field, which can be set up with several styles
34 and behaviors.
35
36 2. FocusContext
37 This helps to easily route the events that comes from keyboards to an
38 appropriate TextArea instance, cycling focuses and dispatching events on
39 the selected field.
40
41 """
42
43 avg = None
44 g_Player = None
45 g_FocusContext = None
46 g_LastKeyEvent = None
47 g_activityCallback = None
48 g_LastKeyRepeated = 0
49 g_RepeatDelay = 0.2
50 g_CharDelay = 0.1
51
52 KEYCODE_TAB = 9
53 KEYCODE_LINEFEED = 13
54 KEYCODE_SHTAB = 25
55 KEYCODE_FORMFEED = 12
56 KEYCODE_CRS_UP = 63232
57 KEYCODE_CRS_DOWN = 63233
58 KEYCODE_CRS_LEFT = 63234
59 KEYCODE_CRS_RIGHT = 63235
60 KEYCODES_BACKSPACE = (8,127)
61 KEYCODES_DEL = 63272
62
63 CURSOR_PADDING_PCT = 15
64 CURSOR_WIDTH_PCT = 4
65 CURSOR_SPACING_PCT = 4
66 CURSOR_FLASHING_DELAY = 1000
67 CURSOR_FLASH_AFTER_INACTIVITY = 200
68
69 DEFAULT_BLUR_OPACITY = 0.3
70
71 import time
72 import platform
73
74 try:
75 from . import avg, Point2D
76 except ValueError:
77
78 if platform.system() == 'Windows':
79 from libavg import Point2D
80 else:
81 from avg import Point2D
82
83
85 """
86 This class helps to group TextArea elements
87
88 TextArea elements that belong to the same FocusContext cycle focus among
89 themselves. There can be several FocusContextes but only one active at once
90 ( using the global function setActiveFocusContext() )
91 """
93 self.__elements = []
94 self.__isActive = False
95
97 """
98 Test if this FocusContext is currently active
99 """
100 return self.__isActive
101
102 - def register(self, taElement):
103 """
104 Register a floating textarea on this FocusContext
105
106 @param taElement: a TextArea instance
107 """
108 self.__elements.append(taElement)
109
110 - def getFocused(self):
111 """
112 Query the TextArea element that currently has focus
113
114 @return: TextArea instance or None
115 """
116 for ob in self.__elements:
117 if ob.hasFocus():
118 return ob
119 return None
120
121 - def keyCharPressed(self, kchar):
122 """
123 Inject an utf-8 encoded characted into the flow
124
125 Shift a character (Unicode keycode) into the active (w/focus) TextArea
126 @type kchar: string
127 @param kchar: a single character (if more than one, the following are ignored)
128 """
129 uch = unicode(kchar, 'utf-8')
130 self.keyUCodePressed(ord(uch[0]))
131
132 - def keyUCodePressed(self, keycode):
133 """
134 Inject an Unicode code point into the flow
135
136 Shift a character (Unicode keycode) into the active (w/focus) TextArea
137 @type keycode: int
138 @param keycode: unicode code point of the character
139 """
140
141 if keycode == KEYCODE_TAB:
142 self.cycleFocus()
143 return
144
145 if keycode == KEYCODE_SHTAB:
146 self.cycleFocus(True)
147 return
148
149 for ob in self.__elements:
150 if ob.hasFocus():
151 ob.onKeyDown(keycode)
152
153 - def backspace(self):
154 """
155 Emulate a backspace character keypress
156 """
157 self.keyUCodePressed(KEYCODES_BACKSPACE[0])
158
160 """
161 Emulate a delete character keypress
162 """
163 self.keyUCodePressed(KEYCODE_DEL)
164
166 """
167 Clear the active textarea, emulating the press of FF character
168 """
169 self.keyUCodePressed(KEYCODE_FORMFEED)
170
171 - def resetFocuses(self):
172 """
173 Blur every TextArea registered within this FocusContext
174 """
175 for ob in self.__elements:
176 ob.clearFocus()
177
178 - def cycleFocus(self, backwards=False):
179 """
180 Force a focus cycle among instantiated textareas
181
182 TAB/Sh-TAB keypress is what is translated in a focus cycle.
183 @param backwards: as default, the method cycles following the order
184 that has been followed during the registration of TextArea
185 instances. Setting this to True, the order is inverted.
186 """
187
188 els = []
189 els.extend(self.__elements)
190
191 if len(els) == 0:
192 return
193
194 if backwards:
195 els.reverse()
196
197 elected = 0
198 for ob in els:
199 if not ob.hasFocus():
200 elected = elected + 1
201 else:
202 break
203
204
205
206 if elected in (len(els), len(els)-1):
207 elected = 0
208 else:
209 elected = elected + 1
210
211 for ob in els:
212 ob.setFocus(False)
213
214 els[elected].setFocus(True)
215
216 - def getRegistered(self):
217 """
218 Returns a list of TextArea currently registered within this FocusContext
219 @return: a list of registered TextArea instances
220 """
221 return self.__elements
222
223 - def _switchActive(self, active):
224 if active:
225 self.resetFocuses()
226 self.cycleFocus()
227 else:
228 self.resetFocuses()
229
230 self.__isActive = active
231
232
234 """
235 TextArea class is a libavg widget to create editable text fields
236
237 TextArea is an extended <words> node that reacts to user input (mouse/touch for
238 focus, keyboard for text input). Can be set as a single line or span to multiple
239 lines.
240 It sits in a given container matching its dimensions, therefore the appropriate
241 way to create it, is to set a <div> node with defined width/height attributes.
242 """
243 - def __init__(self, parent, focusContext=None, disableMouseFocus=False, id=''):
244 """
245 @param parent: a div node with defined dimensions
246 @param focusContext: FocusContext object which directs focus for TextArea elements
247 @param disableMouseFocus: boolean, prevents that mouse can set focus for
248 this instance
249 @param id: optional handle to identify the object when dealing with events. ID
250 uniqueness is not guaranteed
251 """
252 global g_Player
253 g_Player = avg.Player.get()
254 self.__parent = parent
255 self.__focusContext = focusContext
256 self.__blurOpacity = DEFAULT_BLUR_OPACITY
257 self.__border = 0
258 self.__id = id
259 self.__data = []
260 self.__cursorPosition = 0
261
262 textNode = g_Player.createNode("words", {'rawtextmode':True})
263
264 if not disableMouseFocus:
265 parent.setEventHandler(avg.CURSORUP, avg.MOUSE, self.__onClick)
266 parent.setEventHandler(avg.CURSORUP, avg.TOUCH, self.__onClick)
267
268 parent.appendChild(textNode)
269
270 cursorContainer = g_Player.createNode('div', {})
271 cursorNode = g_Player.createNode('line', {'color': '000000'})
272 parent.appendChild(cursorContainer)
273 cursorContainer.appendChild(cursorNode)
274 self.__flashingCursor = False
275
276 self.__cursorContainer = cursorContainer
277 self.__cursorNode = cursorNode
278 self.__textNode = textNode
279 self.__charSize = -1
280 self.setStyle()
281
282 if focusContext is not None:
283 focusContext.register(self)
284 self.setFocus(False)
285
286 g_Player.setInterval(CURSOR_FLASHING_DELAY, self.__tickFlashCursor)
287
288 self.__lastActivity = 0
289
291 """
292 Returns the ID of the textarea (set on the constructor).
293 """
294 return self.__id
295
296 - def clearText(self):
297 """
298 Clears the text
299 """
300 self.setText(u'')
301
302 - def setText(self, uString):
303 """
304 Set the text on the TextArea
305
306 @param uString: an unicode string (or an utf-8 encoded string)
307 """
308 if not isinstance(uString, unicode):
309 uString = unicode(uString, 'utf-8')
310
311 self.__data = []
312 for c in uString:
313 self.__data.append(c)
314
315 self.__cursorPosition = len(self.__data)
316 self.__update()
317
319 """
320 Get the text stored and displayed on the TextArea
321 """
322 return self.__getUnicodeFromData()
323
324 - def setStyle(self, font='Arial', fontsize=12, alignment='left', variant='Regular',
325 color='000000', multiline=True, cursorWidth=None, border=0,
326 blurOpacity=DEFAULT_BLUR_OPACITY, flashingCursor=False,
327 cursorColor='000000', lineSpacing=0, letterSpacing=0):
328 """
329 Set TextArea's graphical appearance
330 @param font: font face
331 @param fontsize: font size in pixels
332 @param alignment: one among 'left', 'right', 'center'
333 @param variant: font variant (eg: 'bold')
334 @param color: RGB hex for text color
335 @param multiline: boolean, whether TextArea has to wrap (undefinitely)
336 or stop at full width
337 @param cursorWidth: int, width of the cursor in pixels
338 @param border: amount of offsetting pixels that words node will have from image
339 extents
340 @param blurOpacity: opacity that textarea gets when goes to blur state
341 @param flashingCursor: whether the cursor should flash or not
342 @param cursorColor: RGB hex for cursor color
343 @param lineSpacing: linespacing property of words node
344 @param letterSpacing: letterspacing property of words node
345 """
346 self.__textNode.font = font
347 self.__textNode.fontsize = int(fontsize)
348 self.__textNode.alignment = alignment
349 self.__textNode.color = color
350 self.__textNode.variant = variant
351 self.__textNode.linespacing = lineSpacing
352 self.__textNode.letterspacing = letterSpacing
353 self.__isMultiline = multiline
354 self.__border = border
355 self.__maxLength = -1
356 self.__blurOpacity = blurOpacity
357
358 if multiline:
359 self.__textNode.width = int(self.__parent.width) - self.__border * 2
360 self.__textNode.wrapmode = 'wordchar'
361 else:
362 self.__textNode.width = 0
363
364 self.__textNode.x = self.__border
365 self.__textNode.y = self.__border
366
367 self.__cursorNode.color = cursorColor
368 if cursorWidth is not None:
369 self.__cursorNode.strokewidth = cursorWidth
370 else:
371 w = float(fontsize) * CURSOR_WIDTH_PCT / 100.0
372 if w < 1:
373 w = 1
374 self.__cursorNode.strokewidth = w
375 x = self.__cursorNode.strokewidth / 2.0
376 self.__cursorNode.pos1 = Point2D(x, self.__cursorNode.pos1.y)
377 self.__cursorNode.pos2 = Point2D(x, self.__cursorNode.pos2.y)
378
379 self.__flashingCursor = flashingCursor
380 if not flashingCursor:
381 self.__cursorContainer.opacity = 1
382
383 self.__updateCursor()
384
385
386 - def setMaxLength(self, maxlen):
387 """
388 Set character limit of the input
389
390 @param maxlen: max number of character allowed
391 """
392 self.__maxLength = maxlen
393
394 - def clearFocus(self):
395 """
396 Compact form to blur the TextArea
397 """
398 self.__parent.opacity = self.__blurOpacity
399 self.__hasFocus = False
400
401 - def setFocus(self, hasFocus):
402 """
403 Force the focus (or blur) of this TextArea
404
405 @param hasFocus: boolean
406 """
407 if self.__focusContext is not None:
408 self.__focusContext.resetFocuses()
409
410 if hasFocus:
411 self.__parent.opacity = 1
412 self.__cursorContainer.opacity = 1
413 else:
414 self.clearFocus()
415 self.__cursorContainer.opacity = 0
416
417 self.__hasFocus = hasFocus
418
419 - def hasFocus(self):
420 """
421 Query the focus status for this TextArea
422 """
423 return self.__hasFocus
424
425 - def onKeyDown(self, keycode):
426 """
427 Inject a keycode into TextArea flow
428
429 Used mainly by FocusContext. It can be used directly, but the best option
430 is always to use a FocusContext helper, which exposes convenience method for
431 injection.
432 @param keycode: characted to insert
433 @type keycode: int (SDL reference)
434 """
435
436 if self.__flashingCursor:
437 self.__cursorContainer.opacity = 1
438
439 if keycode in KEYCODES_BACKSPACE:
440 self.__removeChar(left=True)
441 self.__updateLastActivity()
442 self.__updateCursor()
443 elif keycode == KEYCODES_DEL:
444 self.__removeChar(left=False)
445 self.__updateLastActivity()
446 self.__updateCursor()
447
448 elif keycode == KEYCODE_FORMFEED:
449 self.clearText()
450 elif keycode in (KEYCODE_CRS_UP, KEYCODE_CRS_DOWN, KEYCODE_CRS_LEFT, KEYCODE_CRS_RIGHT):
451 if keycode == KEYCODE_CRS_LEFT and self.__cursorPosition > 0:
452 self.__cursorPosition -= 1
453 self.__update()
454 elif keycode == KEYCODE_CRS_RIGHT and self.__cursorPosition < len(self.__data):
455 self.__cursorPosition += 1
456 self.__update()
457 elif keycode == KEYCODE_CRS_UP and self.__cursorPosition != 0:
458 self.__cursorPosition = 0
459 self.__update()
460 elif keycode == KEYCODE_CRS_DOWN and self.__cursorPosition != len(self.__data):
461 self.__cursorPosition = len(self.__data)
462 self.__update()
463
464 elif keycode == KEYCODE_LINEFEED and self.__isMultiline:
465 self.__appendUChar('\n')
466
467 elif keycode not in (KEYCODE_LINEFEED, 0, 25, 63272):
468 self.__appendKeycode(keycode)
469 self.__updateLastActivity()
470 self.__updateCursor()
471
472 - def __onClick(self, e):
473 if self.__focusContext is not None:
474 if self.__focusContext.isActive():
475 self.setFocus(True)
476 else:
477 self.setFocus(True)
478
480 return u''.join(self.__data)
481
482 - def __appendKeycode(self, keycode):
483 self.__appendUChar(unichr(keycode))
484
485 - def __appendUChar(self, uchar):
486
487 if self.__maxLength > -1 and len(self.__data) > self.__maxLength:
488 return
489
490
491 if len(self.__data) > 0:
492 maxCharDim = self.__textNode.fontsize
493 lastCharPos = self.__textNode.getGlyphPos(len(self.__data) - 1)
494
495
496 if (not self.__isMultiline and
497 lastCharPos[0] + maxCharDim * 1.5 > self.__parent.width - self.__border * 2):
498 return
499
500
501 if (self.__isMultiline and
502 lastCharPos[0] + maxCharDim * 1.5 > self.__parent.width - self.__border * 2 and
503 lastCharPos[1] + maxCharDim * 2 > self.__parent.height - self.__border * 2):
504 return
505
506
507 self.__data.insert(self.__cursorPosition, uchar)
508 self.__cursorPosition += 1
509 self.__update()
510
511 - def __removeChar(self, left=True):
512 if left and self.__cursorPosition > 0:
513 self.__cursorPosition -= 1
514 del self.__data[self.__cursorPosition]
515 self.__update()
516 elif not left and self.__cursorPosition < len(self.__data):
517 del self.__data[self.__cursorPosition]
518 self.__update()
519
520 - def __update(self):
521 self.__textNode.text = self.__getUnicodeFromData()
522 self.__updateCursor()
523
524 - def __updateCursor(self):
525 if self.__cursorPosition == 0:
526 lastCharPos = (0,0)
527 lastCharExtents = (0,0)
528 else:
529 lastCharPos = self.__textNode.getGlyphPos(self.__cursorPosition - 1)
530 lastCharExtents = self.__textNode.getGlyphSize(self.__cursorPosition - 1)
531
532 if self.__data[self.__cursorPosition - 1] == '\n':
533 lastCharPos = (0, lastCharPos[1] + lastCharExtents[1])
534 lastCharExtents = (0, lastCharExtents[1])
535
536 if lastCharExtents[1] > 0:
537 self.__cursorNode.pos2 = Point2D(0, lastCharExtents[1] * (1 - CURSOR_PADDING_PCT/100.0))
538 else:
539 self.__cursorNode.pos2 = Point2D(0, self.__textNode.fontsize)
540
541 self.__cursorContainer.x = lastCharPos[0] + lastCharExtents[0] + self.__border
542 self.__cursorContainer.y = (lastCharPos[1] +
543 self.__cursorNode.pos2.y * CURSOR_PADDING_PCT/200.0 + self.__border)
544
546 self.__lastActivity = time.time()
547
549 if (self.__flashingCursor and
550 self.__hasFocus and
551 time.time() - self.__lastActivity > CURSOR_FLASH_AFTER_INACTIVITY/1000.0):
552 if self.__cursorContainer.opacity == 0:
553 self.__cursorContainer.opacity = 1
554 else:
555 self.__cursorContainer.opacity = 0
556 elif self.__hasFocus:
557 self.__cursorContainer.opacity = 1
558
559
560
561
562
563 -def init(g_avg, catchKeyboard=True, repeatDelay=0.2, charDelay=0.1):
585
586 -def setActiveFocusContext(focusContext):
587 """
588 Tell the module what FocusContext is presently active
589
590 Only one FocusContext at once can be set 'active' and therefore
591 prepared to receive the flow of user events from keyboard.
592 @param focusContext: set the active focusContext. If initialization has been
593 made with 'catchKeyboard' == True, the new active focusContext will receive
594 the flow of events from keyboard.
595 """
596 global g_FocusContext
597
598 if g_FocusContext is not None:
599 g_FocusContext._switchActive(False)
600
601 g_FocusContext = focusContext
602 g_FocusContext._switchActive(True)
603
605 """
606 Set a callback that is called at every keyboard's keypress
607
608 If a callback of user interaction is needed (eg: resetting idle timeout)
609 just pass a function to this method, which is going to be called at each
610 user intervention (keydown, keyup).
611 Active focusContext will be passed as argument
612 """
613 global g_activityCallback
614 g_activityCallback = pyfunc
615
616
624
639
644