'''
This LSR perk is for the Gnome Magnifier (magnifier).

@author: George Kraft IV <gk4@austin.ibm.com>
@author: Eitan Isaacson <eitan@ascender.com>
@organization: IBM Corp
@copyright: Copyright (c) 2006 IBM Corp
@license: The BSD License

All rights reserved. This program and the accompanying materials are made 
available under the terms of the BSD license which accompanies
this distribution, and is available at
U{http://www.opensource.org/licenses/bsd-license.php}

Current revision $Revision: 1.25 $
Latest change by $Author: eitan $ on $Date: 2006/12/12 10:02:34 $
'''
# import useful modules for Perks
import Perk, Task
from i18n import bind, _
from AEConstants import CMD_GOTO
from math import cos, sin, atan, pi, sqrt
import gobject

# metadata describing this Perk
__uie__ = dict(kind='perk', tier=None, all_tiers=True)

class ZoomMove(object):
  '''
  Class that deals with seamless panning and repositioning of the magnifier.

  @cvar pan_rate: Duation in milliseconds between "frames"
  @type pan_rate: int
  @cvar timeout_id: GObject timeout id.
  @type timeout_id: int
  @cvar end_coord: Coordinate that we are trying to get to
  @type end_coord: tuple
  @cvar last_track_type: The last type of tracking we did
  @type last_track_type: int
  @ivar accel: Acceleration in px/sec**2
  @type accel: int
  @ivar velocity: Velocity in px/sec
  @type velocity: int
  @ivar smooth_pan: Allow smooth panning
  @type smooth_pan: boolean
  '''
  pan_rate = 10
  timeout_id = None
  last_track_type = TRACKED_NONE
  smooth_pan = None
  accel = None
  velocity = None
  _curr_velocity = None
  _curr_coord = None
  _vect_length = None
  _end_coord = None
  
  def goTo(self, end_coord, start_coord=None, smooth=False):
    '''
    Main public method for putting the magnifier on a coordinate.

    @param end_coord: Coordinate we want to reach.
    @type end_coord: tuple
    @param start_coord: Coordinate we are starting at, if none is given, 
    there will be no panning.
    @type start: tuple
    
    '''
    if start_coord and self.smooth_pan:
      self._panTo(end_coord, start_coord)
    else:
      self._hopTo(end_coord)

  def _hopTo(self, end_coord):
    '''
    Stop panning and hop to given coordinate.

    @param end_coord: Coordinate we want to reach.
    @type end_coord: tuple
    '''
    self.stopPan()
    self.goToPos(end_coord)

  def _panTo(self, end_coord, start_coord):
    '''
    Pan to given coordinate.

    @param end_coord: Coordinate we want to reach.
    @type end_coord: tuple
    @param start_coord: Coordinate we are starting at.
    @type start_coord: tuple
    '''
    if end_coord == self._end_coord:
      return
        
    self.stopPan()
        
    self._curr_coord = float(start_coord[0]), float(start_coord[1])
    
    self._end_coord = end_coord
    
    x = self._end_coord[0] - self._curr_coord[0]
    y = self._end_coord[1] - self._curr_coord[1]

    self._vect_length = sqrt(x**2 + y**2)

    if (x, y) == (0, 0):
      return
    elif y == 0 and x > 0:
      angle = pi/2
    elif y == 0 and x < 0:
      angle = -pi/2
    elif y < 0:
      angle = atan (float(x)/float(y)) + pi
    else:
      angle = atan (float(x)/float(y))

    self._curr_velocity = 0
      
    x_step = sin(angle)
    y_step = cos(angle)
            
    self.timeout_id = gobject.timeout_add(self.pan_rate, self._panStep,
                                          x_step, y_step)

  def _panStep (self, x_step, y_step):
    '''
    Increment X and Y coordinates in a certain vector.
    Accelerate, deccelerate accordingly.

    @param x_step: Relative progess that should be made in the X vector.
    @type x_step: float
    @param y_step: Relative progess that should be made in the Y vector.
    @type y_step: float
    '''
    # If accel is set to zero it is disabled.
    if self.accel == 0:
      self._curr_velocity = self.velocity
      # currentvelocity**2/(2*acceleration) = The distance needed to get
      # to a complete stop at the current velocity and deceleration.
    elif (self._curr_velocity**2/(2*self.accel)) >= self._vect_length:
      self._curr_velocity -= self.accel
      # If the current velocity is smaller than the final velocity 
      # then accelerate.
    elif self._curr_velocity < self.velocity:
      self._curr_velocity += self.accel

    x = self._curr_velocity*x_step
    y = self._curr_velocity*y_step
    
    self._curr_coord = (x + self._curr_coord[0], 
                        y + self._curr_coord[1])
      
    progress = sqrt(x**2 + y**2)
    self._vect_length -= progress

    if self._vect_length < 0:
      self.goToPos (self._end_coord)
      return False
    else:
      self.goToPos (tuple(map(int,map(round,self._curr_coord))))
      return True

  def stopPan(self):
    '''
    Stop current pan motion.
    '''
    if self.isPanning():
      gobject.source_remove(self.timeout_id)
      self.timeout_id = None

  def isPanning(self):
    '''
    Determine if a pan is accuring.

    @return: True if panning, False if not.
    @rtype: boolean
    '''
    if self.timeout_id is None:
      return False
    else:
      return True

  def goToPos(self, pos):
    '''
    Actual method that translates the zoomer, to be implemented by perk.
    '''
    raise NotImplementedError


