packages feed

proplang-0.1: PropLang/Gtk.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Gtk.hs
-- Copyright   :  (c) Neil Mitchell 2007
-- License     :
--
-- Maintainer  :
-- Stability   :  unstable
-- Portability :  not portable
--
-- Bindings to Gtk
--
-----------------------------------------------------------------------------

module PropLang.Gtk(
    (!),
    text, enabled, key, menu, onClicked, onActivated,
    initPropLang, mainPropLang,
    Window, getWindow, showWindow, showWindowMain, getWindowRaw,
    ComboBox,
    MenuItem, getMenuItem,
    TextView, getTextView, getTextViewRaw,
    textviewBuffer,
    StatusBar, getStatusBar,
    ToolButton, getToolButton,
    FontButton, getFontButton,
    TextEntry, getTextEntry,
    getCtrl,
    
    -- hacks!
    onEnterKey
    ) where

import qualified Graphics.UI.Gtk as Gtk
import Graphics.UI.Gtk hiding (Action, Window, ComboBox, MenuItem, TextView, ToolButton, FontButton, Event, onClicked, onChanged)
import Graphics.UI.Gtk.Glade
import System.Glib

import PropLang.Variable
import PropLang.Value
import PropLang.Event

import Data.IORef
import Data.Maybe
import Data.List
import Foreign.C.Types
import Control.Exception
import Control.Concurrent
import Control.Monad

debug = putStrLn

-- | Initialisation functions from GTK
initPropLang :: IO HandlerId
initPropLang = do if rtsSupportsBoundThreads
                      then error "Don't link with -theaded, Gtk won't work"
                      else do
                             initGUI
                             timeoutAddFull (yield >> return True) priorityDefaultIdle 50

-- | Start Gtk
mainPropLang = mainGUI

-- | Start PropLang
showWindowMain wnd = do
    window wnd `onDestroy` mainQuit
    showWindow wnd
    mainPropLang


-- Property stuff

infixl 9 !

-- | Access widget properties
(!) :: a -> (a -> b) -> b
object ! prop = prop object

-- |
class TextProvider a where; text :: a -> Var String
instance TextProvider Window where; text = windowText
instance TextProvider ComboBox where; text = comboboxText
instance TextProvider TextView where; text = textviewText
instance TextProvider StatusBar where; text = statusbarText
instance TextProvider TextEntry where; text = textentryText
instance TextProvider FontButton where; text = fontbuttonText

-- |
class EnabledProvider a where; enabled :: a -> Var Bool
instance EnabledProvider TextView where; enabled = textviewEnabled
instance EnabledProvider ToolButton where; enabled = toolbuttonEnabled

-- |
class KeyProvider a where; key :: a -> Var String
instance KeyProvider TextView where; key = textviewKey

-- |
class MenuProvider a where; menu :: a -> Var Gtk.Menu
instance MenuProvider MenuItem where; menu = menuitemSubmenu

-- |
class OnClickedProvider a where; onClicked :: a -> Event
instance OnClickedProvider ToolButton where; onClicked = toolbuttonOnClicked
instance OnClickedProvider FontButton where; onClicked = fontbuttonOnClicked

-- |
class OnChangedProvider a where; onChanged :: a -> Event
instance OnChangedProvider ComboBox where; onChanged = comboboxOnChanged

-- |
class OnActivatedProvider a where; onActivated :: a -> Event
instance OnActivatedProvider MenuItem where; onActivated = menuitemOnActivated

-- Helper stuff

gtkProp :: String -> (s -> IO ()) -> (IO s) -> IO (Var s)
gtkProp name set get = newVarWithName name f
    where
        f e = return $ Value (set2 e) get
        set2 e x = do set x
                      raise e

