hsfm/src/GUI/Gtk/Gui.hs

425 lines
13 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
)
import Graphics.UI.Gtk
import Graphics.UI.Gtk.Abstract.Box
import Graphics.UI.Gtk.Builder
import Graphics.UI.Gtk.ModelView
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.Posix
(
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
-- |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
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
[Control] <- eventModifier
"h" <- fmap glibToString eventKeyName
liftIO $ modifyTVarIO (settings mygui)
(\x -> x { showHidden = not . showHidden $ x})
>> updateTreeView mygui myview
_ <- rootWin mygui `on` keyPressEvent $ tryEvent $ do
[Alt] <- eventModifier
"Up" <- fmap glibToString eventKeyName
liftIO $ upDir mygui myview
_ <- treeView mygui `on` rowActivated $ openRow 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?
when (abs && exists) $ do
newFsState <- readPath' fp
newRawModel <- fileListStore newFsState myview
writeTVarIO (rawModel myview) newRawModel
updateTreeView mygui myview
-- |Enter a subdirectory and visualize it in the treeView or
-- open a file.
--
-- This might update the TVar `rawModel`.
openRow :: MyGUI -> MyView -> TreePath -> TreeViewColumn -> IO ()
openRow mygui myview tp tvc = do
rawModel' <- readTVarIO $ rawModel myview
sortedModel' <- readTVarIO $ sortedModel myview
filteredModel' <- readTVarIO $ filteredModel myview
miter <- treeModelGetIter sortedModel' tp
for_ miter $ \iter -> do
cIter' <- treeModelSortConvertIterToChildIter sortedModel' iter
cIter <- treeModelFilterConvertIterToChildIter filteredModel' cIter'
row <- treeModelGetRow rawModel' cIter
case row of
(Dir _ _ _, _) -> do
newRawModel <- fileListStore row myview
rm <- readTVarIO (rawModel myview)
writeTVarIO (rawModel myview) newRawModel
updateTreeView mygui myview
dz@(File _ _, _) ->
withErrorDialog $ openFile (getFullPath dz)
_ -> return ()
-- |Go up one directory and visualize it in the treeView.
--
-- This will update the TVar `rawModel`.
upDir :: MyGUI -> MyView -> IO ()
upDir mygui myview = do
rawModel' <- readTVarIO $ rawModel myview
sortedModel' <- readTVarIO $ sortedModel myview
fS <- readTVarIO $ fsState myview
newRawModel <- fileListStore (goUp fS) myview
writeTVarIO (rawModel myview) newRawModel
updateTreeView mygui myview
-- |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)
-- TODO: make this function more slim so only the most necessary parts are
-- called
-- |Updates 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.
updateTreeView :: MyGUI
-> MyView
-> IO ()
updateTreeView 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
-- |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
-- |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 . baseZipper . dirTree)
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"
-- 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
updateTreeView mygui myview
-- set the bindings
setBindings mygui myview
-- add the treeview to the scroll container
containerAdd scroll treeView
widgetShowAll rootWin