packages feed

astview-0.1: src/Language/Astview/GUI.hs

module Language.Astview.GUI where

-- base
import Prelude hiding (writeFile)
import Data.Maybe(fromJust,isJust)
import Data.List (find,findIndex)
import Control.Monad ((=<<),when,liftM,filterM)
import Data.Char (toLower)
import Control.Monad.Trans (liftIO)

-- io
import System.IO 
  (IOMode,withFile,hGetContents,IOMode(..),hPutStr,hClose)
import System.IO.Error

-- state
import Data.IORef

-- filepath
import System.FilePath 
  (takeExtension,splitFileName,pathSeparator,joinPath)
import System.Directory (doesDirectoryExist)

-- bytestring
import qualified Data.ByteString.Char8 as BS (hGetContents,unpack)

-- containers
import Data.Tree ( Tree(Node,rootLabel) )

-- gtk
import Graphics.UI.Gtk  -- (import all)
import Graphics.UI.Gtk.Gdk.EventM

-- glib
import System.Glib.Signals (ConnectId)

-- glade
import Graphics.UI.Gtk.Glade      -- (import all)
import Graphics.UI.Gtk.ModelView  -- (import all)

-- gtksourceview
import Graphics.UI.Gtk.SourceView -- (import all)

-- commands
import System.Cmd (rawSystem)

-- astview-utils
import Language.Astview.Parser

-- generated on-the-fly by cabal
import Paths_astview (getDataFileName,getDataDir) 


-- -------------------------------------------------------------------
-- * Main GUI types and functions
-- -------------------------------------------------------------------

-- | a simple container type to ease access to GUI components
data GUI = GUI {
    window :: Window              -- ^ main window
  , tv :: TreeView                -- ^ treeview
  , tb :: SourceBuffer            -- ^ sourceview
  , rFile :: IORef String         -- ^ current file
  , rChanged :: IORef Bool        -- ^ true if file changed
  , rParsers:: IORef [Parser]     -- ^ parsers
  , rCurParser :: IORef Parser    -- ^ current parser
  , dlgOpen :: FileChooserDialog  -- ^ filechooser (fc)
  , dlgSave :: FileChooserDialog  -- ^ filechooser (save as)
  , dlgAbout :: AboutDialog       -- ^ about dialog
  , btnOpenOpen :: Button         -- ^ open button of the fc
  , btnOpenCancel :: Button       -- ^ cancel button of the fc
  , btnSaveSave :: Button         -- ^ save as button of dlg_SaveAs
  , btnSaveCancel :: Button       -- ^ cancel button of dlg_SaveAs
  , entryName :: Entry            -- ^ text entry of dlg_SaveAs
  , cbox :: ComboBox
  }

-- | takes a reference to the compound GUI type and performs
--   some action on the GUI. see section GUI-Actions below
type GUIAction = GUI -> IO ()

-- | pairs a gtk-id with a GUIAction to easily map over all MenuItems
type MenuAction = (String,GUIAction)

-- | a list of pairs of gtk-ids and GUIActions 
menuActions :: [MenuAction]
menuActions = 
  [("mNew",actionEmptyGUI)
  ,("mOpen",actionDlgOpenRun)
  ,("mParse",actionReparse)
  ,("mSave",actionSave)
  ,("mSaveAs",actionDlgSaveRun)
  ,("mCut",actionCutSource)
  ,("mCopy",actionCopySource)
  ,("mPaste",actionPasteSource)
  ,("mDelete",actionDeleteSource)
  ,("mAbout",actionAbout)
  ,("mShowHelp",actionHelp)
  ,("mQuit",actionQuit)
  ]

