Is it possible to clean some code using gtk2hs?

179 Views Asked by At

I'm starting a GUI with haskell and gtk2hs. I've got a notebook widget and I want to switch pages with "F1, F2 ... F11" keys.

My working code is:

import Control.Monad.Trans (liftIO)
import Graphics.UI.Gtk

main = do
  initGUI

  builder <- builderNew
  builderAddFromFile builder "M62.glade"

  window <- builderGetObject builder castToWindow "window1"
  notebook <- builderGetObject builder castToNotebook "notebook1"

  window `on` keyPressEvent $ tryEvent $ do "F1" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 0
  window `on` keyPressEvent $ tryEvent $ do "F2" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 1
  window `on` keyPressEvent $ tryEvent $ do "F3" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 2
  window `on` keyPressEvent $ tryEvent $ do "F4" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 3
  window `on` keyPressEvent $ tryEvent $ do "F5" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 4
  window `on` keyPressEvent $ tryEvent $ do "F6" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 5
  window `on` keyPressEvent $ tryEvent $ do "F7" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 6
  window `on` keyPressEvent $ tryEvent $ do "F8" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 7
  window `on` keyPressEvent $ tryEvent $ do "F9" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 8
  window `on` keyPressEvent $ tryEvent $ do "F10" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 9
  window `on` keyPressEvent $ tryEvent $ do "F11" <- eventKeyName
                                            liftIO $ notebookSetCurrentPage notebook 10

  onDestroy window mainQuit
  widgetShowAll window
  mainGUI

Is there betters and/or concises ways to do it? I've tried to handle it out of the 'main' but then only "F1" works. I don't see how to manage this boilerplate.

3

There are 3 best solutions below

2
On BEST ANSWER

How about this:

window `on` keyPressEvent $ tryEvent $ do
    'F':n_ <- eventKeyName
    let (n, ""):_ = reads n_
    liftIO . notebookSetCurrentPage notebook $ n - 1

This is hopelessly partial: there are two partial pattern matches that can throw an exception. But that's okay, because that's what tryEvent is for. At time of writing, all other answers involve registering many event handlers, whereas this one registers only one. This should have a (slight) performance advantage.

6
On

Try splitting the repeated part into a function, like this:

import Control.Monad
import Graphics.UI.Gtk


main = do
  initGUI

  builder <- builderNew
  builderAddFromFile builder "M62.glade"

  window <- builderGetObject builder castToWindow "window1"
  notebook <- builderGetObject builder castToNotebook "notebook1"

  -- Split the repeated code into a reusable function, like this
  let registerKeyPressEvent n =
    window `on` keyPressEvent $ tryEvent $ do
      pressed <- eventKeyName
      guard (pressed == ("F" ++ show (n + 1)))
      liftIO $ notebookSetCurrentPage notebook n
  -- Thanks to Tarmil for catching a bug in the code that used to be above.
  -- Tarmil got it right, so I'm borrowing his/her version.

  -- Then you can call it more than once
  registerKeyPressEvent  0
  registerKeyPressEvent  1
  registerKeyPressEvent  2
  registerKeyPressEvent  3
  registerKeyPressEvent  4
  registerKeyPressEvent  5
  registerKeyPressEvent  6
  registerKeyPressEvent  7
  registerKeyPressEvent  8
  registerKeyPressEvent  9
  registerKeyPressEvent 10

  -- But even that is too verbose.
  -- You can shorten it even further like this:
  mapM_ registerKeyPressEvent [0..10]

mapM is like map, except for monads. The type of map is:

map :: (a -> b) -> [a] -> [b]

meaning that it takes a function and applies it to every element of a list, returning the result. The type of mapM is:

mapM :: Monad m => (a -> m b) -> [a] -> m [b]

meaning that it takes a monadic function (such as registerKeyPressEvent, function in the IO monad that creates the side effect of registering a key press event). mapM then executes this function once for every element in the list, and not only collects the results into a list, but collects the monadic actions into the resulting monad, meaning that the side effects from running registerKeyPressEvent 11 times are performed in order.

The final piece of the puzzle is that you might get a type error if you use mapM, because it assumes you care about the resulting list, and therefore returns m [b]. However, in this case, the type of main is IO (), and () is not going to match up to [b]. You therefore want a slight variation on mapM that throws away the resulting list, only collecting the monadic actions:

mapM_ :: Monad m => (a -> m b) -> [a] -> m ()

This has the return type you're looking for.

1
On

I don't know much about gtk2hs, but using forM_ to loop over the key indices should go a long way. Also, it seems events are a MonadPlus, so pattern match failure can be favorably replaced with guard.

forM_ [0..10] $ \i -> do
    let key = "F" ++ show (i + 1)
    window `on` keyPressEvent $ tryEvent $ do
        pressed <- eventKeyName
        guard (pressed == key)
        liftIO $ notebookSetCurrentPage notebook i