gtkPropEvent ::  String -> (IO () -> IO any_) -> (String -> IO ()) -> (IO String) -> IO (Var String)
gtkPropEvent name reg set get = newVarWithName name f
    where
        f e = do reg (raise e)
                 return $ Value set' get'
              where set' s = do old <- get
	                        debug $ name++": want to change "++(show old)++" to "++(show s)
 				if old /= s then do
					blockEvent e
					set ""
					unblockEvent e
					set s
				 else debug "(not happening)"
	            get'   = do s <- get; debug (name++": Getting "++(show s)); return s
    -- we do not raise on set, as gtk does that anyway (tested with Entry)


-- | Window
data Window = Window {
    xml :: GladeXML, window :: Gtk.Window,
    children :: [(String,AWidget)],
    windowText :: Var String
    }

-- Hack, I guess.
getWindowRaw :: Window -> Gtk.Window
getWindowRaw = window

-- |
getWindow :: FilePath -> String -> IO Window
getWindow file name = do
        dialogXmlM <- xmlNew file
        let dialogXml = case dialogXmlM of
                (Just dialogXml) -> dialogXml
                Nothing -> error $ "Can't find the glade file \"" ++ file ++ "\""

        wnd <- xmlGetWidget dialogXml castToWindow name
        windowText <- gtkProp ("gtk.window.text[" ++ name ++ "]")
                              (windowSetTitle wnd)
                              (windowGetTitle wnd)
                              
        children <- getChildWindowsAll $ toWidget wnd
        c2 <- mapM f children
        return $ Window dialogXml wnd (catMaybes c2) windowText
    where
        f w = do
            name <- widgetGetName w
            if "Gtk" `isPrefixOf` name
                then return Nothing
                else do x <- liftWidget w
                        return $ Just (name,x)
                

-- |
showWindow :: Window -> IO ()
showWindow wnd = widgetShowAll $ window wnd

-- Widgets

data AWidget = AComboBox ComboBox
	     | AMenuItem MenuItem
	     | ATextView TextView
             | ATextEntry TextEntry
             | AStatusBar StatusBar
             | AToolButton ToolButton
	     | AFontButton FontButton
             | AUnknown

liftWidget :: Widget -> IO AWidget
liftWidget x = do
    cb <- getWidgetMaybe castToComboBox x
    mi <- getWidgetMaybe castToMenuItem x
    tv <- getWidgetMaybe castToTextView x
    te <- getWidgetMaybe castToEntry x
    sb <- getWidgetMaybe castToStatusbar x
    tb <- getWidgetMaybe castToToolButton x
    fb <- getWidgetMaybe castToFontButton x
    case () of
	_ | isJust cb -> f AComboBox liftComboBox cb
        _ | isJust mi -> f AMenuItem liftMenuItem mi
        _ | isJust tv -> f ATextView liftTextView tv
        _ | isJust sb -> f AStatusBar liftStatusBar sb
        _ | isJust tb -> f AToolButton liftToolButton tb
	_ | isJust fb -> f AFontButton liftFontButton fb
        _ | isJust te -> f ATextEntry liftTextEntry te
        _ -> return AUnknown
    where
        f wrap conv (Just x) = do
            x2 <- conv x
            return $ wrap x2


getAWidget :: (AWidget -> a) -> Window -> String -> a
getAWidget f wnd name = case lookup name (children wnd) of
                            Nothing -> error $ "Widget not found: " ++ name
                            Just x -> f x


class GetCtrl a where
    getCtrl :: Window -> String -> a

instance GetCtrl ComboBox where ; getCtrl = getComboBox
instance GetCtrl MenuItem where ; getCtrl = getMenuItem
instance GetCtrl TextView where ; getCtrl = getTextView
instance GetCtrl StatusBar where ; getCtrl = getStatusBar
instance GetCtrl ToolButton where ; getCtrl = getToolButton
instance GetCtrl TextEntry where ; getCtrl = getTextEntry
instance GetCtrl FontButton where ; getCtrl = getFontButton

--
-- | ComboBox
--