-- | builds the GUI 
buildGUI :: [Parser] -> IO GUI
buildGUI parsers = do
  -- GTK init
  initGUI 

  -- load GladeXML
  Just xml <- xmlNew =<< getDataFileName "data/astview.glade"
 
  -- get or create widgets
  window   <- xmlGetWidget xml castToWindow "mainWindow"
  treeview <- xmlGetWidget xml castToTreeView "treeview"

  tb <- buildSourceView 
    =<< xmlGetWidget xml castToScrolledWindow "swSource" 

  dlgOpen <-xmlGetWidget xml castToFileChooserDialog "dlgOpen"
  dlgSave <-xmlGetWidget xml castToFileChooserDialog "dlgSave"
  dlgAbout <-xmlGetWidget xml castToAboutDialog "dlgAbout"
  btnOpenOpen <- xmlGetWidget xml castToButton "btnOpenOpen"
  btnOpenCancel <- xmlGetWidget xml castToButton "btnOpenCancel"
  btnSaveSave <- xmlGetWidget xml castToButton "btnSaveSave"
  btnSaveCancel <- xmlGetWidget xml castToButton "btnSaveCancel"
  entryName <- xmlGetWidget xml castToEntry "entryName"

  -- storage for current file
  rFile <- newIORef unsavedDoc
  -- 
  rChanged <- newIORef False
  -- storage for all parsers
  rParsers <- newIORef parsers
  -- storage for current parser 
  rCurParser <- newIORef $ head parsers

  -- setup combobox
  vbox <- xmlGetWidget xml castToVBox "vboxMain"
  cbox <- comboBoxNewText
  containerAdd vbox cbox
  mapM_ (comboBoxAppendText cbox . buildLabel) parsers 

  -- build compound datatype
  let gui = GUI {
      window=window 
    , tv=treeview 
    , tb=tb
    , rFile=rFile
    , rChanged=rChanged
    , rParsers=rParsers
    , rCurParser=rCurParser
    , dlgOpen=dlgOpen
    , dlgSave=dlgSave
    , dlgAbout=dlgAbout
    , btnOpenOpen=btnOpenOpen 
    , btnOpenCancel=btnOpenCancel 
    , btnSaveSave=btnSaveSave
    , btnSaveCancel=btnSaveCancel
    , entryName=entryName
    , cbox=cbox
    }

  -- get all menuitems from xml and register guiactions to them
  mapM_ (registerMenuAction xml gui) menuActions
  
  -- add hooks to buttons
  hooks gui 

  -- finally return gui
  return gui

-- -------------------------------------------------------------------
-- ** some helper functions
-- -------------------------------------------------------------------

-- |builds combobox label for a parser
buildLabel :: Parser -> String
buildLabel parser = 
  name parser
  ++ " ["
  ++ concatMap (" "++) (exts parser)
  ++ "]"

-- |suffix of window title
suffix :: String
suffix = " - astview"

-- |unsaved document
unsavedDoc :: String
unsavedDoc = "Unsaved document"

-- | setup the GtkSourceView and add it to the ScrollPane. return the 
-- underlying textbuffer
buildSourceView :: ScrolledWindow -> IO SourceBuffer
buildSourceView sw = do
  sourceBuffer <- sourceBufferNew Nothing
  sourceBufferSetHighlightSyntax sourceBuffer True
  sourceView <- sourceViewNewWithBuffer sourceBuffer
  sourceViewSetShowLineNumbers sourceView True
  sourceViewSetHighlightCurrentLine sourceView True
  srcfont <- fontDescriptionFromString "Monospace 10"
  widgetModifyFont sourceView (Just srcfont)
  containerAdd sw sourceView
  return sourceBuffer

-- | registers one GUIAction with a MenuItem
registerMenuAction 
  :: GladeXML -> GUI -> MenuAction -> IO (ConnectId MenuItem)
registerMenuAction xml gui (gtkId,guiaction) = do
  item <- xmlGetWidget xml castToMenuItem gtkId
  onActivateLeaf item (guiaction gui)

