packages feed

barrie-0.4.1: src/Barrie/Gadgets.hs

module Barrie.Gadgets (Gadget, Behaviour,
                       displayG, editorG, commandG, sectionG,
                       chooserG, chooserG', enumChooserG, chooseEqG,
                       childG, initG, returnG,
                       gadgetDisplay, gadgetUpdate, gadgetCommand,
                       gadgetChooser, gadgetChild,
                       findGadget, gadgetName, flatName,
                       gadgetElementType,
                       gadgetConfig, gadgetEnabled, enabled, dynamicLabel,
                       columnTitles,
                       renderWith, gadgetRenderer,
                       gadgets, abstract,
                       createLayout)
where

import Control.Monad

import Data.Dynamic
import Data.List
import Data.Maybe
import Data.Typeable

import Barrie.Config
import Barrie.Style
import Barrie.Widgets

data Gadget a = G { gadgetName     :: [String],
                    gadgetConfig   :: Config a,
                    gadgetElement  :: Maybe (Element a),
                    gadgetInner    :: Maybe (Gadget a),
                    gadgetLayout   :: AutoLayoutFunction a,
                    gadgetChildren :: [Gadget a]
                  }

data AutoLayout = AL { alDirection :: LayoutDirection,
                       alEditable  :: Bool,
                       alNames     :: [String]
                     }

instance (Typeable a) => Typeable (Gadget a) where
    typeOf g = mkTyConApp (mkTyCon "Barrie.Gadgets.Gadget")
                          [typeOf (elementType (fromJust (gadgetElement g)))]

data Element a = E { elementType      :: TypeRep,
                     elementGet       :: a -> Dynamic,
                     elementSet       :: Dynamic -> a -> a,
                     elementChoices   :: Maybe (a -> [Dynamic]),
                     elementRender    :: [Dynamic -> Dynamic]
                   }

instance (Typeable a) => Typeable (Element a) where
    typeOf e = mkTyConApp (mkTyCon "Barrie.Gadgets.Element") [elementType e]

data Behaviour a = B [Gadget a]

flatName :: Gadget a -> String
flatName = intercalate "." . gadgetName

mkGadget :: String -> Element a -> Gadget a
mkGadget name e = G [name] defaultConfig (Just e) Nothing boxLayout []

mkElement :: (Typeable b) =>
             (a -> b)
          -> (b -> a -> a)
          -> Element a