data ComboBox = ComboBox {
    comboBox :: Gtk.ComboBox,
    comboboxOnChanged :: Event,
    comboboxText :: Var String
    }

-- |
getComboBox :: Window -> String -> ComboBox
getComboBox window ctrl = getAWidget (\(AComboBox x) -> x) window ctrl

liftComboBox :: Gtk.ComboBox -> IO ComboBox
liftComboBox cb = do
    name <- widgetGetName cb
    cbChanged <- newEventName $ "gtk.combobox.changed [" ++ name ++ "]"
    -- Might be better just to use this widget as the source since
    -- it doesn't work as expected (i.e. appends) the other way
    comboboxText <- gtkPropEvent ("gtk.combobox.text[" ++ name ++ "]")
				  (afterChanged cb)
				  (comboBoxSetActiveText cb)
				  (return . maybe "" id =<< comboBoxGetActiveText cb)
    cb `Gtk.onChanged` raise cbChanged
    return $ ComboBox cb cbChanged comboboxText

    where
	comboBoxSetActiveText cb text = do
	    model <- comboBoxGetModel cb
	    iter  <- treeModelGetIterFirst (fromJust model)
	    target <- findInModel (fromJust model) iter text
	    case target of
		Nothing -> return ()
		Just x  -> comboBoxSetActiveIter cb x
	findInModel :: TreeModel -> Maybe TreeIter -> String -> IO (Maybe TreeIter)
	findInModel model iter text = do
	    case iter of
		Nothing -> return Nothing
		Just x  -> do
		    val <- treeModelGetValue model x 0
		    next <- treeModelIterNext model x
		    case val of 
			GVstring (Just y) -> if y == text
						then return $ Just x
						else findInModel model next text
			_                 -> findInModel model next text

--
-- | MenuItem
--

data MenuItem = MenuItem {
    menuItem :: Gtk.MenuItem,
    menuitemOnActivated :: Event,
    menuitemSubmenu :: Var Gtk.Menu
    }

-- |
getMenuItem :: Window -> String -> MenuItem
getMenuItem window ctrl = getAWidget (\(AMenuItem x) -> x) window ctrl

liftMenuItem :: Gtk.MenuItem -> IO MenuItem
liftMenuItem mi = do
    name <- widgetGetName mi
    miActivated <- newEventName $ "gtk.menuitem.activated [" ++ name ++ "]"
    menuitemSubmenu <- gtkProp ("gtk.menuitem.menu[" ++ name ++ "]")
			       (menuItemSetSubmenu mi)
			       (maybe menuNew (return . castToMenu) =<< menuItemGetSubmenu mi)
    mi `onActivateLeaf` raise miActivated
    return $ MenuItem mi miActivated menuitemSubmenu

--
-- | TextView
--

data TextView = TextView {
    textview :: Gtk.TextView,
    textviewText :: Var String, textviewEnabled :: Var Bool,
    textviewKey :: Var String
    }


textviewBuffer :: TextView -> IO TextBuffer
textviewBuffer txt = textViewGetBuffer (textview txt)


getTextViewRaw :: TextView -> Gtk.TextView
getTextViewRaw txt = textview txt


-- |
getTextView :: Window -> String -> TextView
getTextView window ctrl = getAWidget (\(ATextView x) -> x) window ctrl

liftTextView :: Gtk.TextView -> IO TextView
liftTextView txt = do
    name <- widgetGetName txt
    buf <- textViewGetBuffer txt
    textviewText <- gtkPropEvent ("gtk.textview.text[" ++ name ++ "]")
                                 (afterBufferChanged buf)
                                 (textBufferSetText buf)
                                 (textBufferGet buf)
    textviewEnabled <- newEnabled txt ("gtk.textview.enabled[" ++ name ++ "]")
    -- Might be buggy, but I've kept this alternate key API in for now
    textviewKey <- newVarWithName ("gtk.textview.key[" ++ name ++ "]")
	               (`newValueAlwaysTrigger` "")
    txt `onKeyPress` (handle textviewKey)
    return $ TextView txt textviewText textviewEnabled textviewKey

    where
        textBufferGet buf = do
            strt <- textBufferGetStartIter buf
            end <- textBufferGetEndIter buf
            textBufferGetText buf strt end False
        handle k x = do
            case x of
                Key{eventKeyName = name} -> do 
		    k -< name
		    return False
                _ -> return False

