Package libavg :: Module textarea

Source Code for Module libavg.textarea

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  # 
  4  # libavg - Media Playback Engine. 
  5  # Copyright (C) 2003-2008 Ulrich von Zadow 
  6  # 
  7  # This library is free software; you can redistribute it and/or 
  8  # modify it under the terms of the GNU Lesser General Public 
  9  # License as published by the Free Software Foundation; either 
 10  # version 2 of the License, or (at your option) any later version. 
 11  # 
 12  # This library is distributed in the hope that it will be useful, 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 15  # Lesser General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU Lesser General Public 
 18  # License along with this library; if not, write to the Free Software 
 19  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 20  # 
 21  # Current versions can be found at www.libavg.de 
 22  # 
 23  # Original author of this module is Marco Fagiolini <mfx at archi-me-des dot de> 
 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      # We're running unit tests 
 78      if platform.system() == 'Windows': 
 79          from libavg import Point2D 
 80      else: 
 81          from avg import Point2D 
 82   
 83   
84 -class FocusContext:
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 """
92 - def __init__(self):
93 self.__elements = [] 94 self.__isActive = False
95
96 - def isActive(self):
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 # TAB key cycles focus through textareas 141 if keycode == KEYCODE_TAB: 142 self.cycleFocus() 143 return 144 # Shift-TAB key cycles focus through textareas backwards 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
159 - def delete(self):
160 """ 161 Emulate a delete character keypress 162 """ 163 self.keyUCodePressed(KEYCODE_DEL)
164
165 - def clear(self):
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 # elects the first if no ta are in focus or if the 205 # last one has it 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
233 -class TextArea:
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
290 - def getID(self):
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
318 - def getText(self):
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 # Ensure that the cursor is shown 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 # NP/FF clears text 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 # add linefeed only on multiline textareas 464 elif keycode == KEYCODE_LINEFEED and self.__isMultiline: 465 self.__appendUChar('\n') 466 # avoid shift-tab, return, zero, delete 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
479 - def __getUnicodeFromData(self):
480 return u''.join(self.__data)
481
482 - def __appendKeycode(self, keycode):
483 self.__appendUChar(unichr(keycode))
484
485 - def __appendUChar(self, uchar):
486 # if maximum number of char is specified, honour the limit 487 if self.__maxLength > -1 and len(self.__data) > self.__maxLength: 488 return 489 490 # Boundary control 491 if len(self.__data) > 0: 492 maxCharDim = self.__textNode.fontsize 493 lastCharPos = self.__textNode.getGlyphPos(len(self.__data) - 1) 494 495 # don't wrap when TextArea is not multiline 496 if (not self.__isMultiline and 497 lastCharPos[0] + maxCharDim * 1.5 > self.__parent.width - self.__border * 2): 498 return 499 500 # don't flee from borders in a multiline textarea 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
545 - def __updateLastActivity(self):
546 self.__lastActivity = time.time()
547
548 - def __tickFlashCursor(self):
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 # MODULE FUNCTIONS 562
563 -def init(g_avg, catchKeyboard=True, repeatDelay=0.2, charDelay=0.1):
564 """ 565 Initialization routine for the module 566 567 This method should be called immediately after avg file 568 load (Player.loadFile()) 569 @param g_avg: avg package 570 @param catchKeyboard: boolean, if true events from keyboard are catched 571 @param repeatDelay: wait time (seconds) before starting to repeat a key which 572 is held down 573 @param charDelay: delay among character repetition (of an steadily pressed key) 574 """ 575 global avg, g_RepeatDelay, g_CharDelay 576 avg = g_avg 577 g_RepeatDelay = repeatDelay 578 g_CharDelay = charDelay 579 580 avg.Player.get().setOnFrameHandler(_onFrame) 581 582 if catchKeyboard: 583 avg.Player.get().getRootNode().setEventHandler(avg.KEYDOWN, avg.NONE, _onKeyDown) 584 avg.Player.get().getRootNode().setEventHandler(avg.KEYUP, avg.NONE, _onKeyUp)
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
604 -def setActivityCallback(pyfunc):
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
617 -def _onFrame():
618 global g_LastKeyEvent, g_LastKeyRepeated, g_CharDelay 619 if (g_LastKeyEvent is not None and 620 time.time() - g_LastKeyRepeated > g_CharDelay and 621 g_FocusContext is not None): 622 g_FocusContext.keyUCodePressed(g_LastKeyEvent.unicode) 623 g_LastKeyRepeated = time.time()
624
625 -def _onKeyDown(e):
626 global g_LastKeyEvent, g_LastKeyRepeated, g_RepeatDelay, g_activityCallback 627 628 if e.unicode == 0: 629 return 630 631 g_LastKeyEvent = e 632 g_LastKeyRepeated = time.time() + g_RepeatDelay 633 634 if g_FocusContext is not None: 635 g_FocusContext.keyUCodePressed(e.unicode) 636 637 if g_activityCallback is not None: 638 g_activityCallback(g_FocusContext)
639
640 -def _onKeyUp(e):
641 global g_LastKeyEvent 642 643 g_LastKeyEvent = None
644