I have written a small application which tracks my progress in TV Series. The application is written in Haskell with functional reactive programming (FRP) with reactive banana.
The application can:
add/remove new TV Series to the table
change the season and episode of an series
I have problems writing the code that adds a new TV series to the table and wires the new events. The CRUD example from here didn't quite help me because I have more requirements then just selecting an element from the list.
How do I write a reactiveTable function like the reactiveListDisplay function from the CRUD Example in a FRP way? How can events be added for the remove button and the season and episode spin buttons after the network has been compiled?
data Series = Series { name :: String
, season :: Int
, episode :: Int
}
insertIntoTable :: TableClass t => t -> SeriesChangeHandler -> SeriesRemoveHandler -> Series -> IO ()
insertIntoTable table changeHandler removeHandler (Series name s e) = do
(rows, cols) <- tableGetSize table
tableResize table (rows+1) cols
nameLabel <- labelNew $ Just name
adjustmentS <- adjustmentNew (fromIntegral s) 1 1000 1 0 0
adjustmentE <- adjustmentNew (fromIntegral e) 1 1000 1 0 0
seasonButton <- spinButtonNew adjustmentS 1.0 0
episodeButton <- spinButtonNew adjustmentE 1.0 0
removeButton <- buttonNewWithLabel "remove"
let getSeries = do
s <- spinButtonGetValue seasonButton
e <- spinButtonGetValue episodeButton
return $ Series name (round s) (round e)
handleSeries onEvent widget handler = do
onEvent widget $ do
series <- getSeries
handler series
handleSeries onValueSpinned seasonButton changeHandler
handleSeries onValueSpinned episodeButton changeHandler
onPressed removeButton $ do
series <- getSeries
containerRemove table nameLabel
containerRemove table seasonButton
containerRemove table episodeButton
containerRemove table removeButton
removeHandler series
let tadd widget x = tableAdd table widget x (rows - 1)
tadd nameLabel 0
tadd seasonButton 1
tadd episodeButton 2
tadd removeButton 3
widgetShowAll table
main :: IO ()
main = do
initGUI
window <- windowNew
scroll <- scrolledWindowNew Nothing Nothing
table <- tableNew 1 5 True
addButton <- buttonNewWithLabel "add series"
vbox <- vBoxNew False 10
containerAdd window vbox
boxPackStart vbox addButton PackNatural 0
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
addEvent <- eventButton addButton
(changeHandler,fireChange) <- liftIO $ newAddHandler
changeEvent <- fromAddHandler changeHandler
(removeHandler,fireRemove) <- liftIO $ newAddHandler
removeEvent <- fromAddHandler removeHandler
let insertIntoTable' = insertIntoTable table fireChange fireRemove
addSeries e = do
s <- addSeriesDialog
liftIO $ insertIntoTable' s
liftIO $ mapM_ insertIntoTable' initSeries
reactimate $ addSeries <$> addEvent
reactimate $ updateSeries conn <$> changeEvent
reactimate $ removeSeries conn <$> removeEvent
network <- compile networkDescription
actuate network
onDestroy window $ do
D.disconnect conn
mainQuit
widgetShowAll window
mainGUI
I want to refactor the insertIntoTable method to use events and behaviors rather than using simple callbacks.
EDIT:
I have tried the gtk TreeView with a ListStore backend. In this scenario you don't need dynamic event switching. I have written the reactiveList function below to get a list behavior out of insert, change and remove events. It works ^^
reactiveList :: (Frameworks t)
=> ListStore a
-> Event t (Int,a) -- insert event
-> Event t (Int,a) -- change event
-> Event t (Int,a) -- remove event
-> Moment t (Behavior t [a])
reactiveList store insertE changeE removeE = do
(listHandler,fireList) <- liftIO $ newAddHandler
let onChange f (i,a) = do
f i a
list <- listStoreToList store
fireList list
reactimate $ onChange (listStoreInsert store) <$> insertE
reactimate $ onChange (listStoreSetValue store) <$> changeE
reactimate $ onChange (const . listStoreRemove store) <$> removeE
initList <- liftIO $ listStoreToList store
fromChanges initList listHandler
main :: IO ()
main = do
initGUI
window <- windowNew
addButton <- buttonNewWithLabel "add series"
vbox <- vBoxNew False 10
seriesList <- listStoreNew (initSeries :: [Series])
listView <- treeViewNewWithModel seriesList
treeViewSetHeadersVisible listView True
let newCol title newRenderer f = do
col <- treeViewColumnNew
treeViewColumnSetTitle col title
renderer <- newRenderer
cellLayoutPackStart col renderer False
cellLayoutSetAttributes col renderer seriesList f
treeViewAppendColumn listView col
return renderer
newCol "Image" cellRendererPixbufNew $ \s -> [cellPixbuf :=> newPixbuf s]
newCol "Name" cellRendererTextNew $ \s -> [cellText := name s]
seasonSpin <- newCol "Season" cellRendererSpinNew $ \s ->
[ cellRendererSpinAdjustment :=> adjustmentNew (fromIntegral (season s)) 1 1000 1 0 0
, cellText := (show $ season s)
, cellTextEditable := True
]
episodeSpin <- newCol "Episode" cellRendererSpinNew $ \s ->
[ cellRendererSpinAdjustment :=> adjustmentNew (fromIntegral (episode s)) 1 1000 1 0 0
, cellText := (show $ episode s)
, cellTextEditable := True
]
containerAdd window vbox
boxPackStart vbox listView PackGrow 0
boxPackStart vbox addButton PackNatural 0
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
(addHandler,fireAdd) <- liftIO $ newAddHandler
maybeSeriesE <- fromAddHandler addHandler
(removeHandler,fireRemove) <- liftIO $ newAddHandler
removeE <- fromAddHandler removeHandler
-- when the add button was pressed,
-- open a dialog and return maybe a new series
askSeriesE <- eventButton addButton
reactimate $ (const $ fireAdd =<< askSeries) <$> askSeriesE
-- ommit all nothing series
let insertE = filterJust maybeSeriesE
insert0E = ((,) 0) <$> insertE
seasonSpinE <- eventSpin seasonSpin seriesList
episodeSpinE <- eventSpin episodeSpin seriesList
let changeSeason (i,d,s) = (i,s {season = round d})
changeEpisode (i,d,s) = (i,s {episode = round d})
let changeE = (changeSeason <$> seasonSpinE) `union` (changeEpisode <$> episodeSpinE)
listB <- reactiveList seriesList insert0E changeE removeE
listE <- (changes listB)
reactimate $ (putStrLn . unlines . map show) <$> listE
reactimate $ insertSeries conn <$> insertE
reactimate $ updateSeries conn . snd <$> changeE
reactimate $ removeSeries conn . snd <$> removeE
return ()
network <- compile networkDescription
actuate network
onDestroy window $ do
D.disconnect conn
mainQuit
widgetShowAll window
mainGUI
I'm open for comments and suggestions.
It sounds like your problem is much closer to the Bar Tab example than the CRUD one.
The basic idea for adding new widgets--along with new behaviors and events--is to use so-called "dynamic event switching". Essentially, this is a way to put newly created events and behaviors back into your network.
The action to create a new widget has two parts. The first part is to just create the widget, using liftIO. The second is to get its inputs and use trimE or trimB as appropriate. Leaving out most of the GTk-specific details (I don't know how to use GTk :P), it'll look something like this:
let newSeries name = do
label <- liftIO . labelNew $ Just name
liftIO $ tadd labelNew 0
{- ... the rest of your controls here ... -}
seasonNumber <- trimB $ getSpinButtonBehavior seasonButton
{- ... wrap the rest of the inputs using trimB and trimE ... -}
return (label, seasonNumber, ...)
So this function creates a new widget, "trims" its inputs and returns the values to you. Now you have to actually use these values:
newSeasons <- execute (FrameworkMoment newSeries <$> nameEvents)
here nameEvents should be an Event String containing an event with the name of the new series each time you want to add it.
Now that you have a stream of all of the new seasons, you can combine it all into a single behavior of a list of entries using something like stepper.
For more details--including things like getting the aggregate information out of all of your widgets--look at the actual example code.
Related
I'm trying to write a simple UI in gtk2hs. It starts with two text boxes and a button. When the button is pressed, it makes a table of buttons of size (m,n) where m and n are taken from the text boxes. For some reason, when the button is pressed, space is allocated for the table but none of the buttons are shown!
import Graphics.UI.Gtk
import Control.Concurrent
t2l :: Int -> Int -> Int -> Int -> Int
t2l r c rr cc = (r * cc) + c
buildTable :: Int -> Int -> IO(Table, [Button])
buildTable r c = do
t <- tableNew r c True
buttons <- sequence $ take (r * c) (repeat buttonNew)
mapM (`set` [buttonLabel := "HELLO"]) buttons
return [tableAttachDefaults t (buttons !! (t2l rr cc r c)) cc (cc+1) rr (rr+1) | cc <- [0..(c+1)] , rr <- [0..(r+1)]]
return (t,buttons)
main = do
initGUI
window <- windowNew
mainSplit <- vBoxNew False 10
contPannel <- hBoxNew False 5
rowTF <- entryNew
colTF <- entryNew
buildBTN <- buttonNew
set buildBTN [buttonLabel := "Build Table"]
set window [containerChild := mainSplit]
boxPackStart mainSplit contPannel PackGrow 0
boxPackStart contPannel rowTF PackGrow 0
boxPackStart contPannel colTF PackGrow 0
boxPackStart contPannel buildBTN PackNatural 0
on window objectDestroy mainQuit
widgetShowAll window
on buildBTN buttonActivated $ do
rT <- get rowTF entryText
cT <- get colTF entryText
r <- return $ read rT
c <- return $ read cT
(t,b) <- buildTable r c
boxPackStart mainSplit t PackGrow 0
widgetShowAll t
return ()
mainGUI
I'm not 100% sure why the error arose, I have some new code which works:
import Graphics.UI.Gtk
import Control.Concurrent
mkBtn :: String -> IO Button
mkBtn label = buttonNew >>= (\b -> set b [buttonLabel := label] >> return b)
buildTable :: Int -> Int -> IO(Grid)
buildTable r c = do
t <- gridNew
gridSetRowHomogeneous t True
mapM (\f -> mkBtn "Hello" >>= (\b -> gridAttach t b (f `mod` c) (f `div` c) 1 1)) [0..(r*c)-1]
return (t)
main = do
initGUI
window <- windowNew
mainSplit <- vBoxNew False 10
contPannel <- hBoxNew False 5
rowTF <- entryNew
colTF <- entryNew
buildBTN <- buttonNew
set buildBTN [buttonLabel := "Build Table"]
set window [containerChild := mainSplit]
boxPackStart mainSplit contPannel PackGrow 0
boxPackStart contPannel rowTF PackGrow 0
boxPackStart contPannel colTF PackGrow 0
boxPackStart contPannel buildBTN PackNatural 0
on window objectDestroy mainQuit
widgetShowAll window
on buildBTN buttonActivated $ do
rT <- get rowTF entryText
cT <- get colTF entryText
r <- return $ read rT
c <- return $ read cT
t <- buildTable r c
boxPackStart mainSplit t PackGrow 0
widgetShowAll t
return ()
mainGUI
Maybe someone will know why this works and the last didn't? I assume it was how I was creating new buttons.
First thing I changed was from gtk2 to 3, this enabled me to use grid instead of table. Instead of using repeat, I used a helper function mkBtn. The other changes are just how I went about populating the grid. Instead of a rather silly list comprehension, I used mapM and converted the index in the list of buttons to table coords instead of table coords to list index (originally done in t2l)
Duplicating this from github as per #HeinrichApfelmus's suggestion:
This may be just a usage error on my part, but I am noticing a strange phenomenon when trying to set up conditional visibility/layout for dynamically created UI elements (in WX of course). As somewhat of a toy-example, I tried to create a widget that created StaticText elements on the fly and allowed the user to "browse" through these elements through '<' '>' buttons.
The problem I am noting is that all labels are invisible until a new one is created, at which point the current widget in focus becomes visible. Whether this is a bug or just a paradigm I am misusing, or a subtlety with reactive frameworks, I am unsure as to how to resolve this. Here is the code I have at this point, which exhibits the problem:
{-# LANGUAGE RecursiveDo #-}
module Test.Adder where
import Reactive.Banana
import Reactive.Banana.WX
import Graphics.UI.WX.Attributes
import Graphics.UI.WX hiding (Event, newEvent, empty, Identity)
import Graphics.UI.WXCore hiding (Event, Timer, empty, Identity, newEvent)
import Graphics.UI.WXCore.Frame
-- | Combine Unit-Events
anyEvent :: [Event ()] -> Event ()
anyEvent = foldl1 (unionWith (\_ _ -> ()))
-- | Unsugared if-then-else function
if_ :: Bool -> a -> a -> a
if_ True x _ = x
if_ False _ y = y
-- | Apply a function to the value at an index, or return a default value
-- if the index is out of range
(!?) :: (a -> b) -> b -> Int -> ([a] -> b)
(f!? ~y) n xs
| n < 0 = y
| otherwise = case drop n xs of
x:_ -> f x
[] -> y
main :: IO ()
main = start test
create :: Window w -> Int -> Behavior Int -> Event Int -> Event () -> MomentIO (StaticText ())
create t i bi ei eRef = do
let tx = replicate i '\t' ++ show i
x <- liftIO $ staticText t [ text := tx ]
let beq = (==i) <$> bi
let eMe = filterE (==i) ei
sink x [ visible :== beq ]
reactimate (refresh x <$ anyEvent [ eRef, () <$ eMe ])
return x
test :: IO ()
test = do
f <- frame [text := "Test"]
add <- button f [ text := "+" ]
prv <- button f [ text := "<" ]
cur <- staticText f []
nxt <- button f [ text := ">" ]
tab <- panel f [ clientSize := sz 200 300 ]
deb <- staticText f []
ref <- button f [ text := "refresh" ]
let networkDescription :: MomentIO ()
networkDescription = mdo
eAdd <- event0 add command
eRef <- event0 ref command
let bNotFirst = (>0) <$> bCur
bNotLast = (<) <$> bCur <*> bNext
sink prv [ enabled :== bNotFirst ]
sink cur [ text :== show <$> bCur ]
sink nxt [ enabled :== bNotLast ]
ePrev <- event0 prv command
eNext <- event0 nxt command
let eDelta :: Enum n => Event (n -> n)
eDelta = unions [ pred <$ whenE bNotFirst ePrev
, succ <$ whenE bNotLast eNext ]
eChange = flip ($) <$> bCur <#> eDelta
bCur <- stepper 0 $ eChange
(eIndex, bCount) <- mapAccum 0 ((\x -> (x, succ x)) <$ eAdd)
let bView = (\n i -> if_ (n==0) (0) i) <$> bCount <*> bCur
bNext = pred <$> bCount
eCreate = (\n -> create tab n bView eChange $ anyEvent [eRef,eAdd]) <$> eIndex
reCreate <- execute eCreate
bItemer <- accumB id $ flip (.) . (:) <$> reCreate
let bItems = ($[]) <$> bItemer
bThis = (widget!?(nullLayouts!!0)) <$> bCur <*> bItems
sink tab [ layout :== bThis ]
liftIO $ set f [ layout := column 5 [ margin 10 $ row 5 [ widget add
, widget prv
, widget cur
, widget nxt
, widget ref
]
, fill $ widget tab
]
]
network <- compile networkDescription
actuate network
>
I'm trying to make a Timer in Haskell using gtk2hs.
I found an example on this website wiki.haskell Tutorial Threaded GUI
which I could successfully implement in my project. The only problem I'm facing is creating a restart button for the timer.
My goal is that when people pres the "New game" button, that a new game starts and that the timer resets.
If a want to just restart a game I can use this line of code
onClicked button1 (startNewGame table window)
, which works. The problem is I can't find a way to bind a the start timer function to a button.
I tried doing this:
onClicked button1 (do (startTimer box) (startNewGame table window))
Which does not work, also this does not work:
onClicked button1 (startTimer box)
How am I suppose to restart a thread correctly?
When I run this code:
onClicked button1 (startTimer box)
I get this error:
gui.hs:29:25:
Couldn't match type `ThreadId' with `()'
Expected type: IO ()
Actual type: IO ThreadId
In the return type of a call of `startTimer'
In the second argument of `onClicked', namely `(startTimer box)'
In a stmt of a 'do' block: onClicked button1 (startTimer box)
How can I bind the (startTimer box) function to a button?
Source code:
import Graphics.UI.Gtk
import SetTest
import qualified Data.Set as Set
import qualified Data.Map.Strict as Map
import Control.Monad.Trans(liftIO)
import Debug.Trace
import Control.Concurrent
import Control.Concurrent.MVar
import System.Exit
main :: IO ()
main = do
initGUI
window <- windowNew
set window [windowTitle := "Minesweeper",
windowDefaultWidth := 450, windowDefaultHeight := 200]
box <- vBoxNew False 0
containerAdd window box
button1 <- buttonNewWithLabel "New game"
boxPackStart box button1 PackGrow 0
widgetShowAll window
table <- tableNew 5 5 True
--onClicked button1 (do (startTimer box) (startNewGame table window))
--onClicked button1 (startTimer box)
onClicked button1 (startNewGame table window)
startTimer box
containerAdd window table
startNewGame table window
boxPackStart box table PackNatural 0
widgetShowAll window
onDestroy window mainQuit
mainGUI
startTimer :: BoxClass self => self -> IO ThreadId
startTimer box = do
timeLabel <- labelNew Nothing
boxPackStart box timeLabel PackNatural 0
forkIO $ do
let
printTime t = do{
threadDelay 1000000;
postGUIAsync $ labelSetText timeLabel (show t);
printTime (t+1)}
printTime 0
startNewGame:: (WidgetClass self, TableClass self1) => self1 -> self -> IO ()
startNewGame table window = let board = (SetTest.initialize 5 (5,5) (1,1)) :: MyBoard
in checkStatusGame table board window
:: (WidgetClass self, TableClass self1) =>
self1 -> MyBoard -> self -> IO ()
checkStatusGame table board window
| won board = do
cleanAndGenerateTable board table window True
(dialogMessage "hurray hurray hurray" "Gratz, you won!!!")
| lost board = do
(dialogMessage "Baby rage window" "Soz, you lost...")
cleanAndGenerateTable board table window True
| otherwise = (cleanAndGenerateTable board table window False)
cleanAndGenerateTable :: (WidgetClass self, TableClass self1) =>
MyBoard -> self1 -> self -> Bool -> IO ()
cleanAndGenerateTable board table window finished = do
let fieldList = [(x,y) | x <- [0 .. (height board)] , y <- [0 .. (width board)] ]
children <- containerGetChildren table
mapM_ (\child -> containerRemove table child >> widgetDestroy child) children
if finished
then mapM_(generateTableFinished board table window) fieldList
else mapM_ (generateTable board table window) fieldList
widgetShowAll window
generateTable board table window (x,y)
| Set.member (x,y) (flaggedCells board) = createButton "flag.jpg" (x,y) table board window
| Map.member (x,y) (clickedCells board) = createClickedButton (show (Map.findWithDefault (-1) (x,y) (clickedCells board))) (x,y) table
| otherwise = createButton "masked.png" (x,y) table board window
generateTableFinished board table window (x,y)
| Set.member (x,y) (bombs board) = createButtonNoAction "bomb.jpg" (x,y) table board window
| Map.member (x,y) (clickedCells board) = createClickedButton (show (Map.findWithDefault (-1) (x,y) (clickedCells board))) (x,y) table
| otherwise = createClickedButton (show (Map.findWithDefault (-1) (x,y) (maskedCells board))) (x,y) table
createButtonNoAction pth (x,y) table board window = do
button <- buttonNew
box <- hBoxNew False 0
image <- imageNewFromFile pth
boxPackStart box image PackRepel 0
containerAdd button box
tableAttachDefaults table button x (x+1) y (y+1)
createClickedButton lbl (x,y) table = do
button <- buttonNew
box <- hBoxNew False 0
label <- labelNew (Just lbl)
boxPackStart box label PackRepel 0
containerAdd button box
tableAttachDefaults table button x (x+1) y (y+1)
createButton pth (x,y) table board window = do
button <- buttonNew
box <- hBoxNew False 0
image <- imageNewFromFile pth
boxPackStart box image PackRepel 0
containerAdd button box
tableAttachDefaults table button x (x+1) y (y+1)
on button buttonReleaseEvent $ do
click <- eventButton
liftIO $ case click of { LeftButton -> (checkStatusGame table (SetTest.click (x,y) board) window); RightButton -> (checkStatusGame table (SetTest.flag (x,y) board) window) }
return False
return ()
dialogMessage title msg = do dialog <- messageDialogNew Nothing [] MessageOther ButtonsOk msg
set dialog [windowTitle := title]
widgetShowAll dialog
dialogRun dialog
widgetDestroy dialog
If you want to communicate with your timer thread, you will need to hand it a communication channel. An MVar seems appropriate here.
startTimer :: BoxClass self => self -> MVar Integer -> IO ThreadId
startTimer box timer = do
timeLabel <- labelNew Nothing
boxPackStart box timeLabel PackNatural 0
forkIO . forever $ do
threadDelay 1000000
t <- takeMVar timer
putMVar timer (t+1)
postGUIAsync $ labelSetText timeLabel (show t)
At the top of main, you can now create a fresh MVar with timer <- newMVar 0, and pass this to startTimer. In your button callback, you can takeMVar timer >> putMVar timer 0 to reset the timer.
I have a little example using IORef in threepenny-gui (TPG):
testIORef :: IORef String -> Window -> UI ()
testIORef ref window = void $ do
return window # set title "Test IORef"
inCell <- UI.input
outCell <- UI.input
-- When value changes write to IORef
on UI.valueChange inCell $ \_ -> do
inValue <- get value inCell
liftIO $ writeIORef ref inValue
-- function that reads the IORef and sets the value of an element with it
let setValue oc = do
newVal <- liftIO $ readIORef ref
element oc # set value newVal
-- When enter is pressed update the value of the output
on UI.keydown inCell $ \c -> if (c==13) then setValue outCell else return outCell
getBody window #+ [ column [ grid [[string "In cell :", element inCell]
,[string "Out cell:", element outCell]
]
, string "Cells should update while typing."
]
]
I am trying to change this to use the Reactive stuff in TPG. I have got as far as making the Behaviors from the events valueChange and keyDown:
inValue <- stepper "0" $ UI.valueChange inCell
inEnter <- stepper "0" $ fmap show $ filterE (\kc-> kc==13) $ UI.keydown inCell
But I am stuck on how to use these Behaviors to save/get the value to/from the IORef. The problem is that the IORef calls will be in the UI monad, so if I use them the Behaviour will be Behavior (UI a), but then they won't work with sink. I know in this case I needn't use an IORef (eg. the currency conversion example) - but in my actual case I do.
EDIT:
I tried writing my own attribute:
valueUI :: ReadWriteAttr Element (UI String) String
so that I can set an attribute with a Behavior (UI String):
inEnter <- stepper "0" $ fmap show $ filterE (\kc-> kc==13) $ UI.keydown inCell
let getValue = fmap (const $ liftIO $ readIORef ref) inEnter
element outCell # sink valueUI getValue
The code compiles but doesn't work.
I would like to do something like this:
handlerOn = do
cid <- canvas `on` buttonPressEvent $ tryEvent do
signalDisconnect cid
handlerOff
putStrLn "handlerOn"
handlerOff = do
cid <- canvas `on` buttonPressEvent $ tryEvent do
signalDisconnect cid
handlerOn
putStrLn "handlerOff"
This won't work as it is, of course, because I'm trying to use cid inside a code block where cid is not assigned yet.
The idea is to register an event listener that when it receives an event, it will deregister itself and register a different event listener which will do the same, back and forth.
GHC supports recursive do.
handlerOn = do
rec cid <- canvas `on` buttonPressEvent $ tryEvent do
signalDisconnect cid
handlerOff
putStrLn "handlerOn"
You could also use Control.Monad.Fix.
handlerOff = do
mfix $ \cid -> canvas `on` buttonPressEvent $ tryEvent do
signalDisconnect cid
handlerOn
putStrLn "handlerOff"
Or manage the handler yourself.
do ...
h <- newIORef undefined
let handlerOn = do
...
writeIORef h handlerOff
handlerOff = do
...
writeIORef h handlerOn
writeIORef h handlerOn
canvas `on` buttonPressEvent $ tryEvent =<< readIORef h
Or just make everything into a single handler.
do ...
ms <- newIORef False
canvas `on` buttonPressEvent $ tryEvent do
s <- readIORef ms
if s
then ...
else ...