class GnomeMagState(Perk.PerkState):
  '''
  Defines a single set of state variables for all instances of the 
  L{GnomeMagPerk} and their L{Task}s. 
  The variables are configuration settings that should be respected 
  in all instances of L{GnomeMagPerk}.

  @cvar MouseTrack: Track cursor
  @type MouseTrack: boolean
  @cvar FocusTrack: Track focus
  @type MouseTrack: boolean
  @cvar CaretTrack: Track caret
  @type MouseTrack: boolean
  @cvar SmoothPan: Smooth pan
  @type MouseTrack: boolean
  @cvar PanAccel: Pan acceleration
  @type PanAccel: int
  @cvar PanVelocity: Pan velocity
  @type PanVelocity: int
  
  '''
  def init(self):
    self.newBool('MouseTrack', True, _('Track cursor'),
       _('Magnifier is to track mouse movemenself.'))
    self.newBool('FocusTrack', True, _('Track focus'),
       _('Magnifier is to track focused area'))
    self.newBool('CaretTrack', True, _('Track caret'),
       _('Magnifier is to track text caret'))
    self.newBool('SmoothPan', False, _('Smooth panning'),
       _('Magnifier will pan to next selection'))
    self.newRange('PanAccel', 40, _('Pan Acceleration'), 1, 100, 0,
       _('Acceleration rate of magnifier pan motion.'))
    self.newRange('PanVelocity', 600, _('Pan Velocity'), 1, 1000, 0,
       _('Velocity of magnifier pan motion.'))
    
  def getGroups(self):
    g = self.newGroup()
    t = g.newGroup(_('Tracking'))
    t.extend(['MouseTrack', 
              'FocusTrack', 
              'CaretTrack', 
              'SmoothPan', 
              'PanAccel', 
              'PanVelocity'])
    return g

