packages feed

leksah-0.16.2.2: src/IDE/Utils/GUIUtils.hs

{-# LANGUAGE CPP, ScopedTypeVariables, OverloadedStrings, DataKinds, PatternSynonyms #-}
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
-----------------------------------------------------------------------------
--
-- Module      :  IDE.Utils.GUIUtils
-- Copyright   :  (c) Juergen Nicklisch-Franken, Hamish Mackenzie
-- License     :  GNU-GPL
--
-- Maintainer  :  <maintainer at leksah.org>
-- Stability   :  provisional
-- Portability :  portable
--
-- |
--
-------------------------------------------------------------------------------
module IDE.Utils.GUIUtils (
    chooseFile
,   chooseDir
,   chooseSaveFile
,   openBrowser
,   showDialog
,   showErrorDialog
,   showDialogOptions
,   showInputDialog

,   getFullScreenState
,   setFullScreenState
,   getDarkState
,   setDarkState

,   getBackgroundBuildToggled
,   setBackgroundBuildToggled
,   getMakeDocs
,   setMakeDocs
,   getRunUnitTests
,   setRunUnitTests
,   getRunBenchmarks
,   setRunBenchmarks
,   getMakeModeToggled
,   setMakeModeToggled
,   getDebugToggled
,   setDebugToggled

,   getRecentFiles
,   getRecentWorkspaces
,   getVCS

,   stockIdFromType
,   mapControlCommand
,   treeViewToggleRow
,   treeViewContextMenu
,   treeViewContextMenu'

,   __

,   fontDescription
) where

import IDE.Utils.Tool (runProcess)
import Data.Maybe
       (listToMaybe, fromMaybe, catMaybes, fromJust, isJust)
import Control.Monad (void, when, unless)
import IDE.Core.State
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Exception as E
import Data.Text (Text)
import Data.Monoid ((<>))
import qualified Data.Text as T (unpack, pack)
import Control.Applicative ((<$>))
import Data.List (intercalate)
import Data.Foldable (forM_)
import Data.Tree (Tree(..), Forest)
import GI.Gtk.Objects.Window
       (setWindowWindowPosition, windowSetTransientFor, setWindowTitle,
        Window(..))
import GI.Gtk.Enums
       (WindowPosition(..), MessageType(..), ButtonsType(..), MessageType,
        ResponseType(..), FileChooserAction(..))
import Data.GI.Base (new')
import GI.Gtk.Objects.FileChooserDialog (FileChooserDialog(..))
import GI.Gtk.Interfaces.FileChooser
       (fileChooserAddFilter, fileChooserGetFilename,
        fileChooserSetCurrentFolder, fileChooserSetAction)
import GI.Gtk.Objects.Dialog
       (Dialog(..), constructDialogUseHeaderBar, dialogGetContentArea,
        dialogSetDefaultResponse, dialogRun, dialogAddButton)
import GI.Gtk.Objects.FileFilter
       (fileFilterAddPattern, fileFilterSetName, fileFilterNew)
import GI.Gtk.Objects.MessageDialog
       (constructMessageDialogText, constructMessageDialogButtons,
        constructMessageDialogMessageType, MessageDialog(..))
import Data.GI.Base.ManagedPtr (unsafeCastTo)
import GI.Gtk.Objects.Box (boxPackStart, Box(..))
import GI.Gtk.Objects.Label (labelNew)
import GI.Gtk.Objects.Entry (getEntryText, setEntryText, entryNew, onEntryActivate)
import Graphics.UI.Editor.Parameters
       (dialogSetDefaultResponse', dialogRun', dialogAddButton',
        boxPackStart', Packing(..))
import GI.Gtk.Objects.ToggleAction
       (toggleActionSetActive, toggleActionGetActive, ToggleAction(..))
import GI.Gtk.Objects.MenuItem
       (toMenuItem, onMenuItemActivate, menuItemNewWithLabel,
        MenuItem(..))
import GI.Gtk.Objects.UIManager (uIManagerGetWidget)
import GI.Gtk.Objects.TreeView
       (toTreeView, TreeView(..), treeViewGetPathAtPos,
        treeViewGetSelection, treeViewExpandRow,
        treeViewCollapseRow, treeViewRowExpanded)
import GI.Gdk.Flags (ModifierType(..))
import GI.Gtk.Structs.TreePath
       (TreePath(..))
import GI.Gtk.Objects.Widget
       (Widget(..), toWidget, noWidget, onWidgetButtonPressEvent, onWidgetPopupMenu,
        widgetShowAll, widgetHide, widgetDestroy, widgetShow, widgetActivate)
import GI.Gdk
       (pattern BUTTON_SECONDARY, getEventButtonType, getEventButtonY,
        getEventButtonX, getEventButtonTime, getEventButtonButton)
import GI.Gdk.Enums (EventType(..))
import GI.Gtk.Objects.TreeSelection
       (treeSelectionSelectPath, treeSelectionUnselectAll, treeSelectionCountSelectedRows)
import Graphics.UI.Editor.Basics (Connection, Connection)
import Data.GI.Gtk.ModelView.ForestStore
       (forestStoreGetValue, forestStoreGetTree, ForestStore(..))
import GI.Gtk.Objects.Menu
       (Menu(..), menuPopup, menuAttachToWidget, menuNew)
import GI.Gtk.Objects.SeparatorMenuItem (separatorMenuItemNew)
import GI.Gtk.Objects.MenuShell (menuShellAppend)
import GI.Pango.Structs.FontDescription
       (fontDescriptionSetFamily, fontDescriptionNew, FontDescription(..))
import GI.Pango (fontDescriptionFromString)
import Data.GI.Base.BasicTypes (UnexpectedNullPointerReturn(..))
import GI.Gtk.Functions (getCurrentEventTime)
import Data.Word (Word32)
import Data.GI.Gtk.ModelView.Types
       (treeSelectionGetSelectedRows', treePathNewFromIndices')

#ifdef LOCALIZATION

import Text.I18N.GetText
import System.IO.Unsafe (unsafePerformIO)

#endif

chooseDir :: Window -> Text -> Maybe FilePath -> IO (Maybe FilePath)
chooseDir window prompt mbFolder = do
    dialog <- new' FileChooserDialog [constructDialogUseHeaderBar 1]
    setWindowTitle dialog prompt
    windowSetTransientFor dialog $ Just window
    fileChooserSetAction dialog FileChooserActionSelectFolder
    dialogAddButton' dialog "gtk-cancel" ResponseTypeCancel
    dialogAddButton' dialog "gtk-open" ResponseTypeAccept
    mapM_ (fileChooserSetCurrentFolder dialog) mbFolder
    widgetShow dialog
    response <- dialogRun' dialog
    case response of
        ResponseTypeAccept -> do
            fn <- fileChooserGetFilename dialog
            widgetDestroy dialog
            return fn
        ResponseTypeCancel -> do
            widgetDestroy dialog
            return Nothing
        ResponseTypeDeleteEvent -> do
            widgetDestroy dialog
            return Nothing
        _                   -> return Nothing

-- | Launch a "choose file" dialog
chooseFile :: Window
           -> Text                   -- ^ Window title
           -> Maybe FilePath         -- ^ Start location
           -> [(String, [String])]   -- ^ File filters, e.g. [("Music Files", ["*.mp3", "*.wav"])]
           -> IO (Maybe FilePath)
chooseFile window prompt mbFolder filters = do
    dialog <- new' FileChooserDialog [constructDialogUseHeaderBar 1]
    setWindowTitle dialog prompt
    windowSetTransientFor dialog $ Just window
    fileChooserSetAction dialog FileChooserActionOpen
    dialogAddButton' dialog "gtk-cancel" ResponseTypeCancel
    dialogAddButton' dialog "gtk-open" ResponseTypeAccept
    mapM_ (fileChooserSetCurrentFolder dialog) mbFolder
    forM_ filters (addFilter dialog)
    widgetShow dialog
    response <- dialogRun' dialog
    case response of
        ResponseTypeAccept -> do
            fn <- fileChooserGetFilename dialog
            widgetDestroy dialog
            return fn
        ResponseTypeCancel -> do
            widgetDestroy dialog
            return Nothing
        ResponseTypeDeleteEvent -> do
            widgetDestroy dialog
            return Nothing
        _                   -> return Nothing

    where
        addFilter dialog (description, exts) = do
            ff <- fileFilterNew
            fileFilterSetName ff . Just $ T.pack description
            forM_ exts (fileFilterAddPattern ff . T.pack)
            fileChooserAddFilter dialog ff

chooseSaveFile :: Window -> Text -> Maybe FilePath -> IO (Maybe FilePath)
chooseSaveFile window prompt mbFolder = do
    dialog <- new' FileChooserDialog [constructDialogUseHeaderBar 1]
    setWindowTitle dialog prompt
    windowSetTransientFor dialog $ Just window
    fileChooserSetAction dialog FileChooserActionSave
    dialogAddButton' dialog "gtk-cancel" ResponseTypeCancel
    dialogAddButton' dialog "gtk-open" ResponseTypeAccept
    mapM_ (fileChooserSetCurrentFolder dialog) mbFolder
    widgetShow dialog
    res <- dialogRun' dialog
    case res of
        ResponseTypeAccept  ->  do
            fileName <- fileChooserGetFilename dialog
            widgetDestroy dialog
            return fileName
        _               ->  do
            widgetDestroy dialog
            return Nothing



openBrowser :: Text -> IDEAction
openBrowser url = do
    prefs' <- readIDE prefs
    liftIO (E.catch (do
                runProcess (T.unpack $ browser prefs') [T.unpack url] Nothing Nothing Nothing Nothing Nothing
                return ())
            (\ (_ :: SomeException) -> sysMessage Normal ("Can't find browser executable " <> browser prefs')))
    return ()

-- | Show a text dialog with an Ok button and a specific messagetype
showDialog :: MonadIO m => Maybe Window -> Text -> MessageType -> m ()
showDialog parent msg msgType = do
    dialog <- new' MessageDialog [
        constructDialogUseHeaderBar 0,
        constructMessageDialogMessageType msgType,
        constructMessageDialogButtons ButtonsTypeOk,
        constructMessageDialogText msg]
    windowSetTransientFor dialog parent
    setWindowWindowPosition dialog WindowPositionCenterOnParent
    _ <- dialogRun' dialog
    widgetDestroy dialog
    return ()


-- | Show an error dialog with an Ok button
showErrorDialog :: MonadIO m => Maybe Window -> Text -> m ()
showErrorDialog parent msg = showDialog parent msg MessageTypeError


-- | Show a dialog with custom buttons and callbacks
showDialogOptions :: MonadIO m
                  => Maybe Window     -- ^ Parent window to use with `windowSetTransientFor`
                  -> Text             -- ^ the message
                  -> MessageType      -- ^ type of dialog
                  -> [(Text, m ())]  -- ^ button text and corresponding actions
                  -> Maybe Int        -- ^ index of button that has default focus (0-based)
                  -> m ()
showDialogOptions parent msg msgType buttons mbIndex = do
    dialog <- new' MessageDialog [
        constructDialogUseHeaderBar 0,
        constructMessageDialogMessageType msgType,
        constructMessageDialogButtons ButtonsTypeNone,
        constructMessageDialogText msg]

    windowSetTransientFor dialog parent
    forM_ (zip [0..] buttons) $ \(n,(text, _)) ->
        dialogAddButton' dialog text (AnotherResponseType n)

    dialogSetDefaultResponse' dialog (AnotherResponseType $ fromMaybe 0 mbIndex)
    setWindowWindowPosition dialog WindowPositionCenterOnParent
    res <- dialogRun' dialog
    widgetHide dialog
    case res of
        AnotherResponseType n | n >= 0 && n < length buttons -> map snd buttons !! n
        _ -> return ()




-- | Show a simple dialog that asks the user for some text
showInputDialog :: Maybe Window -- ^ Parent window to use with `windowSetTransientFor`
                -> Text -- ^ The message text
                -> Text -- ^ The default value
                -> IO (Maybe Text)
showInputDialog parent msg def = do
    dialog <- new' Dialog [constructDialogUseHeaderBar 1] -- Nothing [] MessageQuestion ButtonsOkCancel msg
    windowSetTransientFor dialog parent
    vbox   <- dialogGetContentArea dialog >>= unsafeCastTo Box
    label  <- labelNew (Just msg)
    entry  <- entryNew
    setEntryText entry def
    boxPackStart' vbox label PackNatural 0
    boxPackStart' vbox entry PackNatural 0
    widgetShowAll vbox

    -- Can't use messageDialog because of https://github.com/gtk2hs/gtk2hs/issues/114
    dialogAddButton' dialog "Cancel" ResponseTypeCancel
    okBtn <- dialogAddButton' dialog "Ok" ResponseTypeOk
    onEntryActivate entry $ widgetActivate okBtn >> return ()
    dialogSetDefaultResponse' dialog ResponseTypeOk

    res <- dialogRun' dialog
    widgetHide dialog

    case res of
        ResponseTypeOk -> do
            text <- getEntryText entry
            widgetDestroy dialog
            return (Just text)
        _ -> widgetDestroy dialog >> return Nothing




-- get widget elements (menu & toolbar)


getFullScreenState :: PaneMonad alpha => alpha Bool
getFullScreenState = do
    ui <- getUIAction "ui/menubar/_View/_Full Screen" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setFullScreenState :: PaneMonad alpha => Bool -> alpha ()
setFullScreenState b = do
    ui <- getUIAction "ui/menubar/_View/_Full Screen" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getDarkState :: PaneMonad alpha => alpha Bool
getDarkState = do
    ui <- getUIAction "ui/menubar/_View/_Use Dark Interface" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setDarkState :: PaneMonad alpha => Bool -> alpha ()
setDarkState b = do
    ui <- getUIAction "ui/menubar/_View/_Use Dark Interface" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getMenuItem :: Text -> IDEM MenuItem
getMenuItem path = (do
    uiManager' <- getUiManager
    mMenuItem <- uIManagerGetWidget uiManager' path
    case mMenuItem of
      Nothing -> throwIDE ("State.hs>>getMenuItem: Can't find ui path " <> path)
      Just item -> liftIO $ unsafeCastTo MenuItem item)
        `catchIDE` \(_::UnexpectedNullPointerReturn) ->
            throwIDE ("State.hs>>getMenuItem: Can't find ui path " <> path)

getBackgroundBuildToggled :: PaneMonad alpha => alpha Bool
getBackgroundBuildToggled = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/BackgroundBuild" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setBackgroundBuildToggled :: PaneMonad alpha => Bool -> alpha ()
setBackgroundBuildToggled b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/BackgroundBuild" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getMakeDocs :: PaneMonad alpha => alpha Bool
getMakeDocs = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/MakeDocs" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setMakeDocs :: PaneMonad alpha => Bool -> alpha ()
setMakeDocs b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/MakeDocs" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getRunUnitTests :: PaneMonad alpha => alpha Bool
getRunUnitTests = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/RunUnitTests" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setRunUnitTests :: PaneMonad alpha => Bool -> alpha ()
setRunUnitTests b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/RunUnitTests" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getRunBenchmarks :: PaneMonad alpha => alpha Bool
getRunBenchmarks = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/RunBenchmarks" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setRunBenchmarks :: PaneMonad alpha => Bool -> alpha ()
setRunBenchmarks b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/RunBenchmarks" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getMakeModeToggled :: PaneMonad alpha => alpha Bool
getMakeModeToggled = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/MakeMode" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setMakeModeToggled :: PaneMonad alpha => Bool -> alpha ()
setMakeModeToggled b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/MakeMode" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getDebugToggled :: PaneMonad alpha => alpha Bool
getDebugToggled = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/Debug" (unsafeCastTo ToggleAction)
    toggleActionGetActive ui

setDebugToggled :: PaneMonad alpha => Bool -> alpha ()
setDebugToggled b = do
    ui <- getUIAction "ui/toolbar/BuildToolItems/Debug" (unsafeCastTo ToggleAction)
    toggleActionSetActive ui b

getRecentFiles , getRecentWorkspaces, getVCS :: IDEM MenuItem
getRecentFiles    = getMenuItem "ui/menubar/_File/Recent Files"


getRecentWorkspaces = getMenuItem "ui/menubar/_File/Recent Workspaces"
getVCS = getMenuItem "ui/menubar/Version Con_trol" --this could fail, try returning Menu if it does
-- (toolbar)

stockIdFromType :: DescrType -> Text
stockIdFromType Variable        =   "ide_function"
stockIdFromType Newtype         =   "ide_newtype"
stockIdFromType Type            =   "ide_type"
stockIdFromType Data            =   "ide_data"
stockIdFromType Class           =   "ide_class"
stockIdFromType Instance        =   "ide_instance"
stockIdFromType Constructor     =   "ide_konstructor"
stockIdFromType Field           =   "ide_slot"
stockIdFromType Method          =   "ide_method"
stockIdFromType PatternSynonym  =   "ide_konstructor"
stockIdFromType _               =   "ide_other"


-- | Toggles a row in a `TreeView`
treeViewToggleRow treeView path = do
    expanded <- treeViewRowExpanded treeView path
    if expanded
        then treeViewCollapseRow treeView path
        else treeViewExpandRow   treeView path False

-- maps control key for Macos
#if defined(darwin_HOST_OS)
mapControlCommand ModifierTypeMod1Mask = ModifierTypeControlMask
#endif
mapControlCommand a = a

-- | Sets the context menu for a treeView widget
treeViewContextMenu' :: TreeView                 -- ^ The view
                     -> ForestStore a              -- ^ The model
                     -> (a -> TreePath -> ForestStore a -> IDEM [[(Text, IDEAction)]]) -- ^ Produces the menu items for the selected values when right clicking
                                                                                     -- The lists are seperated by a seperator
                     -> IDEM [Connection]
treeViewContextMenu' view store itemsFor = reifyIDE $ \ideRef -> do
    cid1 <- onWidgetPopupMenu view $ do
        t <- getCurrentEventTime
        showMenu 0 t ideRef
    cid2 <- onWidgetButtonPressEvent view $ \e -> do
        button    <- getEventButtonButton e
        click     <- getEventButtonType e
        timestamp <- getEventButtonTime e
        x         <- getEventButtonX e
        y         <- getEventButtonY e
        case (fromIntegral button, click) of
            (BUTTON_SECONDARY, EventTypeButtonPress) -> do
                sel <- treeViewGetSelection view
                selCount <- treeSelectionCountSelectedRows sel
                when (selCount <= 1) $ do
                    pathInfo <- treeViewGetPathAtPos view (floor x) (floor y)
                    case pathInfo of
                        (True, Just path, _, _, _) -> do
                            treeSelectionUnselectAll sel
                            treeSelectionSelectPath sel path
                        _ -> return ()
                showMenu button timestamp ideRef
            _ -> return False
    return $ map (ConnectC view) [cid1, cid2]
  where
    showMenu :: Word32 -> Word32 -> IDERef -> IO Bool
    showMenu button timestamp ideRef = do

        selPaths     <- treeViewGetSelection view >>= treeSelectionGetSelectedRows'
        selValues    <- mapM (forestStoreGetValue store) selPaths
        theMenu      <- menuNew
        menuAttachToWidget theMenu view Nothing
        forM_ (listToMaybe $ zip selValues selPaths) $ \(val, path) -> do
            itemsPerSection     <- flip reflectIDE ideRef $ itemsFor val path store
            menuItemsPerSection <- mapM (mapM (menuItemNewWithLabel . fst)) itemsPerSection


            forM_ (zip itemsPerSection menuItemsPerSection) $ \(section, itemsSection) ->
                forM_ (zip section itemsSection) $ \((_, onActivated), m) ->
                    onMenuItemActivate m $ reflectIDE onActivated ideRef

            unless (null itemsPerSection) $ do
                itemsAndSeparators <- sequence $
                    intercalate [separatorMenuItemNew >>= toMenuItem]
                                (map (map return) menuItemsPerSection)
                mapM_ (menuShellAppend theMenu) itemsAndSeparators
                menuPopup theMenu noWidget noWidget Nothing button timestamp
                widgetShowAll theMenu
        return True

treeViewContextMenu :: MonadIO m
                    => TreeView
                    -> (Menu -> IO ())
                    -> m [Connection]
treeViewContextMenu treeView populateMenu = do
    cid1 <- onWidgetPopupMenu treeView $ showMenu 0 =<< getCurrentEventTime
    cid2 <- onWidgetButtonPressEvent treeView $ \e -> do
        button    <- getEventButtonButton e
        click     <- getEventButtonType e
        timestamp <- getEventButtonTime e
        x         <- getEventButtonX e
        y         <- getEventButtonY e
        case (fromIntegral button, click) of
            (BUTTON_SECONDARY, EventTypeButtonPress) -> do
                sel <- treeViewGetSelection treeView
                selCount <- treeSelectionCountSelectedRows sel
                when (selCount <= 1) $ do
                    pathInfo <- treeViewGetPathAtPos treeView (floor x) (floor y)
                    case pathInfo of
                        (True, Just path, _, _, _) -> do
                            treeSelectionUnselectAll sel
                            treeSelectionSelectPath sel path
                        _ -> return ()
                showMenu button timestamp
            _ -> return False
    return $ map (ConnectC treeView) [cid1, cid2]
  where
    showMenu button timestamp = do
        theMenu <- menuNew
        menuAttachToWidget theMenu treeView Nothing
        populateMenu theMenu
        menuPopup theMenu noWidget noWidget Nothing button timestamp
        widgetShowAll theMenu
        return True

#ifdef LOCALIZATION

-- | For i18n using hgettext
__ :: Text -> Text
__ = T.pack . unsafePerformIO . getText . T.unpack


#else

-- | For i18n support. Not included in this build.
__ :: Text -> Text
__ = id

#endif

fontDescription :: Maybe Text -> IDEM FontDescription
fontDescription mbFontString =
    case mbFontString of
        Just str ->
            fontDescriptionFromString str
        Nothing -> do
            f <- fontDescriptionNew
            fontDescriptionSetFamily f "Monospace"
            return f