{-# LANGUAGE OverloadedStrings, OverloadedLabels, PatternSynonyms #-}
module WildBind.Indicator
(
withNumPadIndicator,
wildBindWithIndicator,
bindingHook,
Indicator,
updateDescription,
getPresence,
setPresence,
togglePresence,
quit,
adaptIndicator,
toggleBinding,
NumPadPosition(..)
) where
import Control.Applicative ((<$>))
import Control.Concurrent
(rtsSupportsBoundThreads, newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.Async (withAsync)
import Control.Exception (throwIO, finally)
import Control.Monad (void, forM_)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
import Data.IORef (newIORef, readIORef)
import qualified Data.Map as M
import Data.Monoid (mconcat, First(First))
import Data.Text (Text, pack)
import Data.Word (Word32)
import System.IO (stderr, hPutStrLn)
import System.Environment (getArgs)
import WildBind ( ActionDescription, Option(optBindingHook),
FrontEnd(frontDefaultDescription), Binding, Binding',
binding, Action(Action),
wildBind', defOption
)
import WildBind.Input.NumPad (NumPadUnlocked(..), NumPadLocked(..))
import Paths_wild_bind_indicator (getDataFileName)
import GI.Gdk.Functions (threadsAddIdle)
import GI.GLib.Constants (pattern PRIORITY_DEFAULT)
import GI.Gtk
(
AttrOp((:=))
)
import qualified GI.Gtk as GIAttr (set, get, on)
import GI.Gtk.Enums (WindowType(..), Justification(..))
import qualified GI.Gtk.Functions as GIFunc
import GI.Gtk.Objects.Button (buttonNew, buttonSetAlignment)
import GI.Gtk.Objects.CheckMenuItem (checkMenuItemNewWithMnemonic, checkMenuItemSetActive)
import GI.Gtk.Objects.Container (containerAdd)
import GI.Gtk.Objects.Label (Label, labelNew, labelSetLineWrap, labelSetJustify, labelSetText)
import GI.Gtk.Objects.Menu (Menu, menuNew, menuPopup)
import GI.Gtk.Objects.MenuItem (menuItemNewWithMnemonic)
import GI.Gtk.Objects.Misc (miscSetAlignment)
import GI.Gtk.Objects.StatusIcon (statusIconNewFromFile)
import GI.Gtk.Objects.Table (Table, tableNew, tableAttachDefaults)
import GI.Gtk.Objects.Widget (Widget, widgetSetSizeRequest, widgetShowAll, widgetHide)
import GI.Gtk.Objects.Window
( Window, windowNew, windowSetKeepAbove, windowSetTitle, windowMove
)
data Indicator s i =
Indicator
{ updateDescription :: i -> ActionDescription -> IO (),
getPresence :: IO Bool,
setPresence :: Bool -> IO (),
quit :: IO (),
allButtons :: [i]
}
togglePresence :: Indicator s i -> IO ()
togglePresence ind = (setPresence ind . not) =<< getPresence ind
transportIndicator :: Indicator s i -> Indicator s i
transportIndicator ind = ind { updateDescription = \i d -> postGUIAsync $ updateDescription ind i d,
getPresence = postGUISync $ getPresence ind,
setPresence = \visible -> postGUIAsync $ setPresence ind visible,
quit = postGUISync $ quit ind
}
class NumPadPosition a where
toNumPad :: a -> NumPadLocked
instance NumPadPosition NumPadLocked where
toNumPad = id
instance NumPadPosition NumPadUnlocked where
toNumPad input = case input of
NumInsert -> NumL0
NumEnd -> NumL1
NumDown -> NumL2
NumPageDown -> NumL3
NumLeft -> NumL4
NumCenter -> NumL5
NumRight -> NumL6
NumHome -> NumL7
NumUp -> NumL8
NumPageUp -> NumL9
NumDivide -> NumLDivide
NumMulti -> NumLMulti
NumMinus -> NumLMinus
NumPlus -> NumLPlus
NumEnter -> NumLEnter
NumDelete -> NumLPeriod
data NumPadConfig =
NumPadConfig { confButtonWidth, confButtonHeight :: Int,
confWindowX, confWindowY :: Int,
confIconPath :: FilePath
}
numPadConfig :: IO NumPadConfig
numPadConfig = do
icon <- getDataFileName "icon.svg"
return NumPadConfig
{ confButtonWidth = 70,
confButtonHeight = 45,
confWindowX = 0,
confWindowY = 0,
confIconPath = icon
}
type NumPadContext = ReaderT NumPadConfig IO
withNumPadIndicator :: (NumPadPosition i, Enum i, Bounded i) => (Indicator s i -> IO ()) -> IO ()
withNumPadIndicator action = if rtsSupportsBoundThreads then impl else error_impl where
error_impl = throwIO $ userError "You need to build with -threaded option when you use WildBind.Indicator.withNumPadIndicator function."
textArgs = fmap (map pack) $ getArgs
impl = do
void $ (GIFunc.init . Just) =<< textArgs
conf <- numPadConfig
indicator <- createMainWinAndIndicator conf
status_icon <- createStatusIcon conf indicator
status_icon_ref <- newIORef status_icon
withAsync (asyncAction indicator) $ \_ -> do
GIFunc.main
void $ readIORef status_icon_ref
createMainWinAndIndicator conf = flip runReaderT conf $ do
win <- newNumPadWindow
(tab, updater) <- newNumPadTable
containerAdd win tab
let indicator = Indicator
{ updateDescription = \i d -> updater i d,
getPresence = GIAttr.get win #visible,
setPresence = \visible -> if visible then widgetShowAll win else widgetHide win,
quit = GIFunc.mainQuit,
allButtons = enumFromTo minBound maxBound
}
void $ GIAttr.on win #deleteEvent $ \_ -> do
widgetHide win
return True
return indicator
asyncAction indicator =
(action $ transportIndicator indicator) `finally` (postGUIAsync GIFunc.mainQuit)
createStatusIcon conf indicator = do
status_icon <- statusIconNewFromFile $ confIconPath conf
void $ GIAttr.on status_icon #popupMenu $ \button time -> do
menu <- makeStatusMenu indicator
menuPopup menu (Nothing :: Maybe Widget) (Nothing :: Maybe Widget) Nothing button time
return status_icon
wildBindWithIndicator :: Ord i => Indicator s i -> Binding s i -> FrontEnd s i -> IO ()
wildBindWithIndicator ind b front = wildBind' (defOption { optBindingHook = bindingHook ind front }) b front
bindingHook :: Ord i => Indicator s1 i -> FrontEnd s2 i -> [(i, ActionDescription)] -> IO ()
bindingHook ind front bind_list = forM_ (allButtons ind) $ \input -> do
let desc = M.findWithDefault (frontDefaultDescription front input) input (M.fromList bind_list)
updateDescription ind input desc
newNumPadWindow :: NumPadContext Window
newNumPadWindow = do
win <- windowNew WindowTypeToplevel
windowSetKeepAbove win True
GIAttr.set win [ #skipPagerHint := True,
#skipTaskbarHint := True,
#acceptFocus := False,
#focusOnMap := False
]
windowSetTitle win "WildBind Description"
win_x <- (fromIntegral . confWindowX) <$> ask
win_y <- (fromIntegral . confWindowY) <$> ask
windowMove win win_x win_y
return win
type DescriptActionGetter i = i -> First (ActionDescription -> IO ())
newNumPadTable :: NumPadPosition i => NumPadContext (Table, (i -> ActionDescription -> IO ()))
newNumPadTable = do
tab <- tableNew 5 4 False
(\label -> labelSetText label "NumLock") =<< addButton tab 0 1 0 1
descript_action_getter <-
fmap mconcat $ sequence $
[ getter NumLDivide $ addButton tab 1 2 0 1,
getter NumLMulti $ addButton tab 2 3 0 1,
getter NumLMinus $ addButton tab 3 4 0 1,
getter NumL7 $ addButton tab 0 1 1 2,
getter NumL8 $ addButton tab 1 2 1 2,
getter NumL9 $ addButton tab 2 3 1 2,
getter NumLPlus $ addButton tab 3 4 1 3,
getter NumL4 $ addButton tab 0 1 2 3,
getter NumL5 $ addButton tab 1 2 2 3,
getter NumL6 $ addButton tab 2 3 2 3,
getter NumL1 $ addButton tab 0 1 3 4,
getter NumL2 $ addButton tab 1 2 3 4,
getter NumL3 $ addButton tab 2 3 3 4,
getter NumLEnter $ addButton tab 3 4 3 5,
getter NumL0 $ addButton tab 0 2 4 5,
getter NumLPeriod $ addButton tab 2 3 4 5
]
let description_updater = \input -> case descript_action_getter $ toNumPad input of
First (Just act) -> act
First Nothing -> const $ return ()
return (tab, description_updater)
where
getter :: Eq i => i -> NumPadContext Label -> NumPadContext (DescriptActionGetter i)
getter bound_key get_label = do
label <- get_label
return $ \in_key -> First (if in_key == bound_key then Just $ labelSetText label else Nothing)
addButton :: Table -> Word32 -> Word32 -> Word32 -> Word32 -> NumPadContext Label
addButton tab left right top bottom = do
lab <- labelNew Nothing
labelSetLineWrap lab True
miscSetAlignment lab 0 0.5
labelSetJustify lab JustificationLeft
button <- buttonNew
buttonSetAlignment button 0 0.5
containerAdd button lab
tableAttachDefaults tab button left right top bottom
bw <- (fromIntegral . confButtonWidth) <$> ask
bh <- (fromIntegral . confButtonHeight) <$> ask
widgetSetSizeRequest lab (bw * fromIntegral (right - left)) (bh * fromIntegral (bottom - top))
return lab
makeStatusMenu :: Indicator s i -> IO Menu
makeStatusMenu ind = impl where
impl = do
menu <- menuNew
containerAdd menu =<< makeQuitItem
containerAdd menu =<< makeToggler
return menu
makeQuitItem = do
quit_item <- menuItemNewWithMnemonic "_Quit"
widgetShowAll quit_item
void $ GIAttr.on quit_item #activate (quit ind)
return quit_item
makeToggler = do
toggler <- checkMenuItemNewWithMnemonic "_Toggle description"
widgetShowAll toggler
checkMenuItemSetActive toggler =<< getPresence ind
void $ GIAttr.on toggler #toggled (togglePresence ind)
return toggler
adaptIndicator :: (i -> i')
-> (i' -> Maybe i)
-> Indicator s i
-> Indicator s i'
adaptIndicator mapper cmapper ind =
ind { updateDescription = newDesc,
allButtons = map mapper $ allButtons ind
}
where
newDesc input = case cmapper input of
Nothing -> const $ return ()
Just orig_input -> updateDescription ind orig_input
toggleBinding :: (NumPadPosition i, Ord i, Enum i, Bounded i)
=> Indicator s i
-> NumPadLocked
-> Binding' bs fs i
toggleBinding ind button = binding $ map (\input -> (input, Action "Toggle description" $ togglePresence ind)) help_likes
where
help_likes = filter ((== button) . toNumPad) $ enumFromTo minBound maxBound
postGUIAsync :: IO () -> IO ()
postGUIAsync action = void $ threadsAddIdle PRIORITY_DEFAULT (action >> return False)
postGUISync :: IO a -> IO a
postGUISync action = do
mret <- newEmptyMVar
postGUIAsync $ do
ret <- action
putMVar mret ret
takeMVar mret