File: Menu.hs

package info (click to toggle)
haskell-glut 2.1.2.1-1
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 1,936 kB
  • ctags: 25
  • sloc: haskell: 10,092; sh: 2,811; ansic: 53; makefile: 2
file content (310 lines) | stat: -rw-r--r-- 12,330 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
{-# OPTIONS_GHC -fno-cse #-}

--------------------------------------------------------------------------------
-- |
-- Module      :  Graphics.UI.GLUT.Menu
-- Copyright   :  (c) Sven Panne 2002-2005
-- License     :  BSD-style (see the file libraries/GLUT/LICENSE)
--
-- Maintainer  :  sven.panne@aedion.de
-- Stability   :  stable
-- Portability :  portable
--
-- GLUT supports simple cascading pop-up menus. They are designed to let a user
-- select various modes within a program. The functionality is simple and
-- minimalistic and is meant to be that way. Do not mistake GLUT\'s pop-up menu
-- facility with an attempt to create a full-featured user interface.
--
--------------------------------------------------------------------------------

module Graphics.UI.GLUT.Menu (
   Menu(..), MenuItem(..), MenuCallback, attachMenu,
   numMenuItems
) where

import Data.Array ( listArray, (!) )
import Data.IORef ( IORef, newIORef, readIORef, modifyIORef )
import qualified Data.Map as Map ( empty, lookup, insert, delete )
import Data.Map ( Map )
import Foreign.C.String ( CString, withCString )
import Foreign.C.Types ( CInt )
import Foreign.Ptr ( FunPtr, freeHaskellFunPtr )
import Control.Monad ( unless, zipWithM, when )
import System.IO.Unsafe ( unsafePerformIO )
import Graphics.Rendering.OpenGL.GL.StateVar (
   HasGetter(get), HasSetter(($=)),
   GettableStateVar, makeGettableStateVar,
   StateVar, makeStateVar )
import Graphics.UI.GLUT.Constants ( glut_MENU_NUM_ITEMS )
import Graphics.UI.GLUT.QueryUtils ( simpleGet )
import Graphics.UI.GLUT.Types ( marshalMouseButton )
import Graphics.UI.GLUT.Window ( Window )
import Graphics.UI.GLUT.Callbacks.Registration ( getCurrentWindow )
import Graphics.UI.GLUT.Callbacks.Window ( MouseButton )

--------------------------------------------------------------------------------

-- | A menu is simply a list of menu items.
newtype Menu = Menu [MenuItem]

-- | A single item within a menu can either be a plain menu entry or a sub-menu
-- entry, allowing for arbitrarily deep nested menus.
data MenuItem
   = MenuEntry String MenuCallback -- ^ A plain menu entry with an associated
                                   --   callback, which is triggered when the
                                   --   user selects the entry
   | SubMenu   String Menu         -- ^ A sub-menu, which is cascaded when the
                                   --   user selects the entry, allowing
                                   --   sub-menu entries to be selected

type MenuCallback = IO ()

-- | Create a new pop-up menu for the /current window,/ attaching it to the
-- given mouse button. A previously attached menu (if any), is detached before
-- and won\'t receive callbacks anymore.
--
-- It is illegal to call 'attachMenu' while any (sub-)menu is in use, i.e.
-- popped up.
--
-- /X Implementation Notes:/ If available, GLUT for X will take advantage of
-- overlay planes for implementing pop-up menus. The use of overlay planes can
-- eliminate display callbacks when pop-up menus are deactivated. The
-- @SERVER_OVERLAY_VISUALS@ convention is used to determine if overlay visuals
-- are available.

attachMenu :: MouseButton -> Menu -> IO ()
attachMenu mouseButton menu@(Menu items) = do
   win <- getCurrentWindow "attachMenu"
   let hook = MenuHook win mouseButton
   detachMenu hook
   unless (null items) $ do
      (_, destructor) <- traverseMenu menu
      addToMenuTable hook destructor
      attachMenu_ mouseButton

detachMenu :: MenuHook -> IO ()
detachMenu hook@(MenuHook _ mouseButton) = do
   maybeDestructor <- lookupInMenuTable hook
   case maybeDestructor of
      Nothing         -> return ()
      Just destructor -> do detachMenu_ mouseButton
                            destructor
   deleteFromMenuTable hook

traverseMenu :: Menu -> IO (MenuID, Destructor)
traverseMenu (Menu items) = do
   let callbackArray = listArray (1, length items) (map makeCallback items)
   cb <- makeMenuFunc (\i -> callbackArray ! (fromIntegral i))
   menuID <- glutCreateMenu cb
   destructors <- zipWithM addMenuItem items [1..]
   let destructor = do sequence_ destructors
                       glutDestroyMenu menuID
                       freeHaskellFunPtr cb
   return (menuID, destructor)

makeCallback :: MenuItem -> MenuCallback
makeCallback (MenuEntry _ cb) = cb
makeCallback _ = error "shouldn't receive a callback for submenus"

addMenuItem :: MenuItem -> Value -> IO Destructor
addMenuItem (MenuEntry s _) v = do
   addMenuEntry s v
   return $ glutRemoveMenuItem 1
addMenuItem (SubMenu s m) _ = do
   (menuID, destructor) <- saveExcursion (traverseMenu m)
   addSubMenu s menuID
   return $ do glutRemoveMenuItem 1
               destructor

-- Perform an action, saving/restoring the current menu around it
saveExcursion :: IO a -> IO a
saveExcursion act = do
   menuID <- get currentMenu
   returnValue <- act
   when (isRealMenu menuID) $
      currentMenu $= menuID
   return returnValue

--------------------------------------------------------------------------------
-- This seems to be a common Haskell hack nowadays: A plain old global variable
-- with an associated mutator. Perhaps some language/library support is needed?

{-# NOINLINE theMenuTable #-}
theMenuTable :: IORef MenuTable
theMenuTable = unsafePerformIO (newIORef emptyMenuTable)

getMenuTable :: IO MenuTable
getMenuTable = readIORef theMenuTable

modifyMenuTable :: (MenuTable -> MenuTable) -> IO ()
modifyMenuTable = modifyIORef theMenuTable

--------------------------------------------------------------------------------
-- To facilitate cleanup, we have to keep track how to destroy menus which are
-- currently attached in a window to a mouse button.

data MenuHook = MenuHook Window MouseButton
   deriving ( Eq, Ord )

type Destructor = IO ()

type MenuTable = Map MenuHook Destructor

emptyMenuTable :: MenuTable
emptyMenuTable = Map.empty

lookupInMenuTable :: MenuHook -> IO (Maybe Destructor)
lookupInMenuTable callbackID =
   fmap (Map.lookup callbackID) getMenuTable

deleteFromMenuTable :: MenuHook -> IO ()
deleteFromMenuTable callbackID =
   modifyMenuTable (Map.delete callbackID)

addToMenuTable :: MenuHook -> Destructor -> IO ()
addToMenuTable callbackID funPtr =
   modifyMenuTable (Map.insert callbackID funPtr)

--------------------------------------------------------------------------------

type MenuID = CInt
type Value  = CInt
type Item   = CInt

--------------------------------------------------------------------------------

-- | The type of a menu callback action that is called when a menu entry from a
-- menu is selected. The value passed to the callback is determined by the value
-- for the selected menu entry.

type MenuCB = CInt -> IO ()

-- | Create a new pop-up menu and return a unique identifier for it, which can
-- be used when setting 'currentMenu'. Implicitly, the /current menu/ is set to
-- the newly created menu.
--
-- When the menu callback is called because a menu entry is selected for the
-- menu, the /current menu/ will be implicitly set to the menu with the selected
-- entry before the callback is made.
--
-- /X Implementation Notes:/ If available, GLUT for X will take advantage of
-- overlay planes for implementing pop-up menus. The use of overlay planes can
-- eliminate display callbacks when pop-up menus are deactivated. The
-- @SERVER_OVERLAY_VISUALS@ convention is used to determine if overlay visuals
-- are available.

foreign import CALLCONV unsafe "glutCreateMenu" glutCreateMenu ::
   FunPtr MenuCB -> IO MenuID

foreign import ccall "wrapper" makeMenuFunc :: MenuCB -> IO (FunPtr MenuCB)

-- | Destroy the specified menu. If it was the /current menu/, the /current
-- menu/ becomes invalid and 'currentMenu' will contain 'Nothing'.

foreign import CALLCONV unsafe "glutDestroyMenu" glutDestroyMenu ::
   MenuID -> IO ()

--------------------------------------------------------------------------------

-- | Controls the /current menu./ If no menus exist or the previous /current
-- menu/ was destroyed, a pseudo menu is returned, see 'isRealMenu'.

currentMenu :: StateVar MenuID
currentMenu = makeStateVar glutGetMenu glutSetMenu

foreign import CALLCONV unsafe "glutSetMenu" glutSetMenu :: MenuID -> IO ()

foreign import CALLCONV unsafe "glutGetMenu" glutGetMenu :: IO MenuID

-- | Returns 'True' if the given menu identifier refers to a real menu, not
-- a pseudo one.

isRealMenu :: MenuID -> Bool
isRealMenu = (/= 0)

--------------------------------------------------------------------------------

-- | Add a menu entry to the bottom of the /current menu./ The given string will
-- be displayed for the newly added menu entry. If the menu entry is selected by
-- the user, the menu\'s callback will be called passing the given value as the
-- callback\'s parameter.

addMenuEntry :: String -> Value -> IO ()
addMenuEntry name value = withCString name $ \n -> glutAddMenuEntry n value

foreign import CALLCONV unsafe "glutAddMenuEntry" glutAddMenuEntry ::
   CString -> Value -> IO ()

-- | Add a sub-menu trigger to the bottom of the /current menu./ The given
-- string will be displayed for the newly added sub-menu trigger. If the
-- sub-menu trigger is entered, the sub-menu specified by the given menu
-- identifier will be cascaded, allowing sub-menu menu items to be selected.

addSubMenu :: String -> MenuID -> IO ()
addSubMenu name menuID = withCString name $ \n -> glutAddSubMenu n menuID

foreign import CALLCONV unsafe "glutAddSubMenu" glutAddSubMenu ::
   CString -> MenuID -> IO ()

--------------------------------------------------------------------------------

{- UNUSED
-- | Change the specified menu entry in the /current menu/ into a menu entry.
-- The given position determines which menu item should be changed and must be
-- between 1 (the topmost menu item) and
-- 'Graphics.UI.GLUT.State.getNumMenuItems' inclusive. The menu item to change
-- does not have to be a menu entry already. The given string will be displayed
-- for the newly changed menu entry. The given value will be returned to the
-- menu\'s callback if this menu entry is selected.

foreign import CALLCONV unsafe "glutChangeToMenuEntry" glutChangeToMenuEntry ::
   Item -> CString -> Value -> IO ()

-- | Change the specified menu item in the /current menu/ into a sub-menu
-- trigger. The  given position determines which menu item should be changed and
-- must be between 1 and 'Graphics.UI.GLUT.State.getNumMenuItems' inclusive. The
-- menu item to change does not have to be a sub-menu trigger already. The
-- given name will be displayed for the newly changed sub-menu trigger. The
-- given menu identifier names the sub-menu to cascade from the newly added
-- sub-menu trigger.

foreign import CALLCONV unsafe "glutChangeToSubMenu" glutChangeToSubMenu ::
   Item -> CString -> MenuID -> IO ()
-}

--------------------------------------------------------------------------------

-- | Remove the menu item at the given position, regardless of whether it is a
-- menu entry or sub-menu trigger. The position must be between 1 (the topmost
-- menu item) and 'Graphics.UI.GLUT.State.getNumMenuItems' inclusive. Menu items
-- below the removed menu item are renumbered.

foreign import CALLCONV unsafe "glutRemoveMenuItem" glutRemoveMenuItem ::
   Item -> IO ()

--------------------------------------------------------------------------------

-- | Attach a mouse button for the /current window/ to the identifier of the
-- /current menu./ By attaching a menu identifier to a button, the named menu
-- will be popped up when the user presses the specified button. Note that the
-- menu is attached to the button by identifier, not by reference.


attachMenu_ :: MouseButton -> IO ()
attachMenu_ = glutAttachMenu . marshalMouseButton

foreign import CALLCONV unsafe "glutAttachMenu" glutAttachMenu :: CInt -> IO ()

-- | Detach an attached mouse button from the /current window./

detachMenu_ :: MouseButton -> IO ()
detachMenu_ = glutDetachMenu . marshalMouseButton

foreign import CALLCONV unsafe "glutDetachMenu" glutDetachMenu :: CInt -> IO ()

--------------------------------------------------------------------------------

-- | Contains the number of menu items in the /current menu./

numMenuItems :: GettableStateVar Int
numMenuItems = makeGettableStateVar $ simpleGet fromIntegral glut_MENU_NUM_ITEMS