/reactive-banana-wx/src/CRUDIncremental.hs

https://github.com/killerswan/reactive-banana · Haskell · 264 lines · 160 code · 43 blank · 61 comment · 7 complexity · 1ddc65898f9cdc8e0e1a3d2a80684e83 MD5 · raw file

  1. {-----------------------------------------------------------------------------
  2. reactive-banana-wx
  3. Example: ListBox with CRUD operations
  4. ------------------------------------------------------------------------------}
  5. {-# LANGUAGE ScopedTypeVariables #-} -- allows "forall t. NetworkDescription t"
  6. {-# LANGUAGE RecursiveDo #-}
  7. import Control.Monad (join)
  8. import qualified Data.List
  9. import Data.Maybe
  10. import qualified Data.Map as Map
  11. import Graphics.UI.WX as WX hiding (Event)
  12. import Reactive.Banana
  13. import Reactive.Banana.WX
  14. {-----------------------------------------------------------------------------
  15. Main
  16. ------------------------------------------------------------------------------}
  17. main = start $ do
  18. -- GUI layout
  19. f <- frame [ text := "CRUD Example" ]
  20. listBox <- singleListBox f []
  21. create <- button f [ text := "Create" ]
  22. delete <- button f [ text := "Delete" ]
  23. filter <- entry f [ processEnter := True ]
  24. name <- entry f [ processEnter := True ]
  25. surname <- entry f [ processEnter := True ]
  26. let dataItem = grid 10 10 [[label "Name:", widget name]
  27. ,[label "Surname:", widget surname]]
  28. set f [layout := margin 10 $
  29. grid 10 5
  30. [[row 5 [label "Filter prefix:", widget filter], glue]
  31. ,[minsize (sz 200 300) $ widget listBox, dataItem]
  32. ,[row 10 [widget create, widget delete], glue]
  33. ]]
  34. -- event network
  35. let networkDescription :: forall t. NetworkDescription t ()
  36. networkDescription = mdo
  37. -- events from buttons
  38. eDelete <- event0 delete command
  39. eCreate <- event0 create command
  40. -- time-varying value corresponding to the filter string
  41. (bFilter, eFilter) <- reactimateTextEntry filter (pure "")
  42. let dFilter = stepperD "" $ bFilter <@ eFilter
  43. -- list box with selection
  44. dSelectedItem <- reactimateListBox listBox database dFilter
  45. -- data corresponding to the selected item in the list box
  46. (inDataItem, changeDataItem)
  47. <- reactimateDataItem (name, surname) outDataItem
  48. let
  49. -- update the database whenever
  50. -- a data item is created, updated or deleted
  51. database :: DatabaseTime DataItem
  52. database = accumDatabase $
  53. (Create Nothing ("Emil","Example") <$ eCreate)
  54. `union` (Update <$> dSelectedItem <@>
  55. (inDataItem <@ changeDataItem))
  56. `union` (Delete <$> dSelectedItem <@ eDelete )
  57. -- display the data item whenever the selection changes
  58. outDataItem = stepperD ("","") $
  59. lookup <$> valueDB database <@> changes dSelectedItem
  60. where
  61. lookup database m = fromMaybe ("","") $
  62. readDatabase database =<< m
  63. -- automatically enable / disable editing
  64. let dDisplayItem = isJust <$> dSelectedItem
  65. sink delete [ enabled :== dDisplayItem ]
  66. sink name [ enabled :== dDisplayItem ]
  67. sink surname [ enabled :== dDisplayItem ]
  68. network <- compile networkDescription
  69. actuate network
  70. {-----------------------------------------------------------------------------
  71. Database Model
  72. ------------------------------------------------------------------------------}
  73. -- Create/Update/Delete data type for efficient updates
  74. data CUD key a
  75. = Create { getKey :: key, getItem :: a }
  76. | Update { getKey :: key, getItem :: a }
  77. | Delete { getKey :: key }
  78. instance Functor (CUD key) where
  79. fmap f (Delete x) = Delete x
  80. fmap f cud = cud { getItem = f $ getItem cud }
  81. isDelete (Delete _) = True
  82. isDelete _ = False
  83. -- Database type
  84. type DatabaseKey = Int
  85. data Database a = Database { nextKey :: !Int, db :: Map.Map DatabaseKey a }
  86. emptyDatabase = Database 0 Map.empty
  87. -- Time-varying database,
  88. -- similar to the Discrete type
  89. data DatabaseTime a = DatabaseTime
  90. { valueDB :: Behavior (Database a)
  91. , initialDB :: Database a
  92. , changesDB :: Event (CUD DatabaseKey a)
  93. }
  94. -- accumulate a database from CUD operations
  95. accumDatabase :: Event (CUD (Maybe DatabaseKey) a) -> DatabaseTime a
  96. accumDatabase e = DatabaseTime valueDB initialDB changesDB
  97. where
  98. (changesDB, valueDB) = mapAccum initialDB $ acc <$> filterE valid e
  99. initialDB = emptyDatabase
  100. valid (Create Nothing _) = True
  101. valid cud = isJust $ getKey cud
  102. -- accumulation function
  103. acc (Create Nothing x) (Database newkey db)
  104. = (Create newkey x, Database (newkey+1) $ Map.insert newkey x db)
  105. acc (Update (Just key) x) (Database newkey db)
  106. = (Update key x, Database newkey $ Map.insert key x db)
  107. acc (Delete (Just key)) (Database newkey db)
  108. = (Delete key , Database newkey $ Map.delete key db)
  109. -- read a value from the database
  110. readDatabase :: Database a -> DatabaseKey -> Maybe a
  111. readDatabase (Database _ db) = flip Map.lookup db
  112. {-----------------------------------------------------------------------------
  113. Data items that are stored in the data base
  114. ------------------------------------------------------------------------------}
  115. type DataItem = (String, String)
  116. -- text entry widgets in terms of discrete time-varying values
  117. reactimateTextEntry
  118. :: TextCtrl a
  119. -> Discrete String -- set text programmatically (view)
  120. -> NetworkDescription
  121. (Behavior String -- current text (both view & controller)
  122. ,Event ()) -- user changes (controller)
  123. reactimateTextEntry entry input = do
  124. sink entry [ text :== input ]
  125. -- event: Enter key
  126. eEnter <- event0 entry command
  127. -- event: text entry loses focus
  128. eLeave <- (() <$) . filterE not <$> event1 entry focus
  129. b <- behavior entry text
  130. return (b, eEnter `union` eLeave)
  131. -- whole data item (consisting of two text entries)
  132. reactimateDataItem
  133. :: (TextCtrl a, TextCtrl b)
  134. -> Discrete DataItem
  135. -> NetworkDescription
  136. (Behavior DataItem, Event ())
  137. reactimateDataItem (name,surname) input = do
  138. (d1,e1) <- reactimateTextEntry name (fst <$> input)
  139. (d2,e2) <- reactimateTextEntry surname (snd <$> input)
  140. return ( (,) <$> d1 <*> d2 , e1 `union` e2 )
  141. -- custom show function
  142. showDataItem (name, surname) = surname ++ ", " ++ name
  143. {-----------------------------------------------------------------------------
  144. List Box View
  145. ------------------------------------------------------------------------------}
  146. -- Display the data base in a list box (view).
  147. -- Also keep track of the currently selected item (controller).
  148. reactimateListBox
  149. :: SingleListBox b -- list box widget
  150. -> DatabaseTime DataItem -- database
  151. -> Discrete String -- filter string
  152. -> NetworkDescription
  153. (Discrete (Maybe DatabaseKey)) -- current selection as database key
  154. reactimateListBox listBox database filter = do
  155. -- The list box keeps track
  156. -- of which data items are displayed, at which positions
  157. let (eListBoxUpdates, bDisplayMap)
  158. = mapAccum Map.empty
  159. $ (cudUpdate . fmap showDataItem <$> changesDB database)
  160. `union` (filterUpdate <$> valueDB database <@> changes filter)
  161. -- "animate" changes to the list box
  162. reactimate eListBoxUpdates
  163. -- debug: reactimate $ fmap print $ bDisplayMap <@ eListBoxUpdates
  164. -- event: item selection, maps to database key
  165. fixSelectionEvent listBox
  166. bSelection <- behavior listBox selection
  167. eSelect <- event0 listBox select
  168. let eDelete = filterE isDelete $ changesDB database
  169. return $ stepperD Nothing $
  170. -- event: item deleted
  171. (Nothing <$ eDelete) `union`
  172. -- event: filter string changed
  173. (Nothing <$ changes filter) `union`
  174. -- event: user changes selection
  175. (lookupPositon <$> bSelection <*> bDisplayMap <@ eSelect)
  176. where
  177. -- turn CUD into a function that updates
  178. -- ( the graphics of the list box
  179. -- , the map from database keys to list positions )
  180. cudUpdate
  181. :: CUD DatabaseKey String -> DisplayMap -> (IO (), DisplayMap)
  182. cudUpdate (Create key str) display
  183. = (itemAppend listBox str, appendKey key display)
  184. cudUpdate (Update key str) display
  185. = case lookupKey key display of
  186. Just position -> (set listBox [ item position := str ], display)
  187. Nothing -> (return (), display)
  188. cudUpdate (Delete key) display
  189. = case lookupKey key display of
  190. Just position -> (itemDelete listBox position
  191. ,deleteKey key position display)
  192. Nothing -> (return (), display)
  193. -- rebuild listBox when filter string changes
  194. filterUpdate database s _ = (set listBox [ items := xs ], display)
  195. where
  196. dat = Map.filter (s `Data.List.isPrefixOf`)
  197. . Map.map showDataItem . db $ database
  198. xs = Map.elems dat
  199. display = Map.fromList $ zip (Map.keys dat) [0..]
  200. -- Map between database keys and their position in the list box
  201. type DisplayMap = Map.Map DatabaseKey Int
  202. lookupKey = Map.lookup
  203. lookupPositon pos = fmap fst . Data.List.find ((pos ==) . snd) . Map.toList
  204. appendKey key display = Map.insert key (Map.size display) display
  205. deleteKey key position display
  206. = Map.delete key
  207. -- recalculate positions of the other elements
  208. . Map.map (\pos -> if pos > position then pos - 1 else pos)
  209. $ display
  210. {-----------------------------------------------------------------------------
  211. wxHaskell bug fixes
  212. ------------------------------------------------------------------------------}
  213. -- Fix @select@ event not being fired when items are *un*selected
  214. fixSelectionEvent listbox =
  215. liftIO $ set listbox [ on unclick := handler ]
  216. where
  217. handler _ = do
  218. propagateEvent
  219. s <- get listbox selection
  220. when (s == -1) $ join $ get listbox (on select)