-----------------------------------------------------------------------------
-- |
-- Module      :  XMonadContrib.UpdatePointer
-- Copyright   :  (c) Robert Marlow <robreim@bobturf.org>
-- License     :  BSD3-style (see LICENSE)
--
-- Maintainer  :  Robert Marlow <robreim@bobturf.org>
-- Stability   :  stable
-- Portability :  portable
--
-- Causes the pointer to follow whichever window focus changes to. Compliments
-- the idea of switching focus as the mouse crosses window boundaries to
-- keep the mouse near the currently focused window
--
-----------------------------------------------------------------------------

module XMonad.Actions.UpdatePointer
    (
     -- * Usage
     -- $usage
     updatePointer
     , PointerPosition (..)
    )
    where

import XMonad
import Control.Monad
import XMonad.StackSet (member, peek, screenDetail, current)
import Data.Maybe

-- $usage
-- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@:
--
-- > import XMonad
-- > import XMonad.Actions.UpdatePointer
--
-- Enable it by including it in your logHook definition. Eg:
--
-- > logHook = updatePointer Nearest
--
-- which will move the pointer to the nearest point of a newly focused window, or
--
-- > logHook = updatePointer (Relative 0.5 0.5)
--
-- which will move the pointer to the center of a newly focused window.
--
-- To use this with an existing logHook, use >> :
--
-- > logHook = dynamicLog
-- >           >> updatePointer (Relative 1 1)
--
-- which moves the pointer to the bottom-right corner of the focused window.

data PointerPosition = Nearest | Relative Rational Rational | TowardsCentre Rational Rational
    deriving (Read,Show)

-- | Update the pointer's location to the currently focused
-- window or empty screen unless it's already there, or unless the user was changing
-- focus with the mouse
updatePointer :: PointerPosition -> X ()
updatePointer p = do
  ws <- gets windowset
  dpy <- asks display
  rect <- case peek ws of
        Nothing -> return $ (screenRect . screenDetail .current) ws
        Just w  -> windowAttributesToRectangle `fmap` io (getWindowAttributes dpy w)
  root <- asks theRoot
  mouseIsMoving <- asks mouseFocused
  (_sameRoot,_,currentWindow,rootx,rooty,_,_,_) <- io $ queryPointer dpy root
  drag <- gets dragging
  unless (pointWithin (fi rootx) (fi rooty) rect
          || mouseIsMoving
          || isJust drag
          || not (currentWindow `member` ws || currentWindow == none)) $
    case p of
    Nearest -> do
      let x = moveWithin (fi rootx) (rect_x rect) (fi (rect_x rect) + fi (rect_width  rect))
          y = moveWithin (fi rooty) (rect_y rect) (fi (rect_y rect) + fi (rect_height rect))
      io $ warpPointer dpy none root 0 0 0 0 x y
    TowardsCentre xfrc yfrc -> do
      let cx = fi (rect_width rect) / 2 + fi (rect_x rect)
          cy = fi (rect_height rect) / 2 + fi (rect_y rect)
          x,y,cx,cy :: Rational
          x = moveWithin (fi rootx) (fi $ rect_x rect) (fi (rect_x rect) + fi (rect_width  rect))
          y = moveWithin (fi rooty) (fi $ rect_y rect) (fi (rect_y rect) + fi (rect_height rect))
      io $ warpPointer dpy none root 0 0 0 0 (round $ x + xfrc*(cx-x)) (round $ y + yfrc*(cy-y))
    Relative h v ->
      io $ warpPointer dpy none root 0 0 0 0
           (rect_x rect + fraction h (rect_width rect))
           (rect_y rect + fraction v (rect_height rect))
        where fraction x y = floor (x * fromIntegral y)

windowAttributesToRectangle :: WindowAttributes -> Rectangle
windowAttributesToRectangle wa = Rectangle (fi (wa_x wa))     (fi (wa_y wa))
                                           (fi (wa_width wa)) (fi (wa_height wa))
moveWithin :: Ord a => a -> a -> a -> a
moveWithin now lower upper =
    if now < lower
    then lower
    else if now > upper
         then upper
         else now

fi :: (Num b, Integral a) => a -> b
fi = fromIntegral