-- | adds actions to some widgets
hooks :: GUI -> IO (ConnectId Window)
hooks gui = do
  -- filechooser open
  onClicked (btnOpenOpen gui) (actionLoadChooser gui)
  onClicked (btnOpenCancel gui) (widgetHide (dlgOpen gui) )
  afterFileActivated (dlgOpen gui) (actionLoadChooser gui)
  -- filechooser save
  onClicked (btnSaveSave gui) (actionSaveAs gui)
  onClicked (btnSaveCancel gui) (widgetHide (dlgSave gui))
  onUpdatePreview (dlgSave gui) (actionUpdateName gui) 
  
  -- textbuffer
  onBufferChanged (tb gui) (actionBufferChanged gui)
  
  -- ctrl+p to reparse
  (window gui) `on` keyPressEvent $ tryEvent $ do
    [Control] <- eventModifier
    "p" <- eventKeyName
    liftIO $ actionReparse gui 

  (cbox gui) `on` changed $ do
    i <- comboBoxGetActive (cbox gui) 
    parsers <- readIORef (rParsers gui)
    let parser = parsers!!i
    writeIORef (rCurParser gui) parser 
    comboBoxSetActive (cbox gui) i
    actionParse (parser) gui

  (dlgAbout gui) `onResponse` (\ _ -> widgetHide (dlgAbout gui) )
        
  (window gui) `on` deleteEvent $ tryEvent $ do
    liftIO $ actionQuit gui
  
  -- window    
  onDestroy (window gui) mainQuit
    
-- -------------------------------------------------------------------
-- * GUI Actions
-- -------------------------------------------------------------------

-- -------------------------------------------------------------------
-- ** filemenu
-- -------------------------------------------------------------------

-- | resets the GUI, 
actionEmptyGUI :: GUIAction
actionEmptyGUI gui = do
  maybeCol <- treeViewGetColumn (tv gui) 0
  case maybeCol of
    Just col-> treeViewRemoveColumn (tv gui) col
    Nothing -> return undefined
  textBufferSetText (tb gui) ""
  writeIORef (rFile gui) unsavedDoc
  writeIORef (rChanged gui) False
  windowSetTitle (window gui) (unsavedDoc++suffix)

-- | updates the sourceview with a given file, chooses a parser by 
-- extension and parses the file
actionLoadHeadless :: FilePath -> GUIAction
actionLoadHeadless file gui = 
  catch 
    (do
      parsers <- readIORef (rParsers gui) 
      writeIORef (rFile gui) file
      contents <- withFile 
        file ReadMode (fmap BS.unpack . BS.hGetContents)
      textBufferSetText (tb gui) contents

      let filename = snd $ splitFileName file
      windowSetTitle (window gui) (filename ++ suffix)
      let e = takeExtension file
      whenJust 
        (find (elem e . exts) parsers) $
        \parser -> do   
          activateParser parser gui
          actionParse parser gui
          writeIORef (rChanged gui) False 
    )
    print

-- | helper for loadHeadless
activateParser :: Parser -> GUIAction
activateParser parser gui = do
  writeIORef (rCurParser gui) parser
  parsers <- readIORef (rParsers gui) 
  case findIndex (parser==) parsers of
    Just i -> comboBoxSetActive (cbox gui) i
    Nothing-> return ()

-- | helper for loadHeadless
setupSyntaxHighlighting :: Parser -> GUIAction
setupSyntaxHighlighting parser gui = do
  langManager <- sourceLanguageManagerGetDefault
  maybeLang <- sourceLanguageManagerGetLanguage 
    langManager 
    (map toLower (name parser))
  case maybeLang of
    Just l -> do
      sourceBufferSetHighlightSyntax (tb gui) True
      sourceBufferSetLanguage (tb gui) l 
    Nothing-> 
      sourceBufferSetHighlightSyntax (tb gui) False   

  
-- | gets filename from filechooser and delegates to 
-- actionLoadHeadless
actionLoadChooser :: GUIAction
actionLoadChooser gui = do
  whenJustM
    (fileChooserGetFilename (dlgOpen gui)) $ 
    \file -> do
      actionLoadHeadless file gui
      widgetHide (dlgOpen gui)