--
-- | TextEntry
--

data TextEntry = TextEntry {
    textentry :: Gtk.Entry,
    textentryText :: Var String
    }


-- |
getTextEntry :: Window -> String -> TextEntry
getTextEntry window ctrl = getAWidget (\(ATextEntry x) -> x) window ctrl

liftTextEntry :: Gtk.Entry -> IO TextEntry
liftTextEntry txt = do
    name <- widgetGetName txt
    textentryText <- gtkPropEvent ("gtk.textentry.text[" ++ name ++ "]")
                                  (afterEditableChanged txt)
                                  (entrySetText txt)
                                  (entryGetText txt)
    return $ TextEntry txt textentryText



--
-- | StatusBar
--

data StatusBar = StatusBar {
    statusbar :: Gtk.Statusbar, statusbarText :: Var String,
    context :: CUInt, statusbarValue :: IORef String
    }

-- |
getStatusBar :: Window -> String -> StatusBar
getStatusBar window ctrl = getAWidget (\(AStatusBar x) -> x) window ctrl

liftStatusBar :: Gtk.Statusbar -> IO StatusBar
liftStatusBar sb = do
    name <- widgetGetName sb
    context <- statusbarGetContextId sb ""
    val <- newIORef ""
    statusbarText <- gtkProp ("gtk.statusbar[" ++ name ++ "]")
                             (statusBarSet sb val context)
                             (readIORef val)
    return $ StatusBar sb statusbarText context val
    
    where
        statusBarSet sb val context x = do
            writeIORef val x
            statusbarPop sb context
            statusbarPush sb context x
            return ()

--
-- | ToolButton
--

data ToolButton = ToolButton {
    toolbutton :: Gtk.ToolButton,
    toolbuttonEnabled :: Var Bool,
    toolbuttonOnClicked :: Event
    }


-- |
getToolButton :: Window -> String -> ToolButton
getToolButton window ctrl = getAWidget (\(AToolButton x) -> x) window ctrl

liftToolButton :: Gtk.ToolButton -> IO ToolButton
liftToolButton tb = do
    name <- widgetGetName tb
    tbEnabled <- newEnabled tb ("gtk.toolbutton.enabled[" ++ name ++ "]")
    tbClicked <- newEventName $ "gtk.toolbutton.clicked[" ++ name ++ "]"
    tb `onToolButtonClicked` raise tbClicked
    return $ ToolButton tb tbEnabled tbClicked

--
-- | FontButton
--

data FontButton = FontButton {
    fontbutton :: Gtk.FontButton,
    fontbuttonEnabled :: Var Bool,
    fontbuttonOnClicked :: Event,
    fontbuttonText :: Var String
    }

-- |
getFontButton :: Window -> String -> FontButton
getFontButton window ctrl = getAWidget (\(AFontButton x) -> x) window ctrl

liftFontButton :: Gtk.FontButton -> IO FontButton
liftFontButton fb = do
    name <- widgetGetName fb
    fbEnabled <- newEnabled fb ("gtk.toolbutton.enabled[" ++ name ++ "]")
    fbClicked <- newEventName $ "gtk.toolbutton.clicked[" ++ name ++ "]"
    fb `Gtk.onClicked` raise fbClicked
    fontbuttonText <- gtkPropEvent ("gtk.fontbutton.text[" ++ name ++ "]")
				   (afterFontSet fb)
				   (\x -> fontButtonSetFontName fb x >> return ())
				   (fontButtonGetFontName fb)
    return $ FontButton fb fbEnabled fbClicked fontbuttonText