class GnomeMagPerk(Perk.Perk, ZoomMove):
  '''
  A perk to control the Gnome Magnifier.
  '''

  STATE=GnomeMagState
  end_point = None

  def init(self):
    '''
    Registers L{Task}s to handle focus, caret, selector, mouse, and 
    state change events. 
    '''
    self.setPerkIdealOutput('magnifier')

    self.registerTask(MouseTrack())
    self.registerTask(HandleSelectorChange('default selector'))
    self.registerTask(HandleFocusChange('default focus'))
    self.registerTask(HandleCaretChange('default caret'))
    self.registerTask(IncreaseZoom('increase zoom'))
    self.registerTask(DecreaseZoom('decrease zoom'))

    kbd = self.getInputDevice('Keyboard')
    self.addInputModifiers(kbd, kbd.AEK_ALT_L, kbd.AEK_SHIFT_L, 
       kbd.AEK_ALT_R, kbd.AEK_SHIFT_R) 

    self.registerCommand(kbd, 'increase zoom', False,
                         [kbd.AEK_ALT_L, kbd.AEK_SHIFT_L, kbd.AEK_PAGE_UP])
    self.registerCommand(kbd, 'decrease zoom', False,
                         [kbd.AEK_ALT_L, kbd.AEK_SHIFT_L, kbd.AEK_PAGE_DOWN])


    self.accel = self.perk_state.PanAccel*(self.pan_rate/1000.0)
    self.velocity = self.perk_state.PanVelocity*(self.pan_rate/1000.0)
    self.smooth_pan = self.perk_state.SmoothPan

    for setting in self.state:
      setting.addObserver(self._updateSetting)

    self.focus_flag = False
    self.last_caret_offset = 0
    
  def _updateSetting(self, state, setting):
    name = setting.name
    value = setting.value
    if name == 'PanAccel':
      self.accel = value*(self.pan_rate/1000.0)
    elif name == 'PanVelocity':
      self.velocity = value*(self.pan_rate/1000.0)
    elif name == 'SmoothPan':
      self.smooth_pan = value
    
  def getDescription(self):
    return _('Gnome Magnifier.')

  def goToPos(self, pos):
    '''
    Send command to magnifier device to center zoomer on given coordinate.
    
    @param pos: Coordinate
    @type pos: tuple
    '''
    self.send(CMD_GOTO, pos)

class IncreaseZoom(Task.InputTask):
  '''
  Increase the magnification factor.

  @see: L{DecreaseZoom}
  '''
  def execute(self, **kwargs):
    style = self.getStyle()
    if style.Zoom + 0.5 <= style.ZoomMax:
      style.Zoom += 0.5

class DecreaseZoom(Task.InputTask):
  '''
  Decrease the magnification factor.

  @see: L{IncreaseZoom}
  '''
  def execute(self, **kwargs):
    style = self.getStyle()
    if style.Zoom - 0.5 >= style.ZoomMin:
      style.Zoom -= 0.5

class HandleFocusChange(Task.FocusTask):
  '''
  Moves the magnifier to coordinate of this focused element.
  '''
  def executeGained(self, por, layer, **kwargs):
    if not self.perk.perk_state.FocusTrack:
      return True

    self.perk.focus_flag = True
    end_pos = self.getAccVisualPoint()
    start_pos = self.send(CMD_GOTO, None)
    self.perk.goTo(end_pos, start_pos)
    

class HandleCaretChange(Task.CaretTask):
  '''
  Moves the magnifier to coordinate of this caret
  '''
  def execute(self, por, **kwargs):
    if not self.perk.perk_state.CaretTrack:
      return True

    if self.perk.focus_flag:
      self.perk.focus_flag = False
      return True

    end_pos = self.getAccVisualPoint()

    if por.item_offset != self.perk.last_caret_offset:
      start_pos = self.send(CMD_GOTO, None)
      self.perk.goTo(end_pos, start_pos)
    else:
      self.perk.goTo(end_pos)
    self.perk.last_caret_offset =  por.item_offset

    

class HandleSelectorChange(Task.SelectorTask):
  '''
  Moves the magnifier to coordinate of this selected element
  '''
  def execute(self, por, **kwargs):
    if not self.perk.perk_state.FocusTrack:
      return True

    end_pos = self.getAccVisualPoint()
    start_pos = self.send(CMD_GOTO, None)
    self.perk.goTo(end_pos, start_pos)

class MouseTrack(Task.MouseTask):
  '''
  Moves the magnifier to coordinate of mouse
  '''

  def executeMoved(self, pos, **kwargs):
    '''
    Execute on each mouse event, for now we only look at cursor movements.
    '''
    if not self.perk.perk_state.MouseTrack:
      return True

    self.perk.goTo(pos)
