Module BasicMagScript
[hide private]
[frames] | no frames]

Source Code for Module BasicMagScript

  1  ''' 
  2  Provides basic magnification services. 
  3   
  4  @author: Eitan Isaacson 
  5  @author: Peter Parente 
  6  @organization: IBM Corporation 
  7  @copyright: Copyright (c) 2006 IBM Corp 
  8   
  9  @license: I{The BSD License} 
 10  All rights reserved. This program and the accompanying materials are made  
 11  available under the terms of the BSD license which accompanies 
 12  this distribution, and is available at 
 13  U{http://www.opensource.org/licenses/bsd-license.php} 
 14  ''' 
 15  # import useful modules for Scripts 
 16  from AccessEngine import AEScript, AccessEngineAPI 
 17  from AccessEngine import AEConstants 
 18  from Tools.i18n import bind, _ 
 19  from AccessEngine.AEConstants import CMD_GOTO, CMD_GET_ROI 
 20  from math import cos, sin, atan, pi, sqrt 
 21   
 22  # metadata describing this Script 
 23  __uie__ = dict(kind='script', tier=None, all_tiers=True) 
 24   
25 -class ZoomMove(object):
26 ''' 27 Class that deals with seamless panning and repositioning of the magnifier. 28 29 @ivar smooth_pan: Allow smooth panning 30 @type smooth_pan: boolean 31 @ivar accel: Acceleration in px/sec**2 32 @type accel: integer 33 @ivar velocity: Velocity in px/sec 34 @type velocity: integer 35 @cvar pan_rate: Duration in milliseconds between "frames" 36 @type pan_rate: integer 37 @cvar _end_coord: Coordinate that we are trying to get to 38 @type _end_coord: tuple 39 @cvar _curr_velocity: Current velocity 40 @type _curr_velocity: integer 41 @cvar _curr_coord: Current coordinates 42 @type _curr_coord: tuple 43 @cvar _vect_length: vector length 44 @type _vect_length: number 45 ''' 46 pan_rate = 40 47 smooth_pan = None 48 accel = None 49 velocity = None 50 _curr_velocity = None 51 _curr_coord = None 52 _vect_length = None 53 _end_coord = None 54
55 - def goTo(self, layer, end_coord, start_coord=None, smooth=False):
56 ''' 57 Main public method for putting the magnifier on a coordinate. 58 59 @param layer: Layer 60 @type layer: integer 61 @param end_coord: Coordinate we want to reach. 62 @type end_coord: tuple 63 @param start_coord: Coordinate we are starting at, if none is given, 64 there will be no panning. 65 @type start_coord: tuple 66 @param smooth: Use smooth panning? 67 @type smooth: boolean 68 ''' 69 if start_coord and (self.smooth_pan or smooth): 70 self._panTo(end_coord, start_coord) 71 else: 72 self._hopTo(end_coord, layer)
73
74 - def _hopTo(self, end_coord, layer):
75 ''' 76 Stop panning and hop to given coordinate. 77 78 @param end_coord: Coordinate we want to reach. 79 @type end_coord: tuple 80 @param layer: Layer 81 @type layer: integer 82 ''' 83 self.stopPan() 84 self.goToPos(end_coord, layer)
85
86 - def _panTo(self, end_coord, start_coord):
87 ''' 88 Pan to given coordinate. 89 90 @param end_coord: Coordinate we want to reach. 91 @type end_coord: tuple 92 @param start_coord: Coordinate we are starting at. 93 @type start_coord: tuple 94 ''' 95 if end_coord == self._end_coord: 96 return 97 98 self.stopPan() 99 100 self._curr_coord = float(start_coord[0]), float(start_coord[1]) 101 102 self._end_coord = end_coord 103 104 x = self._end_coord[0] - self._curr_coord[0] 105 y = self._end_coord[1] - self._curr_coord[1] 106 107 self._vect_length = sqrt(x**2 + y**2) 108 109 if (x, y) == (0, 0): 110 return 111 elif y == 0 and x > 0: 112 angle = pi/2 113 elif y == 0 and x < 0: 114 angle = -pi/2 115 elif y < 0: 116 angle = atan (float(x)/float(y)) + pi 117 else: 118 angle = atan (float(x)/float(y)) 119 120 self._curr_velocity = 0 121 122 self._x_step = sin(angle) 123 self._y_step = cos(angle) 124 125 self.startPan()
126
127 - def _panStep(self, layer):
128 ''' 129 Increment X and Y coordinates in a certain vector. 130 Accelerate, deccelerate accordingly. 131 132 @param layer: Layer 133 @type layer: integer 134 ''' 135 # This is neccesary to avoid jerks when tracking is enabled both with 136 # mouse and anything else. 137 if not self.smooth_pan: 138 self.goToPos(self._end_coord, layer) 139 return False 140 141 # If accel is set to zero it is disabled. 142 if self.accel == 0: 143 self._curr_velocity = self.velocity 144 # currentvelocity**2/(2*acceleration) = The distance needed to get 145 # to a complete stop at the current velocity and deceleration. 146 elif (self._curr_velocity**2/(2*self.accel)) >= self._vect_length: 147 self._curr_velocity -= self.accel 148 # If the current velocity is smaller than the final velocity 149 # then accelerate. 150 elif self._curr_velocity < self.velocity: 151 self._curr_velocity += self.accel 152 153 x = self._curr_velocity*self._x_step 154 y = self._curr_velocity*self._y_step 155 156 self._curr_coord = (x + self._curr_coord[0], 157 y + self._curr_coord[1]) 158 159 progress = sqrt(x**2 + y**2) 160 self._vect_length -= progress 161 162 if self._vect_length < 0: 163 self.goToPos (self._end_coord, layer) 164 return False 165 else: 166 self.goToPos (tuple(map(int,map(round,self._curr_coord))), layer) 167 return True
168
169 - def stopPan(self):
170 ''' 171 Stop current pan motion. 172 ''' 173 raise NotImplementedError
174
175 - def startPan(self):
176 ''' 177 Start a timeout and execute _panstep 178 ''' 179 raise NotImplementedError
180 181
182 - def goToPos(self, pos, layer):
183 ''' 184 Actual method that translates the zoomer, to be implemented by script. 185 186 @param pos: Position 187 @type pos: tuple 188 @param layer: Layer 189 @type layer: integer 190 ''' 191 raise NotImplementedError
192
193 -class BasicMagScriptState(AEScript.ScriptState):
194 ''' 195 Defines a single set of state variables for all instances of the 196 L{BasicMagScript} and their tasks. The variables are configuration settings 197 that should be respected in all instances of L{BasicMagScript}. 198 199 - B{MouseTrack (bool):} Track cursor 200 - B{FocusTrack (bool):} Track focus 201 - B{CaretTrack (bool):} Track caret 202 - B{SmoothPan (bool):} Smooth pan 203 - B{CaretPan (bool):} Smooth caret panning 204 - B{PanAccel (int):} Pan acceleration 205 - B{PanVelocity (int):} Pan velocity 206 '''
207 - def init(self):
208 ''' 209 Create L{AEState} settings for this L{BasicMagScript}. 210 ''' 211 self.newBool('MouseTrack', True, _('Track cursor'), 212 _('Magnifier is to track mouse movemenself.')) 213 self.newBool('FocusTrack', True, _('Track focus'), 214 _('Magnifier is to track focused area')) 215 self.newBool('CaretTrack', True, _('Track caret'), 216 _('Magnifier is to track text caret')) 217 self.newBool('SmoothPan', False, _('Smooth panning'), 218 _('Magnifier will pan to next selection')) 219 self.newBool('CaretPan', False, _('Smooth caret panning'), 220 _('Magnifier will smoothly pan on caret movements, by ' 221 'default it only pans on new lines.')) 222 self.newRange('PanAccel', 40, _('Pan Acceleration'), 1, 100, 0, 223 _('Acceleration rate of magnifier pan motion (pixels per second ' 224 'squared).')) 225 self.newRange('PanVelocity', 600, _('Pan Velocity'), 1, 1000, 0, 226 _('Velocity of magnifier pan motion (pixels per second).')) 227 self.newBool('MagReview', False, _('Track review keys?'), 228 _('When set, reviewing is tracked by the magnifier. ' 229 'Otherwise, magnifier is not updated when reviewing.'))
230
231 - def getGroups(self):
232 ''' 233 Gets configurable settings for this L{AEScript <AEScript.AEScript>}. 234 235 @return: Group of all configurable settings 236 @rtype: L{AEState.Setting.Group} 237 ''' 238 g = self.newGroup() 239 t = g.newGroup(_('Tracking')) 240 t.extend(['MouseTrack', 241 'FocusTrack', 242 'CaretTrack']) 243 p = g.newGroup(_('Kinematics')) 244 p.extend(['SmoothPan', 245 'CaretPan', 246 'PanAccel', 247 'PanVelocity']) 248 r = g.newGroup(_('Reviewing')) 249 r.append('MagReview') 250 return g
251
252 -class BasicMagScript(AEScript.EventScript, ZoomMove):
253 ''' 254 A script to provide magnification for a user. 255 It defines special hotkeys. 256 - I{(Alt+Caps-Lock+PAGE_UP)} - increase the zoom rate 257 - I{(Alt+Caps-Lock+PAGE_DOWN)} - decrease the zoom rate 258 259 @cvar STATE: L{BasicMagScriptState} object that holds user setting values. 260 @type STATE: L{AEState} 261 @cvar FUDGE: Fudge the Calculation of mouse pointer position 262 @type FUDGE: integer 263 @ivar mousepos: Coordinates of the mouse pointer position 264 @type mousepos: tuple 265 @ivar parent_por_focus: The parent accessible of a focused element 266 @type parent_por_focus: L{AEPor} 267 @ivar parent_por_select: The parent accessible of a selected element 268 @type parent_por_select: L{AEPor} 269 ''' 270 STATE=BasicMagScriptState 271 FUDGE=5 272
273 - def init(self):
274 ''' 275 Registers L{event tasks <AEScript.event_tasks>} to handle 276 L{focus <AEEvent.FocusChange>}, L{caret <AEEvent.CaretChange>}, 277 L{selector <AEEvent.SelectorChange>}, and L{mouse <AEEvent.MouseChange>} 278 change events. Registers L{tasks <AEScript.registered_tasks>} that can be 279 mapped to L{AEInput.Gesture}s. 280 ''' 281 self.mousepos = (0, 0) 282 self.parent_por_focus = None 283 self.parent_por_select = None 284 285 AccessEngineAPI.setScriptIdealOutput(self, 'magnifier') 286 287 # register event handlers 288 self.registerEventTask('mag focus', AEConstants.EVENT_TYPE_FOCUS_CHANGE) 289 self.registerEventTask('mag caret', AEConstants.EVENT_TYPE_CARET_CHANGE) 290 self.registerEventTask('mag mouse', AEConstants.EVENT_TYPE_MOUSE_CHANGE) 291 self.registerEventTask('mag selector', 292 AEConstants.EVENT_TYPE_SELECTOR_CHANGE) 293 294 # register input tasks 295 self.registerTask('mag review', self.onReviewChange) 296 self.registerTask('mag increase zoom', self.changeZoomLevel) 297 self.registerTask('mag decrease zoom', self.changeZoomLevel) 298 299 # get the Keyboard device and register modifiers and commands 300 kbd = AccessEngineAPI.getInputDevice(None, 'keyboard') 301 AccessEngineAPI.addInputModifiers(self, kbd, kbd.AEK_CAPS_LOCK) 302 303 # register commands for either left Alt-CapsLock or right Alt-CapsLock 304 pairs = [[kbd.AEK_ALT_L, kbd.AEK_CAPS_LOCK], 305 [kbd.AEK_ALT_R, kbd.AEK_CAPS_LOCK]] 306 307 for pair in pairs: 308 # magnifier script 309 self.registerCommand(kbd, 'mag increase zoom', _('mag increase zoom'), 310 False, pair+[kbd.AEK_PAGE_UP]) 311 self.registerCommand(kbd, 'mag decrease zoom', _('mag decrease zoom'), 312 False, pair+[kbd.AEK_PAGE_DOWN]) 313 314 315 self.accel = self.state.PanAccel*(self.pan_rate/1000.0) 316 self.velocity = self.state.PanVelocity*(self.pan_rate/1000.0) 317 self.smooth_pan = self.state.SmoothPan 318 319 for setting in self.state: 320 setting.addObserver(self._updateSetting) 321 322 #self.focused_por = self.getVirtualPOR() # MARKED: focused_por 323 self.last_caret_offset = 0 324 325 # link to review mode tasks 326 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 327 'review previous item', 'ReviewScript') 328 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 329 'review current item', 'ReviewScript') 330 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 331 'review next item', 'ReviewScript') 332 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 333 'review previous word', 'ReviewScript') 334 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 335 'review current word', 'ReviewScript') 336 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 337 'review next word', 'ReviewScript') 338 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 339 'review previous char', 'ReviewScript') 340 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 341 'review current char', 'ReviewScript') 342 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 343 'review next char', 'ReviewScript') 344 self.chainTask('mag review', AEConstants.CHAIN_AFTER, 345 'pointer to por', 'ReviewScript')
346
347 - def _updateSetting(self, state, setting):
348 ''' 349 Update special given settings. 350 351 @param state: 352 @type state: 353 @param setting: Name of attribute 354 @type setting: string 355 ''' 356 name = setting.name 357 value = setting.value 358 if name == 'PanAccel': 359 self.accel = value*(self.pan_rate/1000.0) 360 elif name == 'PanVelocity': 361 self.velocity = value*(self.pan_rate/1000.0) 362 elif name == 'SmoothPan': 363 self.smooth_pan = value
364
365 - def getName(self):
366 ''' 367 Provides the localized name of this L{AEScript <AEScript.AEScript>}. 368 369 @return: Human readable name of this script. 370 @rtype: string 371 ''' 372 return _('Basic magnifier')
373
374 - def getDescription(self):
375 ''' 376 Describe what this special L{AEScript} do. 377 378 @return: Human readable translated description of this script. 379 @rtype: string 380 ''' 381 return _('Manages basic screen magnification services. Requires a ' 382 'loaded magnifier device.')
383
384 - def goToPos(self, pos, layer):
385 ''' 386 Send command to magnifier device to center zoomer on given coordinate. 387 388 @param pos: x,y coordinate 389 @type pos: tuple of integer 390 @param layer: Layer 391 @type layer: integer 392 ''' 393 roi = AccessEngineAPI.getMagROI(self, layer, cap='magnifier', role='output') 394 if roi is None: 395 return 396 x1, y1, x2, y2 = roi 397 w, h = x2 - x1, y2 - y1 398 x1 = pos[0] - w/2 399 y1 = pos[1] - h/2 400 x2, y2 = x1 + w, y1 + h 401 AccessEngineAPI.setMagGoto(self, layer, (x1, y1, x2, y2), 402 cap='magnifier', role='output')
403
404 - def getPos(self, layer):
405 ''' 406 Get ROI from magnifier and return center coordinate. 407 408 @param layer: Layer 409 @type layer: integer 410 @return: Magnifier's focal point. 411 @rtype: tuple 412 ''' 413 roi = AccessEngineAPI.getMagROI(self, layer, cap='magnifier', role='output') 414 if roi is None: 415 return (0, 0, 0, 0) 416 else: 417 return roi
418
419 - def getDesiredFocalPoint(self, por, track_mouse=True, **kwargs):
420 ''' 421 Calculates the desired focal point for a specific POR. 422 And retuns current focal point, and desired focal point. 423 424 @param por: POR (Point of Regard) that's focal point needs to be determined. 425 @type por: L{AEPor} 426 @param track_mouse: Take the current position of the mouse pointer into 427 account so that the magnifier doesn't jerk? 428 @type track_mouse: boolean 429 @param kwargs: Arbitrary keyword arguments to pass to the task 430 @type kwargs: dictionary 431 ''' 432 # get the extents of the POR 433 try: 434 por_width, por_height = AccessEngineAPI.getAccVisualExtents(por) 435 except TypeError: 436 # In case the extents of the POR can't be determined, the mouse pointer 437 # position is not taken into account when calculating the desired focal 438 # point, since we can't evaluate if it is within the POR. 439 track_mouse = False 440 # get the position of the POR 441 pos_x, pos_y = AccessEngineAPI.getAccPosition(por) 442 # get the last known mouse position 443 m_x, m_y = self.mousepos 444 # get the recommended focal point for the current POR 445 end_x, end_y = AccessEngineAPI.getAccVisualPoint(por) 446 447 # calculate start_pos 448 # get the left, top, right, and bottom coords of the magnifier zoom reg. 449 roi_x1, roi_y1, roi_x2, roi_y2 = self.getPos(kwargs['layer']) 450 # start position is the center point 451 start_pos = ((roi_x1 + roi_x2)/2, (roi_y1 + roi_y2)/2) 452 453 if (track_mouse and 454 (m_x >= pos_x-self.FUDGE) and (m_x <= (pos_x+por_width+self.FUDGE)) and 455 (m_y >= pos_y-self.FUDGE) and (m_y <= (pos_y+por_height+self.FUDGE))): 456 # if the mouse pointer is within the control and we want to consider that 457 # make the end position of the jump the mouse pointer position so that 458 # the magnifier doesn't jump to some other location in the control and 459 # then jump back again; the user is likely a mouse user anyways 460 end_x = m_x 461 end_y = m_y 462 463 return start_pos, (end_x, end_y)
464 465 ############### 466 ## Focus Task 467 ###############
468 - def onFocusGained(self, **kwargs):
469 ''' 470 Moves the magnifier to coordinate of this focused element. 471 472 @param kwargs: Arbitrary keyword arguments to pass to the task 473 @type kwargs: dictionary 474 ''' 475 if not self.state.FocusTrack: 476 return True 477 478 if kwargs['por'] != self.parent_por_focus: 479 start_pos, end_pos = self.getDesiredFocalPoint(**kwargs) 480 self.goTo(kwargs['layer'], end_pos, start_pos) 481 482 #self.focused_por = self.getVirtualPOR() # MARKED: focused_por 483 self.parent_por_focus = AccessEngineAPI.getParentAcc(kwargs['por'])
484 485 ############### 486 ## Caret Task 487 ###############
488 - def onCaretChange(self, **kwargs):
489 ''' 490 Moves the magnifier to coordinate of this caret. 491 492 @param kwargs: Arbitrary keyword arguments to pass to the task. 493 @type kwargs: dictionary 494 ''' 495 if not self.state.CaretTrack: 496 return True 497 498 por = kwargs['por'] 499 500 # sanity check 501 text_len = AccessEngineAPI.getAccTextLength(por) 502 if (por.char_offset < 0 or 503 por.item_offset < 0 or 504 text_len < (por.char_offset + por.item_offset)): 505 return 506 507 start_pos, end_pos = self.getDesiredFocalPoint(**kwargs) 508 509 # self.focused_por == por or (in der if-Abfrage) 510 if (por.item_offset != self.last_caret_offset or 511 self.state.CaretPan): 512 self.goTo(kwargs['layer'], end_pos, start_pos) 513 else: 514 self.goTo(kwargs['layer'], end_pos) 515 516 self.last_caret_offset = por.item_offset
517 518 ################## 519 ## Selector Task 520 ##################
521 - def onSelectorChange(self, **kwargs):
522 ''' 523 Moves the magnifier to coordinate of this selected element. 524 525 @param kwargs: Arbitrary keyword arguments to pass to the task. 526 @type kwargs: dictionary 527 ''' 528 if not self.state.FocusTrack: 529 return True 530 531 if kwargs['por'] != self.parent_por_select: 532 start_pos, end_pos = self.getDesiredFocalPoint(**kwargs) 533 self.goTo(kwargs['layer'], end_pos, start_pos) 534 535 self.parent_por_select = AccessEngineAPI.getParentAcc(kwargs['por'])
536 537 ############### 538 ## Mouse Task 539 ###############
540 - def onMouseMoved(self, **kwargs):
541 ''' 542 Execute on each mouse event, moves the magnifier to coordinate of mouse. 543 544 @param kwargs: Arbitrary keyword arguments to pass to the task. 545 @type kwargs: dictionary 546 @return: C{True} to allow other tasks to process this event. 547 @rtype: boolean 548 ''' 549 if not self.state.MouseTrack: 550 return True 551 self.mousepos = kwargs['pos'] 552 self.goTo(kwargs['layer'], kwargs['pos'])
553 554 ################ 555 ## Input Tasks 556 ################
557 - def onReviewChange(self, **kwargs):
558 ''' 559 Synchronizes the magnified area to the text at the pointer when reviewing. 560 561 @param kwargs: Arbitrary keyword arguments to pass to the task. 562 @type kwargs: dictionary 563 ''' 564 if self.getScriptSettingVal('MagReview'): 565 result = self.getTempData('review') 566 if result in (AEConstants.REVIEW_OK, AEConstants.REVIEW_WRAP): 567 start_pos, end_pos = self.getDesiredFocalPoint(track_mouse=False, 568 **kwargs) 569 self.goTo(kwargs['layer'], end_pos, start_pos)
570
571 - def changeZoomLevel(self, **kwargs):
572 ''' 573 Increases or decreases zoom level. 574 575 @param kwargs: Arbitrary keyword arguments to pass to the task. 576 @type kwargs: dictionary 577 ''' 578 print AccessEngineAPI.getMagROI(self, kwargs['layer'], 579 cap='magnifier', role='output') 580 s = AccessEngineAPI.getStyleSetting(self, 'Zoom', **kwargs) 581 id = kwargs['task_name'] 582 if id.find('increase') > -1: 583 s.value += 10**(-s.precision) 584 else: 585 s.value -= 10**(-s.precision) 586 print AccessEngineAPI.getMagROI(self, kwargs['layer'], 587 cap='magnifier', role='output')
588 589 ############# 590 ## PanTimer 591 #############
592 - def startPan(self):
593 ''' 594 Register a TimerTask for the pan animation. 595 ''' 596 self.registerTimerTask('mag pan step', self.pan_rate, self.onPanTimer)
597
598 - def stopPan(self):
599 ''' 600 Unregister pan animation timer task. 601 ''' 602 try: 603 self.unregisterTimerTask('mag pan step') 604 except KeyError: 605 pass
606
607 - def onPanTimer(self, **kwargs):
608 ''' 609 Execute on timed intervals. Used to animate pan magnifier movement. 610 611 @param kwargs: Arbitrary keyword arguments to pass to the task. 612 @type kwargs: dictionary 613 ''' 614 rv = self._panStep(kwargs['layer']) 615 if not rv: 616 self.unregisterTimerTask(kwargs['task_name'])
617