{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE TupleSections #-} {-# OPTIONS_HADDOCK show-extensions #-} -- | -- Module : Yi.Frontend.Pango.Layouts -- License : GPL-2 -- Maintainer : yi-devel@googlegroups.com -- Stability : experimental -- Portability : portable -- -- Provides abstract controls which implement 'Yi.Layout.Layout's and -- which manage the minibuffer. -- -- The implementation strategy is to first construct the layout -- managers @WeightedStack@ (implementing the 'Stack' constructor) and -- @SlidingPair@ (implementing the 'Pair' constructor), and then -- construct 'LayoutDisplay' as a tree of these, mirroring the -- structure of 'Layout'. module Yi.Frontend.Pango.Layouts ( -- * Getting the underlying widget WidgetLike(..), -- * Window layout LayoutDisplay, layoutDisplayNew, layoutDisplaySet, layoutDisplayOnDividerMove, -- * Miniwindow layout MiniwindowDisplay, miniwindowDisplayNew, miniwindowDisplaySet, -- * Tabs SimpleNotebook, simpleNotebookNew, simpleNotebookSet, simpleNotebookOnSwitchPage, -- * Utils update, ) where import Control.Applicative import Control.Arrow (first) import Control.Monad hiding (mapM, forM) import Data.Foldable (toList) import Data.IORef import qualified Data.List.PointedList as PL import qualified Data.Text as T import Data.Traversable import Graphics.UI.Gtk as Gtk hiding(Orientation, Layout) import Prelude hiding (mapM) import Yi.Layout(Orientation(..), RelativeSize, DividerPosition, Layout(..), DividerRef) class WidgetLike w where -- | Extracts the main widget. This is the widget to be added to the GUI. baseWidget :: w -> Widget ----------------------- The WeightedStack type {- | A @WeightedStack@ is like a 'VBox' or 'HBox', except that we may specify the ratios of the areas of the child widgets (so this implements the 'Stack' constructor of 'Yi.Layout.Layout'. Essentially, we implement this layout manager from scratch, by implementing the 'sizeRequest' and 'sizeAllocate' signals by hand (see the 'Container' documentation for details, and http://www.ibm.com/developerworks/linux/library/l-widget-pygtk/ for an example in Python). Ideally, we would directly subclass the abstract class 'Container', but Gtk2hs doesn't directly support this. Instead, we start off with the concrete class 'Fixed', and just override its layout behaviour. -} newtype WeightedStack = WS Fixed deriving(GObjectClass, ObjectClass, WidgetClass,ContainerClass) type StackDescr = [(Widget, RelativeSize)] weightedStackNew :: Orientation -> StackDescr -> IO WeightedStack weightedStackNew o s = do when (any ((<= 0) . snd) s) $ error "Yi.Frontend.Pango.WeightedStack.WeightedStack: all weights must be positive" l <- fixedNew set l (fmap ((containerChild :=) . fst) s) void $ Gtk.on l sizeRequest (doSizeRequest o s) void $ Gtk.on l sizeAllocate (relayout o s) return (WS l) -- | Requests the smallest size so that each widget gets its requested size doSizeRequest :: Orientation -> StackDescr -> IO Requisition doSizeRequest o s = let (requestAlong, requestAcross) = case o of Horizontal -> (\(Requisition w _) -> fromIntegral w, \(Requisition _ h) -> h) Vertical -> (\(Requisition _ h) -> fromIntegral h, \(Requisition w _) -> w) totalWeight = sum . fmap snd $ s reqsize (request, relSize) = requestAlong request / relSize sizeAlong widgetRequests = totalWeight * (maximum . fmap reqsize $ widgetRequests) sizeAcross widgetRequests = maximum . fmap (requestAcross . fst) $ widgetRequests mkRequisition wr = case o of Horizontal -> Requisition (round $ sizeAlong wr) (sizeAcross wr) Vertical -> Requisition (sizeAcross wr) (round $ sizeAlong wr) swreq (w, relSize) = (,relSize) <$> widgetSizeRequest w in boundRequisition =<< mkRequisition <$> mapM swreq s -- | Bounds the given requisition to not exceed screen dimensions boundRequisition :: Requisition -> IO Requisition boundRequisition r@(Requisition w h) = do mscr <- screenGetDefault case mscr of Just scr -> Requisition <$> (min w <$> screenGetWidth scr) <*> (min h <$> screenGetHeight scr) Nothing -> return r -- | Position the children appropriately for the given width and height relayout :: Orientation -> StackDescr -> Rectangle -> IO () relayout o s (Rectangle x y width height) = let totalWeight = sum . fmap snd $ s totalSpace = fromIntegral $ case o of Horizontal -> width Vertical -> height wtMult = totalSpace / totalWeight calcPosition pos (widget, wt) = (pos + wt * wtMult, (pos, wt * wtMult, widget)) widgetToRectangle (round -> pos, round -> size, widget) = case o of Horizontal -> (Rectangle pos y size height, widget) Vertical -> (Rectangle x pos width size, widget) startPosition = fromIntegral $ case o of Horizontal -> x Vertical -> y widgetPositions = fmap widgetToRectangle (snd (mapAccumL calcPosition startPosition s)) in forM_ widgetPositions $ \(rect, widget) -> widgetSizeAllocate widget rect ------------------------------------------------------- SlidingPair {-| 'SlidingPair' implements the 'Pair' constructor. Most of what is needed is already implemented by the 'HPaned' and 'VPaned' classes. The main feature added by 'SlidingPair' is that the divider position, *as a fraction of the available space*, remains constant even when resizing. -} newtype SlidingPair = SP Paned deriving(GObjectClass, ObjectClass, WidgetClass, ContainerClass) slidingPairNew :: (WidgetClass w1, WidgetClass w2) => Orientation -> w1 -> w2 -> DividerPosition -> (DividerPosition -> IO ()) -> IO SlidingPair slidingPairNew o w1 w2 pos handleNewPos = do p <- case o of Horizontal -> toPaned <$> hPanedNew Vertical -> toPaned <$> vPanedNew panedPack1 p w1 True True panedPack2 p w2 True True {- We want to catch the sizeAllocate signal. If this event is called, two things could have happened: the size could have changed; or the slider could have moved. We want to correct the slider position, but only if the size has changed. Furthermore, if the size only changes in the direction /orthogonal/ to the slider, then there is also no need to correct the slider position. -} posRef <- newIORef pos sizeRef <- newIORef 0 void $ Gtk.on p sizeAllocate $ \(Rectangle _ _ w h) -> do oldSz <- readIORef sizeRef oldPos <- readIORef posRef let sz = case o of Horizontal -> w Vertical -> h writeIORef sizeRef sz when (sz /= 0) $ if sz == oldSz then do -- the slider was moved; store its new position sliderPos <- get p panedPosition let newPos = fromIntegral sliderPos / fromIntegral sz writeIORef posRef newPos when (oldPos /= newPos) $ handleNewPos newPos else -- the size was changed; restore the slider position and -- save the new position set p [ panedPosition := round (oldPos * fromIntegral sz) ] return (SP p) ----------------------------- LayoutDisplay -- | A container implements 'Layout's. data LayoutDisplay = LD { mainWidget :: Bin, implWidget :: IORef (Maybe LayoutImpl), dividerCallbacks :: IORef [DividerRef -> DividerPosition -> IO ()] } -- | Tree mirroring 'Layout', which holds the layout widgets for 'LayoutDisplay' data LayoutImpl = SingleWindowI { singleWidget :: Widget } | StackI { orientationI :: Orientation, winsI :: [(LayoutImpl, RelativeSize)], stackWidget :: WeightedStack } | PairI { orientationI :: Orientation, pairFstI :: LayoutImpl, pairSndI :: LayoutImpl, divRefI :: DividerRef, pairWidget :: SlidingPair } --- construction layoutDisplayNew :: IO LayoutDisplay layoutDisplayNew = do cbRef <- newIORef [] implRef <- newIORef Nothing box <- toBin <$> alignmentNew 0 0 1 1 return (LD box implRef cbRef) -- | Registers a callback to a divider changing position. (There is -- currently no way to unregister.) layoutDisplayOnDividerMove :: LayoutDisplay -> (DividerRef -> DividerPosition -> IO ()) -> IO () layoutDisplayOnDividerMove ld cb = modifyIORef (dividerCallbacks ld) (cb:) --- changing the layout -- | Sets the layout to the given schema. -- -- * it is permissible to add or remove widgets in this process. -- -- * as an optimisation, this function will first check whether the -- layout has actually changed (so the caller need not be concerned -- with this) -- -- * will run 'widgetShowAll', and hence will show the underlying widgets too layoutDisplaySet :: LayoutDisplay -> Layout Widget -> IO () layoutDisplaySet ld lyt = do mimpl <- readIORef (implWidget ld) let applyLayout = do impl' <- buildImpl (runCb $ dividerCallbacks ld) lyt widgetShowAll (outerWidget impl') set (mainWidget ld) [containerChild := outerWidget impl'] writeIORef (implWidget ld) (Just impl') case mimpl of Nothing -> applyLayout Just impl -> unless (sameLayout impl lyt) $ do unattachWidgets (toContainer $ mainWidget ld) impl applyLayout runCb :: IORef [DividerRef -> DividerPosition -> IO ()] -> DividerRef -> DividerPosition -> IO () runCb cbRef dRef dPos = readIORef cbRef >>= mapM_ (\cb -> cb dRef dPos) buildImpl :: (DividerRef -> DividerPosition -> IO ()) -> Layout Widget -> IO LayoutImpl buildImpl cb = go where go (SingleWindow w) = return (SingleWindowI w) go (s@Stack{}) = do impls <- forM (wins s) $ \(lyt,relSize) -> (,relSize) <$> go lyt ws <- weightedStackNew (orientation s) (first outerWidget <$> impls) return (StackI (orientation s) impls ws) go (p@Pair{}) = do w1 <- go (pairFst p) w2 <- go (pairSnd p) sp <- slidingPairNew (orientation p) (outerWidget w1) (outerWidget w2) (divPos p) (cb $ divRef p) return $ PairI (orientation p) w1 w2 (divRef p) sp -- | true if the displayed layout agrees with the given schema, other -- than divider positions sameLayout :: LayoutImpl -> Layout Widget -> Bool sameLayout (SingleWindowI w) (SingleWindow w') = w == w' sameLayout (s@StackI{}) (s'@Stack{}) = orientationI s == orientation s' && length (winsI s) == length (wins s') && and (zipWith (\(impl, relSize) (layout, relSize') -> relSize == relSize' && sameLayout impl layout) (winsI s) (wins s')) sameLayout (p@PairI{}) (p'@Pair{}) = orientationI p == orientation p' && divRefI p == divRef p' && sameLayout (pairFstI p) (pairFst p') && sameLayout (pairSndI p) (pairSnd p') sameLayout _ _ = False -- removes all widgets from the layout unattachWidgets :: Container -> LayoutImpl -> IO () unattachWidgets parent (SingleWindowI w) = containerRemove parent w unattachWidgets parent s@StackI{} = do containerRemove parent (stackWidget s) mapM_ (unattachWidgets (toContainer $ stackWidget s) . fst) (winsI s) unattachWidgets parent p@PairI{} = do containerRemove parent (pairWidget p) mapM_ (unattachWidgets (toContainer $ pairWidget p)) [pairFstI p, pairSndI p] -- extract the main widget from the tree outerWidget :: LayoutImpl -> Widget outerWidget s@SingleWindowI{} = singleWidget s outerWidget s@StackI{} = toWidget . stackWidget $ s outerWidget p@PairI{} = toWidget . pairWidget $ p instance WidgetLike LayoutDisplay where baseWidget = toWidget . mainWidget ---------------- MiniwindowDisplay data MiniwindowDisplay = MD { mwdMainWidget :: VBox, mwdWidgets :: IORef [Widget] } miniwindowDisplayNew :: IO MiniwindowDisplay miniwindowDisplayNew = do vb <- vBoxNew False 1 wsRef <- newIORef [] return (MD vb wsRef) instance WidgetLike MiniwindowDisplay where baseWidget = toWidget . mwdMainWidget miniwindowDisplaySet :: MiniwindowDisplay -> [Widget] -> IO () miniwindowDisplaySet mwd ws = do curWs <- readIORef (mwdWidgets mwd) -- we could be more careful here, and only remove the widgets which we need to. when (ws /= curWs) $ do forM_ curWs $ containerRemove (mwdMainWidget mwd) forM_ ws $ \w -> boxPackEnd (mwdMainWidget mwd) w PackNatural 0 widgetShowAll $ mwdMainWidget mwd writeIORef (mwdWidgets mwd) ws ---------------------- SimpleNotebook data SimpleNotebook = SN { snMainWidget :: Notebook, snTabs :: IORef (Maybe (PL.PointedList (Widget, T.Text))) } instance WidgetLike SimpleNotebook where baseWidget = toWidget . snMainWidget -- | Constructs an empty notebook simpleNotebookNew :: IO SimpleNotebook simpleNotebookNew = do nb <- notebookNew ts <- newIORef Nothing return (SN nb ts) -- | Sets the tabs simpleNotebookSet :: SimpleNotebook -> PL.PointedList (Widget, T.Text) -> IO () simpleNotebookSet sn ts = do curTs <- readIORef (snTabs sn) let nb = snMainWidget sn tsList = toList ts curTsList = maybe [] toList curTs -- the common case is no change at all when (curTs /= Just ts) $ do -- update the tabs, if they have changed when (fmap fst curTsList /= fmap fst tsList) $ do forM_ curTsList $ const (notebookRemovePage nb (-1)) forM_ tsList $ uncurry (notebookAppendPage nb) -- now update the titles if they have changed forM_ tsList $ \(w,s) -> update nb (notebookChildTabLabel w) s -- now set the focus p <- notebookPageNum nb (fst $ PL._focus ts) maybe (return ()) (update nb notebookPage) p -- write the new status writeIORef (snTabs sn) (Just ts) -- display! widgetShowAll nb -- | The 'onSwitchPage' callback simpleNotebookOnSwitchPage :: SimpleNotebook -> (Int -> IO ()) -> IO () simpleNotebookOnSwitchPage sn = void . (snMainWidget sn `on` switchPage) ------------------- Utils -- Only set an attribute if has actually changed. -- This makes setting window titles much faster. update :: (Eq a) => o -> ReadWriteAttr o a a -> a -> IO () update w attr val = do oldVal <- get w attr when (val /= oldVal) $ set w [attr := val]