-- | parses the contents of the sourceview with the selected parser
actionParse :: Parser -> GUIAction
actionParse parser gui = do
  writeIORef (rCurParser gui) parser
  sourceBufferSetHighlightSyntax (tb gui) True
  setupSyntaxHighlighting parser gui
  plain <- getText gui
  maybeCol <- treeViewGetColumn (tv gui) 0
  case maybeCol of
    Just col-> treeViewRemoveColumn (tv gui) col
    Nothing -> return (-1)
  let tree' = (tree parser) plain
  model <- treeStoreNew [Node "" [tree']]
  treeViewSetModel (tv gui) model
  col <- treeViewColumnNew
  renderer <- cellRendererTextNew
  cellLayoutPackStart col renderer True 
  cellLayoutSetAttributes 
    col 
    renderer 
    model 
    (\row -> [ cellText := row ] )
  treeViewAppendColumn (tv gui) col 
  return ()

-- |saves file to url saved in clipboard
actionSave :: GUIAction
actionSave gui = do
  file <- readIORef (rFile gui)
  text <- getText gui
  actionSaveWorker gui text file

-- |saves curren file if a file is active or calls "save as"-dialog
actionSaveWorker :: GUI -> String -> String -> IO ()
actionSaveWorker gui plain file =
  case file of
    "Unsaved document"  -> actionDlgSaveRun gui
    otherwise           -> do 
      deleteStar gui
      writeIORef (rChanged gui) False
      writeFile file plain 
 
-- |saves file with filename given by textentry
actionSaveAs :: GUIAction
actionSaveAs gui =
  whenJustM
    (fileChooserGetCurrentFolder (dlgSave gui)) $
    \path -> do
      name <- entryGetText (entryName gui)
      let filepath = (path++[pathSeparator]++name)
      writeIORef (rFile gui) filepath
      writeIORef (rChanged gui) False
      writeFile filepath =<< getText gui
      windowSetTitle (window gui) (name++suffix)
      widgetHide (dlgSave gui)
  
-- |updates the textview after selecting a file in save as filechooser
actionUpdateName :: GUIAction
actionUpdateName gui = do
  whenJustM (fileChooserGetFilename (dlgSave gui)) $ \ path -> do
    isDir <- doesDirectoryExist path
    when (not isDir) $
      let (_,file) = splitFileName path in 
      entrySetText (entryName gui) file

-- -------------------------------------------------------------------
-- ** edit menu
-- -------------------------------------------------------------------

-- |moves selected source to clipboard (cut)
actionCutSource :: GUIAction  
actionCutSource gui = do
  actionCopySource gui
  actionDeleteSource gui
 
-- |copies selected source to clipboard  
actionCopySource :: GUIAction
actionCopySource gui = do
  (start,end) <- textBufferGetSelectionBounds (tb gui) 
  clipBoard <- clipboardGet selectionClipboard
  clipboardSetText 
    clipBoard 
    =<< textBufferGetText (tb gui) start end True

-- |pastes text from clipboard at current cursor position  
actionPasteSource :: GUIAction
actionPasteSource gui = do
  clipBoard <- clipboardGet selectionClipboard
  clipboardRequestText clipBoard (insertAt (tb gui)) where
    insertAt :: SourceBuffer -> Maybe String -> IO ()
    insertAt tb m = whenJust m (textBufferInsertAtCursor tb)

-- |deletes selected source
actionDeleteSource :: GUIAction
actionDeleteSource gui = 
  textBufferDeleteSelection (tb gui) False False >> return ()



-- -------------------------------------------------------------------
-- ** help menu
-- -------------------------------------------------------------------

-- | shows the help dialog
actionHelp :: GUIAction
actionHelp gui = do
  helpfile <- getDataFileName "data/astview.html"
  dir <- getDataDir
  rawSystem "firefox" [joinPath [dir,helpfile]]
  return ()
    
-- | launches info dialog
actionAbout :: GUIAction
actionAbout gui = do
  aboutDialogSetUrlHook (\_ -> return ())
  licensefile <- getDataFileName "data/LICENSE.unwrapped"
  contents <- catch 
    (withFile licensefile ReadMode ((fmap BS.unpack) . BS.hGetContents))
    (\ioe -> return $ "Err" ++ (show ioe))
  aboutDialogSetWrapLicense (dlgAbout gui) True 
  aboutDialogSetLicense (dlgAbout gui) (Just contents)
  widgetShow (dlgAbout gui)


-- -------------------------------------------------------------------
-- ** other actions and helpers
-- -------------------------------------------------------------------

-- |similar to *when*. 
whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
whenJust m action =
  when (isJust m) ((action.fromJust) m) 
  
-- |similar to *whenJust*, but value is inside a monad
whenJustM :: Monad m => m(Maybe a) -> (a -> m ()) -> m ()
whenJustM val action = do
  m <- val
  when (isJust m) ((action.fromJust) m)   

-- | adds '*' to window title if file changed and sets state
actionBufferChanged :: GUIAction
actionBufferChanged gui = do
  writeIORef (rChanged gui) True
  t <- windowGetTitle (window gui)
  when (head t /= '*') (windowSetTitle (window gui) ('*':t))


-- | destroys window widget 
actionQuit :: GUIAction
actionQuit gui = do 
  changed <- readIORef (rChanged gui) 
  if changed 
    then do 
      dia <- dialogNew
      dialogAddButton dia stockYes ResponseYes
      dialogAddButton dia stockNo ResponseNo
      dialogAddButton dia stockCancel ResponseCancel
      contain <- dialogGetUpper dia
  
      windowSetTitle dia "astview"
      containerSetBorderWidth dia 2
      file <- readIORef (rFile gui)
      lbl <- labelNew 
        (Just $ "Save changes to document \""++
                (snd $ splitFileName file) ++
                "\" before closing?")
      boxPackStartDefaults contain lbl

      widgetShowAll dia
      response <- dialogRun dia
      case response of 
        ResponseYes    -> actionSave gui
        ResponseCancel -> return ()
        ResponseNo     -> widgetDestroy (window gui)
      widgetHide dia
    else widgetDestroy (window gui)
  

-- | launches open dialog
actionDlgOpenRun :: GUIAction
actionDlgOpenRun gui = dialogRun (dlgOpen gui) >> return ()

-- | launches save dialog
actionDlgSaveRun :: GUIAction
actionDlgSaveRun gui = do
  setupDlgSave gui =<< readIORef (rFile gui)
  dialogRun (dlgSave gui) >> return ()

-- |set current directory and filename in dlgSave (setup)
setupDlgSave :: GUI -> String -> IO ()
setupDlgSave gui s = do 
  let (dir,file) = splitFileName s
  fileChooserSetFilename (dlgSave gui) dir
  entrySetText (entryName gui) file

-- |applies current parser to current sourcebuffer 
actionReparse :: GUIAction
actionReparse gui = do
  parser <- readIORef (rCurParser gui)
  activateParser parser gui
  actionParse parser gui

-- |removes @*@ from window title if existing  
deleteStar :: GUIAction
deleteStar gui = do
  t <- windowGetTitle (window gui)
  when (head t == '*') (windowSetTitle (window gui) (tail t))

-- | helper for various text-processing actions
getText :: GUI -> IO String
getText gui = do
  start <- textBufferGetStartIter (tb gui)
  end <- textBufferGetEndIter (tb gui)
  textBufferGetText (tb gui) start end True
  
-- |safe function to write files
writeFile :: FilePath -> String -> IO ()
writeFile f str = catch
  (withFile f WriteMode (\h -> hPutStr h str >> hClose h))
  (putStrLn . show)