hsfm/src/GUI/Gtk/Gui.hs

522 lines
16 KiB
Haskell

{-# OPTIONS_HADDOCK ignore-exports #-}
module GUI.Gtk.Gui (startMainWindow) where
import Control.Applicative
(
(<$>)
, (<*>)
)
import Control.Concurrent
(
forkIO
)
import Control.Concurrent.STM
(
TVar
, newTVarIO
, readTVarIO
)
import Control.Exception
(
try
, Exception
, SomeException
)
import Control.Monad
(
when
, void
)
import Control.Monad.IO.Class
(
liftIO
)
import Data.DirTree
import Data.DirTree.Zipper
import Data.Foldable
(
for_
)
import Data.List
(
sort
, isPrefixOf
)
import Data.Maybe
(
fromJust
, catMaybes
, fromMaybe
)
import Data.Traversable
(
forM
)
import Graphics.UI.Gtk
import GUI.Gtk.Icons
import IO.Error
import IO.File
import IO.Utils
import System.Directory
(
executable
, doesFileExist
, doesDirectoryExist
)
import System.Environment
(
getArgs
)
import System.FilePath
(
isAbsolute
)
import System.Glib.UTFString
(
glibToString
)
import System.IO.Unsafe
(
unsafePerformIO
)
import System.Process
(
spawnProcess
)
-- TODO: simplify where we modify the TVars
-- TODO: double check garbage collection/gtk ref counting
-- TODO: file watching, when and what to reread
-- |Monolithic object passed to various GUI functions in order
-- to keep the API stable and not alter the parameters too much.
-- This only holds GUI widgets that are needed to be read during
-- runtime.
data MyGUI = MkMyGUI {
-- |main Window
rootWin :: Window
, menubarFileQuit :: ImageMenuItem
, menubarFileOpen :: ImageMenuItem
, menubarFileCut :: ImageMenuItem
, menubarFileCopy :: ImageMenuItem
, menubarFilePaste :: ImageMenuItem
, menubarFileDelete :: ImageMenuItem
, menubarHelpAbout :: ImageMenuItem
, urlBar :: Entry
, statusBar :: Statusbar
-- |tree view
, treeView :: TreeView
-- |first column
, cF :: TreeViewColumn
-- |second column
, cMD :: TreeViewColumn
-- |renderer used for the treeView
, renderTxt :: CellRendererText
, renderPix :: CellRendererPixbuf
, settings :: TVar FMSettings
, folderPix :: Pixbuf
, filePix :: Pixbuf
, errorPix :: Pixbuf
}
-- |FM-wide settings.
data FMSettings = MkFMSettings {
showHidden :: Bool
, isLazy :: Bool
}
-- |This describes the contents of the treeView and is separated from MyGUI,
-- because we might want to have multiple views.
data MyView = MkMyView {
-- |raw model with unsorted data
rawModel :: TVar (ListStore (DTZipper DirTreeInfo DirTreeInfo))
-- |sorted proxy model
, sortedModel :: TVar (TypedTreeModelSort
(DTZipper DirTreeInfo DirTreeInfo))
-- |filtered proxy model
, filteredModel :: TVar (TypedTreeModelFilter
(DTZipper DirTreeInfo DirTreeInfo))
, fsState :: TVar (DTZipper DirTreeInfo DirTreeInfo)
}
-- |Set hotkeys.
setBindings :: MyGUI -> MyView -> IO ()
setBindings mygui myview = do
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
[Control] <- eventModifier
"q" <- fmap glibToString eventKeyName
liftIO mainQuit
_ <- treeView mygui `on` keyPressEvent $ tryEvent $ do
[Control] <- eventModifier
"h" <- fmap glibToString eventKeyName
liftIO $ modifyTVarIO (settings mygui)
(\x -> x { showHidden = not . showHidden $ x})
>> (refreshTreeView' mygui myview =<< readTVarIO (fsState myview))
_ <- treeView mygui `on` keyPressEvent $ tryEvent $ do
[Alt] <- eventModifier
"Up" <- fmap glibToString eventKeyName
liftIO $ upDir mygui myview
_ <- treeView mygui `on` keyPressEvent $ tryEvent $ do
"Delete" <- fmap glibToString eventKeyName
liftIO $ onRow Delete mygui myview
_ <- treeView mygui `on` rowActivated $ (\_ _ -> onRow Open mygui myview)
_ <- menubarFileQuit mygui `on` menuItemActivated $ mainQuit
_ <- urlBar mygui `on` entryActivated $ urlGoTo mygui myview
return ()
-- |Go the the url given at the `urlBar` and visualize it in the given
-- treeView.
--
-- This might update the TVar `rawModel`.
urlGoTo :: MyGUI -> MyView -> IO ()
urlGoTo mygui myview = do
fp <- entryGetText (urlBar mygui)
let abs = isAbsolute fp
exists <- (||) <$> doesDirectoryExist fp <*> doesFileExist fp
-- TODO: more explicit error handling?
refreshTreeView mygui myview (Just fp)
-- |Gets the currently selected row of the treeView, if any.
getSelectedRow :: MyGUI
-> MyView
-> IO (Maybe (DTZipper DirTreeInfo DirTreeInfo))
getSelectedRow mygui myview = do
(tp, _) <- treeViewGetCursor $ treeView mygui
rawModel' <- readTVarIO $ rawModel myview
sortedModel' <- readTVarIO $ sortedModel myview
filteredModel' <- readTVarIO $ filteredModel myview
miter <- treeModelGetIter sortedModel' tp
forM miter $ \iter -> do
cIter' <- treeModelSortConvertIterToChildIter sortedModel' iter
cIter <- treeModelFilterConvertIterToChildIter filteredModel' cIter'
treeModelGetRow rawModel' cIter
-- |Callback for file operations on a row, e.g. open, delete, etc.
--
-- This might update the TVar `rawModel`.
onRow :: FileOperation
-> MyGUI
-> MyView
-> IO ()
onRow fo mygui myview = do
mrow <- getSelectedRow mygui myview
for_ mrow $ \row ->
case fo of
Open -> open row
Delete -> del row
_ -> return ()
where
open row = case row of
(Dir {}, _) ->
refreshTreeView' mygui myview row
dz@(File {}, _) ->
withErrorDialog $ openFile (getFullPath dz)
_ -> return ()
del row = case row of
dz@(Dir {}, _) -> do
let fp = getFullPath dz
cmsg = "Really delete directory \"" ++ fp ++ "\"?"
withConfirmationDialog cmsg
$ withErrorDialog (deleteDir fp
>> refreshTreeView mygui myview Nothing)
dz@(File {}, _) -> do
let fp = getFullPath dz
cmsg = "Really delete file \"" ++ fp ++ "\"?"
withConfirmationDialog cmsg
$ withErrorDialog (deleteFile fp
>> refreshTreeView mygui myview Nothing)
-- |Go up one directory and visualize it in the treeView.
upDir :: MyGUI -> MyView -> IO ()
upDir mygui myview = do
rawModel' <- readTVarIO $ rawModel myview
sortedModel' <- readTVarIO $ sortedModel myview
fS <- readTVarIO $ fsState myview
refreshTreeView' mygui myview (goUp fS)
-- |Create the `ListStore` of files/directories from the current directory.
-- This is the function which maps the Data.DirTree data structures
-- into the GTK+ data structures.
--
-- This also updates the TVar `fsState` inside the given view.
fileListStore :: DTZipper DirTreeInfo DirTreeInfo -- ^ current dir
-> MyView
-> IO (ListStore (DTZipper DirTreeInfo DirTreeInfo))
fileListStore dtz myview = do
writeTVarIO (fsState myview) dtz
listStoreNew (goAllDown dtz)
-- |Re-reads the current directory or the given one and updates the TreeView.
-- This means that the DTZipper is re-initialized.
-- If you can operate on the raw DTZipper directly, use `refreshTreeView'`
-- instead.
--
-- This also updates the TVar `rawModel`.
--
-- This throws exceptions via `dirSanityThrow` if the given/current
-- directory path does not exist.
refreshTreeView :: MyGUI
-> MyView
-> Maybe FilePath
-> IO ()
refreshTreeView mygui myview mfp = do
fsState <- readTVarIO $ fsState myview
let cfp = getFullPath fsState
fp = fromMaybe cfp mfp
-- TODO catch exceptions
dirSanityThrow fp
newFsState <- readPath' fp
newRawModel <- fileListStore newFsState myview
writeTVarIO (rawModel myview) newRawModel
constructTreeView mygui myview
-- |Refreshes the TreeView based on the given Zipper.
--
-- This also updates the TVar `rawModel`.
refreshTreeView' :: MyGUI
-> MyView
-> DTZipper DirTreeInfo DirTreeInfo
-> IO ()
refreshTreeView' mygui myview dtz = do
newRawModel <- fileListStore dtz myview
writeTVarIO (rawModel myview) newRawModel
constructTreeView mygui myview
-- TODO: make this function more slim so only the most necessary parts are
-- called
-- |Constructs the visible TreeView with the current underlying mutable models,
-- which are retrieved from `MyGUI`.
--
-- This also updates the TVars `filteredModel` and `sortedModel` in the process.
constructTreeView :: MyGUI
-> MyView
-> IO ()
constructTreeView mygui myview = do
let treeView' = treeView mygui
cF' = cF mygui
cMD' = cMD mygui
render' = renderTxt mygui
-- update urlBar, this will break laziness slightly, probably
fsState <- readTVarIO $ fsState myview
let urlpath = getFullPath fsState
entrySetText (urlBar mygui) urlpath
rawModel' <- readTVarIO $ rawModel myview
-- filtering
filteredModel' <- treeModelFilterNew rawModel' []
writeTVarIO (filteredModel myview) filteredModel'
treeModelFilterSetVisibleFunc filteredModel' $ \iter -> do
hidden <- showHidden <$> readTVarIO (settings mygui)
row <- treeModelGetRow rawModel' iter
if hidden
then return True
else return $ not ("." `isPrefixOf` (name . unZip $ row))
-- sorting
sortedModel' <- treeModelSortNewWithModel filteredModel'
writeTVarIO (sortedModel myview) sortedModel'
treeSortableSetSortFunc sortedModel' 1 $ \iter1 iter2 -> do
cIter1 <- treeModelFilterConvertIterToChildIter filteredModel' iter1
cIter2 <- treeModelFilterConvertIterToChildIter filteredModel' iter2
row1 <- treeModelGetRow rawModel' cIter1
row2 <- treeModelGetRow rawModel' cIter2
return $ compare (unZip row1) (unZip row2)
treeSortableSetSortColumnId sortedModel' 1 SortAscending
-- set values
treeModelSetColumn rawModel' (makeColumnIdPixbuf 0)
(dirtreePix . unZip)
treeModelSetColumn rawModel' (makeColumnIdString 1)
(name . unZip)
treeModelSetColumn rawModel' (makeColumnIdString 2)
(packModTime . unZip)
treeModelSetColumn rawModel' (makeColumnIdString 3)
(packPermissions . unZip)
-- update treeview model
treeViewSetModel treeView' sortedModel'
return ()
where
dirtreePix (Dir {}) = folderPix mygui
dirtreePix (File {}) = filePix mygui
dirtreePix (Failed {}) = errorPix mygui
pushStatusBar :: MyGUI -> String -> IO (ContextId, MessageId)
pushStatusBar mygui str = do
let sb = statusBar mygui
cid <- statusbarGetContextId sb "FM Status"
mid <- statusbarPush sb cid str
return (cid, mid)
-- |Pops up an error Dialog with the given String.
showErrorDialog :: String -> IO ()
showErrorDialog str = do
errorDialog <- messageDialogNew Nothing
[DialogDestroyWithParent]
MessageError
ButtonsClose
str
_ <- dialogRun errorDialog
widgetDestroy errorDialog
showConfirmationDialog :: String -> IO Bool
showConfirmationDialog str = do
errorDialog <- messageDialogNew Nothing
[DialogDestroyWithParent]
MessageQuestion
ButtonsYesNo
str
rID <- dialogRun errorDialog
widgetDestroy errorDialog
case rID of
ResponseYes -> return True
ResponseNo -> return False
_ -> return False
withConfirmationDialog :: String -> IO () -> IO ()
withConfirmationDialog str io = do
run <- showConfirmationDialog str
when run io
-- |Execute the given IO action. If the action throws exceptions,
-- visualize them via `showErrorDialog`.
withErrorDialog :: IO a -> IO ()
withErrorDialog io = do
r <- try io
either (\e -> showErrorDialog $ show (e :: SomeException))
(\_ -> return ())
r
-- |Set up the GUI.
startMainWindow :: IO ()
startMainWindow = do
settings <- newTVarIO (MkFMSettings False True)
-- get the icons
iT <- iconThemeGetDefault
folderPix <- getIcon IFolder 24
filePix <- getIcon IFile 24
errorPix <- getIcon IError 24
fsState <- readPath' "/" >>= newTVarIO
builder <- builderNew
builderAddFromFile builder "data/Gtk/builder.xml"
-- get the pre-defined gui widgets
rootWin <- builderGetObject builder castToWindow
"rootWin"
scroll <- builderGetObject builder castToScrolledWindow
"mainScroll"
menubarFileQuit <- builderGetObject builder castToImageMenuItem
"menubarFileQuit"
menubarFileOpen <- builderGetObject builder castToImageMenuItem
"menubarFileOpen"
menubarFileCut <- builderGetObject builder castToImageMenuItem
"menubarFileCut"
menubarFileCopy <- builderGetObject builder castToImageMenuItem
"menubarFileCopy"
menubarFilePaste <- builderGetObject builder castToImageMenuItem
"menubarFilePaste"
menubarFileDelete <- builderGetObject builder castToImageMenuItem
"menubarFileDelete"
menubarHelpAbout <- builderGetObject builder castToImageMenuItem
"menubarHelpAbout"
urlBar <- builderGetObject builder castToEntry
"urlBar"
statusBar <- builderGetObject builder castToStatusbar
"statusBar"
-- create initial list store model with unsorted data
rawModel <- newTVarIO =<< listStoreNew . goAllDown =<< readTVarIO fsState
filteredModel <- newTVarIO =<< (\x -> treeModelFilterNew x [])
=<< readTVarIO rawModel
-- create an initial sorting proxy model
sortedModel <- newTVarIO =<< treeModelSortNewWithModel
=<< readTVarIO filteredModel
-- create the final view
treeView <- treeViewNew
-- create final tree model columns
renderTxt <- cellRendererTextNew
renderPix <- cellRendererPixbufNew
let ct = cellText :: (CellRendererTextClass cr) => Attr cr String
cp = cellPixbuf :: (CellRendererPixbufClass self) => Attr self Pixbuf
-- filename column
cF <- treeViewColumnNew
treeViewColumnSetTitle cF "Filename"
treeViewColumnSetResizable cF True
treeViewColumnSetClickable cF True
treeViewColumnSetSortColumnId cF 1
cellLayoutPackStart cF renderPix False
cellLayoutPackStart cF renderTxt True
_ <- treeViewAppendColumn treeView cF
cellLayoutAddColumnAttribute cF renderPix cp $ makeColumnIdPixbuf 0
cellLayoutAddColumnAttribute cF renderTxt ct $ makeColumnIdString 1
-- date column
cMD <- treeViewColumnNew
treeViewColumnSetTitle cMD "Date"
treeViewColumnSetResizable cMD True
treeViewColumnSetClickable cMD True
treeViewColumnSetSortColumnId cMD 2
cellLayoutPackStart cMD renderTxt True
_ <- treeViewAppendColumn treeView cMD
cellLayoutAddColumnAttribute cMD renderTxt ct $ makeColumnIdString 2
-- permissions column
cP <- treeViewColumnNew
treeViewColumnSetTitle cP "Permission"
treeViewColumnSetResizable cP True
treeViewColumnSetClickable cP True
treeViewColumnSetSortColumnId cP 3
cellLayoutPackStart cP renderTxt True
_ <- treeViewAppendColumn treeView cP
cellLayoutAddColumnAttribute cP renderTxt ct $ makeColumnIdString 3
-- construct the gui object
let mygui = MkMyGUI {..}
let myview = MkMyView {..}
-- create the tree model with its contents
constructTreeView mygui myview
-- set the bindings
setBindings mygui myview
-- add the treeview to the scroll container
containerAdd scroll treeView
widgetShowAll rootWin