mkElement get set = E tyrep (toDyn . get) (\ x -> set (get' x)) Nothing [id]
    where get' x = case fromDynamic x of
                     Nothing -> error $ "'impossible' type error detected: " ++
                                        show x ++ " " ++ show (typeOf (get' x))
                     Just v  -> v
          tyrep = typeOf (get undefined)

updateElement :: (Element a -> Element a) -> Gadget a -> Gadget a
updateElement update g = case gadgetElement g of
                           Nothing -> g
                           Just e  -> g { gadgetElement = Just $ update e }

renderWith :: (Typeable b) =>
              ([b -> Dynamic]) -> Gadget a -> Gadget a
renderWith rs = updateElement
                  (\ e -> e { elementRender = map (. fromJust . fromDynamic) rs
                            })

gadgetRenderer :: Gadget a -> [Dynamic -> Dynamic]
gadgetRenderer G { gadgetElement = Nothing } = []
gadgetRenderer G { gadgetElement = Just e } = elementRender e

setChoices :: (a -> [Dynamic]) -> Gadget a -> Gadget a
setChoices getChoices =
    updateElement (\ e -> e { elementChoices = Just getChoices })

autoLayout :: AutoLayoutFunction a -> Gadget a -> Gadget a
autoLayout f g = g { gadgetLayout = f }

lowLevelGadget :: (Typeable b) =>
                  String
               -> (a -> b)
               -> (b -> a -> a)
               -> TypeRep
               -> Gadget a
lowLevelGadget name get set ty = mkGadget name elmt { elementType = ty }
    where elmt = mkElement get set



displayG :: (Typeable b) => String -> (a -> b) -> Gadget a
displayG name get = autoLayout (typeableLayout (typeOf $ get undefined)) $
                       mkGadget name (mkElement get (\ _ st -> st))

editorG :: (Typeable b) => String -> (a -> b) -> (b -> a -> a) -> Gadget a
editorG name get set = autoLayout (typeableLayout (typeOf $ get undefined)) $
                            mkGadget name (mkElement get set)

commandG :: String -> (a -> a) -> Gadget a
commandG name update = autoLayout buttonLayout $
                          mkGadget name (mkElement (const ()) (\ () -> update))

initG :: (Typeable b) => String -> (b -> a) -> Gadget a
initG name initfn = autoLayout textLayout $
                        mkGadget name (mkElement initError (\ x _ -> initfn x))
    where initError _ = error "can't get value from an init gadget"

sectionG :: String -> [Gadget a] -> Gadget a
sectionG name gs = G [name] defaultConfig Nothing Nothing boxLayout gs

returnG :: String -> Gadget a
returnG name = G [name] (quitConfig True defaultConfig)
                        Nothing Nothing buttonLayout []

childG :: String            -- ^ gadget name
       -> (a -> b)          -- ^ get child state
       -> (b -> a -> a)     -- ^ update parent state with child state
       -> Gadget b          -- ^ child gadget
       -> Gadget a
childG name get set child =
   G { gadgetName     = [name],
       gadgetConfig   = defaultConfig,
       gadgetElement  = Nothing,
       gadgetInner    = Just $ transform get set child,
       gadgetLayout   = boxLayout,
       gadgetChildren = []
     }

transform :: (a -> b) -> (b -> a -> a) -> Gadget b -> Gadget a
transform get set g =
    G { gadgetName     = gadgetName g,
        gadgetConfig   = transformConfig get (gadgetConfig g),
        gadgetElement  = liftM (transformE get set) (gadgetElement g),
        gadgetInner    = liftM (transform get set) (gadgetInner g),
        gadgetLayout   = boxLayout,
        gadgetChildren = map (transform get set) (gadgetChildren g)
      }

transformE :: (a -> b) -> (b -> a -> a) -> Element b -> Element a
transformE get set e = e { elementGet = elementGet e . get,
                           elementSet = xfset (elementSet e),
                           elementChoices = xfchoices (elementChoices e)
                         }
   where xfset setb = (\ v st -> set (setb v (get st)) st)
         xfchoices Nothing      = Nothing
         xfchoices (Just getcs) = Just $ getcs . get

-- |A gadget that offers a choice between alternatives.  This function
-- creates a gadget that expects an Int representing the index of the
-- chosen item from the state.  If it's more convenient to supply the
-- actual chosen value, use chooseEqG instead.

chooserG :: (Typeable b) =>
            String
         -> (a -> Int)
         -> (Int -> a -> a)
         -> (a -> [b])
         -> Gadget a
chooserG name get set getChoices =
    chooserG' name get set getChoices [toDyn]

chooserG' :: (Typeable b) =>
             String
          -> (a -> Int)
          -> (Int -> a -> a)
          -> (a -> [b])
          -> ([b -> Dynamic])
          -> Gadget a
chooserG' name get set getChoices renderer =
    autoLayout chooserLayout $ setChoices choices $ renderWith renderer $
               lowLevelGadget name get' set'
                                  (typeOf (head (getChoices undefined)))
        where get' st = (choices st, get st)
              set' (_, v) = set v
              {- if v >= length vs
                             then error ("chooser index " ++ show v ++
                                         " was not in value list " ++
                                         show vs)
                             else set (getValues vs !! v) -}
              choices st = map toDyn (getChoices st)
              --  getValues ds = map (fromJust . fromDynamic) ds

enumChooserG :: (Enum b, Bounded b, Read b, Show b) =>
                String
             -> (a -> b)
             -> (b -> a -> a)
             -> Gadget a
enumChooserG name get set =
    autoLayout enumLayout $
    setChoices elems $ renderWith [renderEnum] $ editorG name get' set'
        where get' st = (elems st, fromEnum $ get st)
              set' (_, v) = if v < 0
                              then error ("not setting enum to " ++ show v)
                              else set $ toEnum v
              elems st = let vs = [minBound .. maxBound] `asTypeOf` [get st]
                             ts = map fromEnum vs
                         in map toDyn ts
              renderEnum v = let e = toEnum v
                                 str = show (e `asTypeOf` get undefined)
                             in toDyn str

chooseEqG :: (Typeable b, Eq b) =>
             String
         -> (a -> b)
         -> (b -> a -> a)
         -> (a -> [b])
         -> Gadget a
chooseEqG name get set choices = chooserG name get' set' choices
    where get' st = case elemIndex (get st) (choices st) of
                      Nothing -> -1
                      Just n  -> n
          set' v st = set (choices st !! v) st

findGadget :: [String] -> Gadget a -> Maybe (Gadget a)
findGadget [] = Just
findGadget (name:names) = \ top -> case filter fst (tries top) of
                                     []        -> Nothing
                                     (_,g):_   -> findGadget names g
    where tries g = [match g, matchInner g] ++
                    map match (gadgetChildren g) ++
                    concatMap tries (gadgetChildren g)
          match g = ([name] == gadgetName g, g)
          matchInner g = case gadgetInner g of
                           Nothing -> (False, g)
                           Just inner -> match inner

-- |Gadgets can change their properties based on the value of their state.
-- Currently, the following configurations are implemented:

-- |dynamicLabel: for write-only gadgets like Command, this can add
-- some state read functionality.  It is interpreted by the
-- renderer.

-- |enabled: a function on the state returning Bool.  If False, the
-- gadget is disabled in some widget- or renderer-specific way
-- (e.g. dimmed, hidden).

addConfig :: (Config a -> Config a) -> Gadget a -> Gadget a
addConfig f g = g { gadgetConfig = f (gadgetConfig g) }

enabled :: (a -> Bool) -> Gadget a -> Gadget a
enabled f = addConfig (enabledCfg f)

gadgetEnabled :: Gadget a -> a -> Bool
gadgetEnabled = isEnabled . gadgetConfig

dynamicLabel :: (a -> String) -> Gadget a -> Gadget a
dynamicLabel f = addConfig (labelConfig f)

columnTitles :: [String] -> Gadget a -> Gadget a
columnTitles ts = addConfig (titleConfig ts)

walk :: [String] -> ([String] -> Gadget a -> [b]) -> Gadget a -> [b]
walk prefix f gadget@(G nm _ _ inner _ gs) =
    f (prefix ++ nm) gadget ++ innerg ++ concatMap (walk (prefix ++ nm) f) gs
        where innerg = case inner of
                         Nothing -> []
                         Just g -> walk (prefix ++ nm) f g

gadgets :: Gadget a -> Behaviour a
gadgets = B . walk [] getGadget
     where getGadget name g = [g { gadgetName = name }]

abstract :: Gadget a -> [String]
abstract = map flatName . walk [] (\ name g -> [g { gadgetName = name }])

gadgetElementType :: Gadget a -> Maybe TypeRep
gadgetElementType g = gadgetElement g >>= return . elementType

gadgetCommand :: Gadget a -> a -> a
gadgetCommand G { gadgetElement = Just e } = elementSet e (toDyn ())
gadgetCommand _ = id

gadgetDisplay :: Gadget a -> a -> Dynamic
gadgetDisplay G { gadgetElement = Just e } = elementGet e
gadgetDisplay g = error $ "Can't connect a display to empty gadget " ++
                          flatName g

gadgetUpdate :: Gadget a -> Dynamic -> a -> a
gadgetUpdate G { gadgetElement = Just e } = elementSet e
gadgetUpdate g = error $ "Can't connect an update to empty gadget " ++
                          flatName g

gadgetChooser :: Gadget a -> a -> [Dynamic]
gadgetChooser g = case getChoices of
                    Nothing -> error $ "Can't connect a chooser to gadget " ++
                                       flatName g
                    Just get -> get
    where getChoices = gadgetElement g >>= elementChoices

gadgetChild :: Gadget a -> Gadget a
gadgetChild g = case gadgetInner g of
                  Just child -> child
                  Nothing    -> error $ "Gadget " ++ flatName g ++
                                        " has no inner child"

startLayout :: AutoLayout
startLayout = AL { alDirection = Vertical,
                   alEditable  = False,
                   alNames     = []
                 }

-- editableLayout :: AutoLayout -> AutoLayout
-- editableLayout al = al { alEditable = True }

type AutoLayoutFunction a = AutoLayout -> [Gadget a] -> Widget

boxLayout :: AutoLayoutFunction a
boxLayout al = boxLayout' . map (createLayout' al')
    where (boxLayout', al') =
              case alDirection al of
                Vertical   -> (vbox, al { alDirection = Horizontal })
                Horizontal -> (hbox, al { alDirection = Vertical })


textLayout :: AutoLayoutFunction a
textLayout = \ al _ -> case alEditable al of
                           True -> textBox
                           False -> textLabel ""

intLayout :: Int -> Int -> AutoLayoutFunction a
intLayout lo hi = \ al _ -> case alEditable al of
                              True -> slider (alDirection al) lo hi 1
                              False -> textLabel ""

boolLayout :: AutoLayoutFunction a
boolLayout = \ _ _ -> checkButton

buttonLayout :: AutoLayoutFunction a
buttonLayout = \ al _ -> caption (last (alNames al)) $ button []

enumLayout :: AutoLayoutFunction a
enumLayout = chooserLayout

chooserLayout :: AutoLayoutFunction a
chooserLayout = \ _ _ -> listView

typeableLayout :: TypeRep -> AutoLayoutFunction a
typeableLayout t = case filter ((t==) . fst) typeableLayouts of
                     []      -> textLayout
                     layouts -> snd (head layouts)
    where typeableLayouts = [(typeOf (1::Int), intLayout 0 999),
                             (typeOf False, boolLayout)]

createLayout :: Gadget a -> Widget
createLayout = createLayout' startLayout

createLayout' :: AutoLayout -> Gadget a -> Widget
createLayout' layout g =
    ui' nm $ gadgetLayout g layout' (gadgetChildren g)
        where nm = alNames layout ++ gadgetName g
              layout' = layout { alNames = nm }