packages feed

brick-3.0: programs/MenuKeybindingsDemo.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Lens.Micro ((^.))
import Lens.Micro.TH (makeLenses)
import Lens.Micro.Mtl
import Control.Monad (void, forM_)
import Control.Monad.Trans (liftIO)
#if !(MIN_VERSION_base(4,11,0))
import Data.Monoid ((<>))
#endif
import Data.Maybe (fromJust)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Graphics.Vty as V
import System.Exit (exitFailure)

import qualified Brick.Types as T
import Brick.AttrMap
import Brick.Util
import Brick.Types (Widget)
import qualified Brick.Main as M
import Brick.Widgets.Core ((<=>), txt, withAttr, txtWrap, hLimit, padLeft, Padding(..))
import Brick.Widgets.Center (center)
import Brick.Widgets.Menu

import qualified Brick.Keybindings as K

-- | The abstract key events for the application.
data KeyEvent = QuitEvent
              | ToggleFileMenuEvent
              | NewEvent
              | OpenEvent
              deriving (Ord, Eq, Show)

-- | The mapping of key events to their configuration field names.
allKeyEvents :: K.KeyEvents KeyEvent
allKeyEvents =
    K.keyEvents [ ("quit",             QuitEvent)
                , ("toggle-file-menu", ToggleFileMenuEvent)
                , ("new",              NewEvent)
                , ("open",             OpenEvent)
                ]

-- | Default key bindings for each abstract key event.
defaultBindings :: [(KeyEvent, [K.Binding])]
defaultBindings =
    [ (QuitEvent,           [K.ctrl 'q'])
    , (ToggleFileMenuEvent, [K.meta 'f'])
    , (NewEvent,            [K.meta 'n'])
    , (OpenEvent,           [K.meta 'o'])
    ]

data Name = FileMenu MenuRegion
          deriving (Show, Ord, Eq)

data St =
    St { _keyConfig :: K.KeyConfig KeyEvent
       , _dispatcher :: K.KeyDispatcher KeyEvent (T.EventM Name St)
       , _fileMenu :: DispatchingMenu St Name KeyEvent
       , _lastAction :: Text.Text
       }

makeLenses ''St

drawUi :: St -> [Widget Name]
drawUi st =
    [ padLeft (Pad 1) $
      renderMenu st (st^.fileMenu)
    , center $
      hLimit 60 $
      (txtWrap $
       Text.unlines $
       [ "Click the menu title with the mouse or press Alt-F to open the menu."
       , ""
       , "When the menu is open, press arrow keys to select items and then " <>
         "press Enter to activate them, or click them with the mouse instead."
       , ""
       , "When the menu is open or closed, press the keybindings shown in the " <>
         "menu to activate the corresponding menu items."
       , ""
       ])
      <=>
      (withAttr emphAttr $
        txt $ "Last action: " <> st^.lastAction)
    ]

-- | Key event handlers for our application.
handlers :: [K.KeyEventHandler KeyEvent (T.EventM n St)]
handlers =
    [ K.onEvent QuitEvent "Quit the program" M.halt

    , K.onEvent ToggleFileMenuEvent "Toggle the File menu" $ do
        lastAction .= "Toggled the File menu"
        fileMenu %= toggleMenu

    , K.onEvent NewEvent "New" $
        lastAction .= "Activated New... menu entry"

    , K.onEvent OpenEvent "Open" $
        lastAction .= "Activated Open... menu entry"

    , K.onKey (K.ctrl 't') "Fixed key" $
        lastAction .= "Activated fixed-key event handler"
    ]

appEvent :: T.BrickEvent Name e -> T.EventM Name St ()
appEvent e = void $ handleMenuEvent fileMenu e

emphAttr :: AttrName
emphAttr = attrName "emphasis"

aMap :: AttrMap
aMap = attrMap V.defAttr
    [ (menuAttr, fg V.white)
    , (menuTitleAttr, fg V.white)
    , (menuTitleSelectedAttr, V.black `on` V.white)
    , (menuEntryDisabledAttr, fg V.red)
    , (menuEntrySelectedAttr, V.black `on` V.yellow)
    , (menuEntrySelectedDisabledAttr, V.black `on` V.red)
    , (menuEntryKeybindingAttr, fg V.cyan `V.withStyle` V.bold)
    , (emphAttr, fg V.white)
    ]

app :: M.App St e Name
app =
    M.App { M.appDraw = drawUi
          , M.appStartEvent = do
              vty <- M.getVtyHandle
              liftIO $ V.setMode (V.outputIface vty) V.Mouse True
          , M.appHandleEvent = appEvent
          , M.appAttrMap = const aMap
          , M.appChooseCursor = M.showFirstCursor
          }

newFileMenu :: K.KeyDispatcher KeyEvent (T.EventM Name St) -> DispatchingMenu St Name KeyEvent
newFileMenu d =
    menuWithDispatcher d "File" FileMenu
        [ menuEntryForEvent "New..." NewEvent
        , menuEntryForEvent "Open..." OpenEvent
        , menuEntryForKey "Test" (K.ctrl 't')
        , menuEntryForAction "Test 2" (lastAction .= "Activated 'Test 2' item")
        , menuSeparator
        , menuEntryForEvent "Exit" QuitEvent
        ]

sectionName :: Text.Text
sectionName = "keybindings"

main :: IO ()
main = do
    -- Create a key config that includes the default bindings.
    let kc = K.newKeyConfig allKeyEvents defaultBindings []

    -- Build a key dispatcher for our event handlers. If this fails
    -- due to key collision detection, we'll print out info about the
    -- collisions.
    d <- case K.keyDispatcher kc handlers of
        Right d -> return d
        Left collisions -> do
            putStrLn "Error: some key events have the same keys bound to them."

            forM_ collisions $ \(b, hs) -> do
                Text.putStrLn $ "Handlers with the '" <> K.ppBinding b <> "' binding:"
                forM_ hs $ \h -> do
                    let trigger = case K.kehEventTrigger $ K.khHandler h of
                            K.ByKey k   -> "triggered by the key '" <> K.ppBinding k <> "'"
                            K.ByEvent e -> "triggered by the event '" <> fromJust (K.keyEventName allKeyEvents e) <> "'"
                        desc = K.handlerDescription $ K.kehHandler $ K.khHandler h

                    Text.putStrLn $ "  " <> desc <> " (" <> trigger <> ")"

            exitFailure

    void $ M.defaultMain app $ St kc d (newFileMenu d) "(none yet)"