packages feed

vision-0.0.5.0: src/Properties/Editor/Model.hs

-- -*-haskell-*-
--  Vision (for the Voice): an XMMS2 client.
--
--  Author:  Oleg Belozeorov
--  Created: 6 Jul. 2010
--
--  Copyright (C) 2010, 2011 Oleg Belozeorov
--
--  This program is free software; you can redistribute it and/or
--  modify it under the terms of the GNU General Public License as
--  published by the Free Software Foundation; either version 3 of
--  the License, or (at your option) any later version.
--
--  This program is distributed in the hope that it will be useful,
--  but WITHOUT ANY WARRANTY; without even the implied warranty of
--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
--  General Public License for more details.
--

module Properties.Editor.Model
  ( initEditorModel
  , store
  , populateModel
  , resetModel
  , propertyText
  , getNavEnables
  , prevTrack
  , nextTrack
  , togglePerTrack
  , changeProperty
  , writeProperties
  ) where

import Prelude hiding (lookup)

import Control.Concurrent
import Control.Concurrent.STM
import Control.Concurrent.STM.TWatch
import Control.Concurrent.STM.TGVar

import Control.Monad
import Control.Monad.State hiding (State, withState, state)
import Control.Applicative
import Control.Arrow

import Data.Maybe
import Data.List hiding (lookup, union)
import Data.Array
import Data.Map (Map, union)
import qualified Data.Map as Map

import Graphics.UI.Gtk hiding (add, get, Entry)

import qualified XMMS2.Client as X

import XMMS
import Context
import Config
import Utils
import Medialib
import Properties.Property
import Properties.Model


type Entry = (MediaInfo, MediaInfo)

data State
  = State { sPos      :: Int
          , sSize     :: Int
          , sIds      :: Array Int X.MediaId
          , sEntries  :: Map X.MediaId Entry
          , sCurrent  :: Entry
          , sPerTrack :: Bool
          }

makeState list =
  let size    = length list
      ids     = array (0, size - 1) $ zip [0 .. ] $ map fst list
      entries = Map.fromList $
        map (\(id, info) -> (id, (info, Map.empty))) list
      current = fromJust $ Map.lookup (ids ! 0) entries
  in State { sPos      = 0
           , sSize     = size
           , sIds      = ids
           , sEntries  = entries
           , sCurrent  = current
           , sPerTrack = True
           }

data Model
  = Model { mState :: MVar (Maybe State)
          , mStore :: ListStore Property
          }

state = mState context
store = mStore context

setupState list =
  modifyMVar_ state . const . return . Just $ makeState list

resetState =
  modifyMVar_ state . const $ return Nothing

withState d f =
  modifyMVar state $ \state ->
    case state of
      Just s  -> do
        (a, s') <- runStateT f s
        return (Just s', a)
      Nothing ->
        return (Nothing, d)

initEditorModel = do
  context <- initContext
  let ?context = context

  cid <- store `on` rowDeleted $ const saveConfig

  prW <- atomically $ newEmptyTWatch propertiesGeneration
  forkIO $ forever $ do
    void $ atomically $ watch prW
    postGUISync $ withSignalBlocked cid updateProperties

  xcW <- atomically $ newTGWatch connectedV
  forkIO $ forever $ do
    void $ atomically $ watch xcW
    postGUISync $ do
      resetModel
      touchAll

  loadConfig

  return ?context

initContext = do
  state <- newMVar Nothing
  store <- listStoreNew []
  return $ augmentContext
    Model { mState = state
          , mStore = store
          }

loadConfig = do
  cfg <- mapM property =<< config configFile []
  populateStore cfg

saveConfig = do
  names <- map propName <$> listStoreToList store
  writeConfig configFile names
  return ()

updateProperties = do
  cur <- mapM (property . propName) =<< listStoreToList store
  listStoreClear store
  populateStore cur

populateStore cur = do
  all <- getProperties
  mapM_ (listStoreAppend store) $
    unionBy (eqBy propName) (catMaybes cur) all

configFile = "property-editor.conf"

populateModel list = do
  setupState list
  touchAll

resetModel =
  resetState

propertyText prop = withState "" $ do
  (b, c) <- gets sCurrent
  let key = propKey prop
      val = Map.lookup key c `mplus` Map.lookup key b
  return $ maybe "" (showValue prop) val

getNavEnables = withState (False, False) $ do
  t <- gets sPerTrack
  p <- gets sPos
  s <- gets sSize
  return (t && p > 0, t && p < s - 1)

prevTrack = stepTrack (-1)
nextTrack = stepTrack 1

stepTrack inc = do
  withState () $ modify $ \s ->
    let e = Map.insert (sIds s ! sPos s) (sCurrent s) (sEntries s)
        p = sPos s + inc
        c = e Map.! (sIds s ! p) in
    s { sEntries = e
      , sPos     = p
      , sCurrent = c
      }
  touchAll

touchAll = do
  s <- listStoreGetSize store
  mapM_ touch [0 .. s - 1]

touch n = do
  Just iter <- treeModelGetIter store [n]
  treeModelRowChanged store [n] iter

togglePerTrack = do
  withState () $ modify $ \s ->
    if sPerTrack s
    then let
      e = Map.insert (sIds s ! sPos s) (sCurrent s) (sEntries s)
      c = (common $ Map.elems e, Map.empty)
      in s { sPerTrack = False
           , sEntries  = e
           , sCurrent  = c
           }
    else let
      u = snd $ sCurrent s
      e = Map.map (second $ Map.union u) $ sEntries s
      c = e Map.! (sIds s ! sPos s)
      in s { sPerTrack = True
           , sEntries  = e
           , sCurrent  = c
           }
  touchAll

common e =
  let (c, i)     = foldl g (h, h) t
      f a b      = if a == b then Just a else Nothing
      g (c, i) e = (Map.differenceWith f c e, Map.intersection i e)
      (h : t)    = map (uncurry $ flip Map.union) e
  in Map.intersection c i

changeProperty n prop text = do
  maybeVal <- case trim text of
    [] -> return . Just $ X.PropString ""
    st -> readValue prop st
  case maybeVal of
    Nothing  -> return False
    Just val -> withState False $ do
      (b, c) <- gets sCurrent
      let key = propKey prop
          cur = Map.lookup key c `mplus` Map.lookup key b
          res = maybeVal /= cur
      when res $ do
        modify $ \s -> s { sCurrent = (b, Map.insert key val c) }
        liftIO $ touch n
      return res

writeProperties = do
  changes <- withState [] extractChanges
  mapM_ writeForId changes
  where writeForId (id, cs) =
          mapM_ (writeProperty id) cs

writeProperty id (key, val) =
  X.medialibEntryPropertySet xmms id
  (Just "client/generic/override")
  key val

extractChanges = do
  s <- get
  let c = sCurrent s
      e = if sPerTrack s
          then Map.insert (sIds s ! sPos s) c (sEntries s)
          else let u = snd c in
          Map.map (second $ union u) $ sEntries s
      e' = Map.map (\(b, c) -> (c `union` b, Map.empty)) e
  put s { sEntries = e'
        , sCurrent = (snd c `union` fst c, Map.empty)
        }
  return $ map (\(id, (_, c)) -> (id, Map.toList c)) $ Map.toList e