Package libavg :: Module textarea

Source Code for Module libavg.textarea

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  avg = None 
  5  g_Player = None 
  6  g_FocusContext = None 
  7  g_LastKeyEvent = None 
  8  g_activityCallback = None 
  9  g_LastKeyRepeated = 0 
 10  g_RepeatDelay = 0.2 
 11  g_CharDelay = 0.1 
 12   
 13  import time 
 14   
 15  KEYCODE_TAB = 9 
 16  KEYCODE_SHTAB = 25 
 17  KEYCODE_FORMFEED = 12 
 18  KEYCODES_BACKSPACE = (8,127) 
 19   
 20  DEFAULT_CURSOR_PX = 'tacursorpx.png' 
 21  CURSOR_PADDING_PCT = 15 
 22  CURSOR_WIDTH_PCT = 4 
 23  CURSOR_SPACING_PCT = 4 
 24  CURSOR_FLASHING_DELAY = 1000 
 25  CURSOR_FLASH_AFTER_INACTIVITY = 200 
 26   
 27  import os.path 
 28  import time 
 29  try: 
 30      from . import avg 
 31  except ValueError: 
 32      pass 
 33   
34 -class FocusContext:
35 """ 36 This object serves as a grouping element for TextAreas. 37 TextArea elements that belong to the same FocusContext cycle 38 focus among themselves. There can be several FocusContextes but 39 only one at once can be activated ( using the global function 40 setActiveFocusContext() ) 41 """
42 - def __init__(self):
43 self.__elements = [] 44 self.__isActive = False
45
46 - def isActive(self):
47 """ 48 Test if this FocusContext is active 49 """ 50 return self.__isActive
51
52 - def register(self, taElement):
53 """ 54 Register a floating textarea with this FocusContext 55 @param taElement: TextArea, a reference to a TextArea 56 """ 57 self.__elements.append(taElement)
58
59 - def getFocused(self):
60 """ 61 Returns a TextArea element that currently has focus within 62 this FocusContext 63 """ 64 for ob in self.__elements: 65 if ob.hasFocus(): 66 return ob 67 return None
68
69 - def keyCharPressed(self, kchar):
70 """ 71 Use this method to inject a character to active 72 (w/ focus) TextArea, convenience method for keyUCodePressed() 73 @param kchar: string, a single character 74 """ 75 uch = unicode(kchar, 'utf-8') 76 self.keyUCodePressed(ord(uch[0]))
77
78 - def keyUCodePressed(self, keycode):
79 """ 80 Shift a character (Unicode) into the active (w/focus) 81 TextArea 82 @param keycode: int, unicode code point of the character 83 """ 84 # TAB key cycles focus through textareas 85 if keycode == KEYCODE_TAB: 86 self.cycleFocus() 87 return 88 # Shift-TAB key cycles focus through textareas backwards 89 if keycode == KEYCODE_SHTAB: 90 self.cycleFocus(True) 91 return 92 93 for ob in self.__elements: 94 if ob.hasFocus(): 95 ob.onKeyDown(keycode)
96
97 - def backspace(self):
98 """ 99 Emulates a backspace character 100 """ 101 self.keyUCodePressed(KEYCODES_BACKSPACE[0])
102
103 - def clear(self):
104 """ 105 Clears the active textarea, emulating the press of FF character 106 """ 107 self.keyUCodePressed(KEYCODE_FORMFEED)
108
109 - def resetFocuses(self):
110 """ 111 Blurs every TextArea registered within this FocusContext 112 """ 113 for ob in self.__elements: 114 ob.clearFocus()
115
116 - def cycleFocus(self, backwards=False):
117 """ 118 Force a focus cycle among instantiated textareas 119 """ 120 121 els = [] 122 els.extend(self.__elements) 123 124 if len(els) == 0: 125 return 126 127 if backwards: 128 els.reverse() 129 130 elected = 0 131 for ob in els: 132 if not ob.hasFocus(): 133 elected = elected + 1 134 else: 135 break 136 137 # elects the first if no ta are in focus or if the 138 # last one has it 139 if elected in (len(els), len(els)-1): 140 elected = 0 141 else: 142 elected = elected + 1 143 144 for ob in els: 145 ob.setFocus(False) 146 147 els[elected].setFocus(True)
148
149 - def getRegistered(self):
150 """ 151 Returns a list of TextArea currently registered within this FocusContext 152 """ 153 return self.__elements
154
155 - def _switchActive(self, active):
156 """ 157 De/Activates this FocusContext. Active FocusContexts route to the focused 158 textarea the keypress stream 159 Use textarea.setActiveFocusContext() instead using it directly 160 @param active: boolean, set to True to activate, False to deactivate 161 """ 162 if active: 163 self.resetFocuses() 164 self.cycleFocus() 165 else: 166 self.resetFocuses() 167 168 self.__isActive = active
169 170
171 -class TextArea:
172 """ 173 TextArea is an extended <words> node that reacts to user input 174 (mouse/touch for focus, keyboard for text input). 175 It sits in a given container matching its dimensions 176 """
177 - def __init__(self, parent, focusContext=None, bgImageFile=None, disableMouseFocus=False, 178 blurOpacity=0.3, border=0, id='', cursorPixFile=None):
179 """ 180 @param parent: a div node with defined dimensions 181 @param focusContext: FocusContext object which groups focus for TextArea elements 182 @param bgImageFile: path and file name (relative to mediadir) of an image 183 that is used as a background for TextArea. The image is stretched to extents 184 of the instance 185 @param disableMouseFocus: boolean, prevents that mouse can set focus for 186 this instance 187 @param blurOpacity: opacity that textarea gets when goes to blur state 188 @param border: amount of offsetting pixels that words node will have from image extents 189 @param id: optional handle to identify the object when dealing with events. ID uniqueness 190 is not guaranteed 191 @param cursorPixFile: one-pixel graphic file used to render the cursor. The default one is 192 a black one 193 """ 194 global g_Player 195 g_Player = avg.Player.get() 196 self.__parent = parent 197 self.__focusContext = focusContext 198 self.__blurOpacity = blurOpacity 199 self.__border = border 200 self.__id = id 201 202 if bgImageFile is not None: 203 bgNode = g_Player.createNode("image", {}) 204 bgNode.href = bgImageFile 205 bgNode.width = parent.width 206 bgNode.height = parent.height 207 parent.appendChild(bgNode) 208 209 textNode = g_Player.createNode("words", {'rawtextmode':True}) 210 211 if not disableMouseFocus: 212 parent.setEventHandler(avg.CURSORUP, avg.MOUSE, self.__onClick) 213 parent.setEventHandler(avg.CURSORUP, avg.TOUCH, self.__onClick) 214 215 parent.appendChild(textNode) 216 217 if focusContext is not None: 218 focusContext.register(self) 219 220 if cursorPixFile is None: 221 crspx = os.path.dirname(__file__)+'/'+DEFAULT_CURSOR_PX 222 else: 223 crspx = cursorPixFile 224 225 cursorNode = g_Player.createNode('image', {'href':crspx}) 226 parent.appendChild(cursorNode) 227 self.__flashingCursor = False 228 229 self.__cursorNode = cursorNode 230 self.__textNode = textNode 231 self.__charSize = -1 232 self.setStyle() 233 self.setFocus(False) 234 235 g_Player.setInterval(CURSOR_FLASHING_DELAY, self.__tickFlashCursor) 236 237 self.__lastActivity = 0
238
239 - def getID(self):
240 """ 241 Returns the ID of the textarea (set on the constructor). 242 Useful with single-entry callbacks 243 """ 244 return self.__id
245
246 - def clearText(self):
247 """ 248 Clears the text 249 """ 250 self.setText('')
251
252 - def setText(self, uString):
253 """ 254 Set the text on the TextArea 255 @param uString: an unicode string 256 """ 257 self.__textNode.text = uString 258 self.__resetCursorPosition()
259
260 - def getText(self):
261 """ 262 Get the text stored and displayed on the TextArea 263 """ 264 return self.__textNode.text
265
266 - def setStyle(self, font='Arial', size=12, alignment='left', variant='Regular', 267 color='000000', multiline=True, cursorWidth=None, flashingCursor=False):
268 """ 269 Set some style parameters of the <words> node of the TextArea 270 @param font: font face 271 @param size: font size in pixels 272 @param alignment: one among 'left', 'right', 'center' 273 @param variant: font variant (eg: 'bold') 274 @param color: RGB hex text color 275 @param multiline: boolean, whether TextArea has to wrap (undefinitely) or stop at full width 276 @param cursorWidth: int, width of the cursor in pixels 277 """ 278 self.__textNode.font = font 279 self.__textNode.size = int(size) 280 self.__textNode.alignment = alignment 281 self.__textNode.color = color 282 self.__textNode.variant = variant 283 self.__isMultiline = multiline 284 self.__maxLength = -1 285 286 if multiline: 287 self.__textNode.parawidth = int(self.__parent.width) - self.__border*2 288 else: 289 self.__textNode.parawidth = -1 290 291 self.__textNode.x = self.__border 292 self.__textNode.y = self.__border 293 if cursorWidth is not None: 294 self.__cursorNode.width = cursorWidth 295 else: 296 w = float(size) * CURSOR_WIDTH_PCT / 100.0 297 if w < 1: 298 w = 1 299 self.__cursorNode.width = w 300 self.__flashingCursor = flashingCursor 301 if not flashingCursor: 302 self.__cursorNode.opacity = 1 303 304 self.__resetCursorPosition()
305 306
307 - def setMaxLength(self, maxlen):
308 """ 309 Set character limit of the input 310 @param maxlen: max number of character allowed 311 """ 312 self.__maxLength = maxlen
313
314 - def clearFocus(self):
315 """ 316 Compact form to blur the TextArea 317 """ 318 self.__parent.opacity = self.__blurOpacity 319 self.__hasFocus = False
320
321 - def setFocus(self, hasFocus):
322 """ 323 Force the focus (or blur) of this TextArea 324 @param hasFocus: boolean 325 """ 326 if self.__focusContext is not None: 327 self.__focusContext.resetFocuses() 328 329 if hasFocus: 330 self.__parent.opacity = 1 331 self.__cursorNode.opacity = 1 332 else: 333 self.clearFocus() 334 self.__cursorNode.opacity = 0 335 336 self.__hasFocus = hasFocus
337
338 - def hasFocus(self):
339 """ 340 Query the focus status for this TextArea 341 """ 342 return self.__hasFocus
343
344 - def onKeyDown(self, keycode):
345 if keycode in KEYCODES_BACKSPACE: 346 self.__removeChar() 347 self.__updateLastActivity() 348 self.__resetCursorPosition() 349 # NP/FF clears text 350 elif keycode == KEYCODE_FORMFEED: 351 self.clearText() 352 # avoid shift-tab, return, zero, delete 353 elif keycode not in (0,13,25,63272): 354 self.__appendChar(keycode) 355 self.__updateLastActivity() 356 self.__resetCursorPosition()
357
358 - def __onClick(self, e):
359 if self.__focusContext is not None: 360 if self.__focusContext.isActive(): 361 self.setFocus(True) 362 else: 363 self.setFocus(True)
364
365 - def __appendChar(self, keycode):
366 slen = len(self.__textNode.text) 367 368 if slen > 0: 369 lastCharPos = self.__textNode.getGlyphPos(slen-1) 370 # don't wrap when TextArea is not multiline 371 if (not self.__isMultiline and 372 lastCharPos[0] > self.__parent.width - self.__textNode.size - self.__border * 2): 373 return 374 375 # don't flee from borders in a multiline textarea 376 if (self.__isMultiline and 377 lastCharPos[1] > self.__parent.height - self.__textNode.size * 2 - self.__border * 2 and 378 lastCharPos[0] > self.__parent.width - self.__textNode.size - self.__border * 2): 379 return 380 381 # if maximum number of char is specified, honour the limit 382 if self.__maxLength > -1 and slen > self.__maxLength: 383 return 384 385 self.__textNode.text = self.__textNode.text + unichr(keycode)
386
387 - def __removeChar(self):
388 self.__textNode.text = self.__textNode.text[0:-1]
389
390 - def __resetCursorPosition(self):
391 wslen = len(self.__textNode.text) 392 393 if wslen == 0: 394 lastCharPos = (0,0) 395 lastCharExtents = (0,0) 396 else: 397 lastCharPos = self.__textNode.getGlyphPos(wslen-1) 398 lastCharExtents = self.__textNode.getGlyphSize(wslen-1) 399 400 if lastCharExtents[1] > 0: 401 self.__cursorNode.height = lastCharExtents[1] * (1 - CURSOR_PADDING_PCT/100.0) 402 else: 403 self.__cursorNode.height = self.__textNode.size 404 405 self.__cursorNode.x = lastCharPos[0] + lastCharExtents[0] + self.__textNode.size * CURSOR_SPACING_PCT/100.0 406 self.__cursorNode.y = lastCharPos[1] + self.__cursorNode.height * CURSOR_PADDING_PCT/200.0
407
408 - def __updateLastActivity(self):
409 self.__lastActivity = time.time()
410
411 - def __tickFlashCursor(self):
412 if (self.__flashingCursor and 413 self.__hasFocus and 414 time.time() - self.__lastActivity > CURSOR_FLASH_AFTER_INACTIVITY/1000.0): 415 if self.__cursorNode.opacity == 0: 416 self.__cursorNode.opacity = 1 417 else: 418 self.__cursorNode.