packages feed

barrie-0.4: src/Barrie/Gadgets.hs

module Barrie.Gadgets (Gadget, Behaviour,
                       displayG, editorG, commandG, sectionG,
                       chooserG, enumChooserG, chooseEqG, childG,
                       gadgetDisplay, gadgetUpdate, gadgetCommand,
                       gadgetChooser, gadgetChild,
                       findGadget, gadgetName, flatName,
                       gadgetConfig, gadgetEnabled, enabled, dynamicLabel,
                       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 (Interface 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, Dynamic)]),
                     elementRender    :: Dynamic -> Dynamic
                   }

data Interface a = IF { ifGadget :: Gadget a }

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 :: (Dynamic -> Dynamic) -> Gadget a -> Gadget a
renderWith r = updateElement (\ e -> e { elementRender = r })

setChoices :: (a -> [(Dynamic, 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 }

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))

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

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 $ IF $ 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 (transformI get set) (gadgetInner g),
        gadgetLayout   = boxLayout,
        gadgetChildren = map (transform get set) (gadgetChildren g)
      }

-- transformL :: (a -> b) -> (b -> a -> a) -> AutoLayoutFunction b
--            -> AutoLayoutFunction a
-- transformL get set f = \ layout gs = map

transformI :: (a -> b) -> (b -> a -> a) -> Interface b -> Interface a
transformI get set (IF g) = IF (transform get set 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)
         -> (b -> a -> a)
         -> (a -> [b])
         -> Gadget a
chooserG name get set getChoices =
    autoLayout chooserLayout $ setChoices choices $ editorG name get' set'
        where get' st = (choices st, get st)
              set' (vs, 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 = zip (map toDyn (getChoices st))
                               (map toDyn [0 :: Int ..])
              getValues ds = map (fromJust . fromDynamic . fst) 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 toStr $ editorG name get' set'
        where get' st = (elems st, fromEnum $ get st)
              set' (_, v) = set $ toEnum v
              elems st = let vs = [minBound ..] `asTypeOf` [get st]
                             is = map fromEnum vs
                             ts = map show vs
                         in zip (map toDyn ts) (map toDyn is)
              toStr d = let (xs, v) = fromDyn d ([], -100)
                            e = toEnum ((v :: Int))
                            _ = xs :: [(Dynamic,Dynamic)]
                        in toDyn (show (e `asTypeOf` get undefined))

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

findGadget :: [String] -> Behaviour a -> Maybe (Gadget a)
findGadget name (B gs) = case filter (match name) gs of
                           []  -> Nothing
                           g:_ -> Just g
    where match [] _ = False
          match nm G { gadgetName = gnm } =
              isPrefixOf (reverse nm) (reverse gnm)

-- |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)

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 (IF 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 }])

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, 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 (IF 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 }