--
-- Helper functions
--

widgetGetSensitivity :: WidgetClass self => self -> IO Bool
widgetGetSensitivity x = do
    y <- widgetGetState x
    return (y /= StateInsensitive)


newEnabled :: WidgetClass a => a -> String -> IO (Var Bool)
newEnabled x name = gtkProp name (widgetSetSensitivity x) (widgetGetSensitivity x)

-- Special value function for things like key events 
-- that should always trigger
newValueAlwaysTrigger :: (Eq a) => Event -> a -> IO (Value a)
newValueAlwaysTrigger e x = do
        i <- newIORef x
        return $ Value (setter i) (readIORef i)
    where
        setter i x = do
	    old <- readIORef i
	    writeIORef i x
	    raise e

ignore2 :: ((a -> b -> IO ()) -> IO ans) -> IO () -> IO ans
ignore2 app f = app (\a b -> f)



-- window enumeration
getChildWindowsAll :: Widget -> IO [Widget]
getChildWindowsAll w = do
    res <- getWidgetMaybe castToMenuItem w
    child <- case res of
      Nothing -> getChildWindows w
      Just m -> getMenuChildren m
    child2 <- mapM getChildWindowsAll child
    return $ child ++ concat child2


getChildWindows :: Widget -> IO [Widget]
getChildWindows w = do
        c <- getWidgetMaybe castToContainer w
        case c of
            Nothing -> return []
	    Just c -> do
		containerGetChildren c
{-
            Just c -> do
                i <- newIORef []
                containerForeach c (f i)
                readIORef i
    where
        f i x = do
            r <- readIORef i
            writeIORef i (x:r)
-}          

-- A hack to enumerate menu items into PropLang's model
getMenuChildren :: Gtk.MenuItem -> IO [Widget]
getMenuChildren m = do
    sub <- menuItemGetSubmenu m
    case sub of
      Nothing -> return []
      Just menu -> do
        ws <- getChildWindowsAll menu
        --ws2 <- mapM (getMenuChildren . castToMenuItem) ws
        return ws

getWidgetMaybe :: GObjectClass obj => (obj -> conc) -> obj -> IO (Maybe conc)
getWidgetMaybe cast o = 
    Control.Exception.catch
        (return $! Just $! cast o)
        (\e -> return Nothing)




-- short term hack
onEnterKey :: TextView -> IO () -> IO ()
onEnterKey txt act = do
        onKeyPress (textview txt) handle
        return ()
    where
        handle x = do
            case x of
                Key{eventKeyName = "Return"} -> act >> return True
                _ -> return False

    
    {-
    
    type ContainerForeachCB = Widget -> IO ()
    
    
    
    cmdFilename <- xmlGetWidget dialogXml castToButton "cmdFilename"
    lblFilename <- xmlGetWidget dialogXml castToLabel "lblFilename"
    tvStack <- xmlGetWidget dialogXml castToTreeView "tvStack"
    lblStack <- xmlGetWidget dialogXml castToLabel "lblStack"
    tvStackData <- treeStoreNew [TMstring, TMstring]
    txtCover <- xmlGetWidget dialogXml castToTextView "txtCover"
    txtCoverData <- textViewGetBuffer txtCover
    let hatGui = HatGui
                    wndMain cmdFilename lblFilename
                    tvStack lblStack tvStackData
                    txtCover txtCoverData




-- they should all already be buffered
getTextBox :: Window -> String -> IO TextBox


data TextBox = TextBox {Gtk.TextBox, textVaraible}

instance TextProvider TextBox where
    text (TextBox a b) = b

-}




{-

varFromEntryText :: Entry -> IO (Var String)
varFromEntryText entry = do
    let val = Value (entrySetText entry) (entryGetText entry)
    var <- newVarValue val
    onInsertAtCursor entry (const $ fireNotify var)
    return var
-}