{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ScopedTypeVariables #-} ----------------------------------------------------------------------------- -- | -- Module : XMonad.Layout.LayoutBuilder -- -- Copyright : (c) 2009 Anders Engstrom <ankaan@gmail.com>, -- 2011 Ilya Portnov <portnov84@rambler.ru>, -- 2015 Peter Jones <pjones@devalot.com> -- -- License : BSD3-style (see LICENSE) -- -- Maintainer : Anders Engstrom <ankaan@gmail.com>, -- Ilya Portnov <portnov84@rambler.ru>, -- Peter Jones <pjones@devalot.com> -- -- Stability : unstable -- Portability : unportable -- -- A layout combinator that sends a specified number of windows to one rectangle -- and the rest to another. Each of these rectangles are given a layout that -- is used within them. This can be chained to provide an arbitrary number of -- rectangles. The layout combinator allows overlapping rectangles, but such -- layouts does not work well together with hinting -- ("XMonad.Layout.LayoutHints", "XMonad.Layout.HintedGrid" etc.) -- ----------------------------------------------------------------------------- module XMonad.Layout.LayoutBuilder ( -- * Usage -- $usage layoutN, layoutR, layoutP, layoutAll, -- * Selecting Windows -- $selectWin Predicate (..), Proxy(..), -- * Messages IncLayoutN (..), -- * Utilities SubMeasure (..), SubBox (..), absBox, relBox, LayoutB, LayoutN, ) where -------------------------------------------------------------------------------- import Control.Applicative ((<|>)) import Control.Monad (foldM) import Data.Maybe import XMonad import qualified XMonad.StackSet as W import XMonad.Util.WindowProperties -------------------------------------------------------------------------------- -- $usage -- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@: -- -- > import XMonad.Layout.LayoutBuilder -- -- Then edit your @layoutHook@ by adding something like: -- -- > myLayout = ( (layoutN 1 (relBox 0 0 0.5 1) (Just $ relBox 0 0 1 1) $ simpleTabbed) -- > $ (layoutAll (relBox 0.5 0 1 1) $ simpleTabbed) -- > ) ||| -- > ( (layoutN 1 (relBox (1/3) 0 (1/2) 1) (Just $ relBox 0 0 1 1) $ Tall 0 0.01 0.5) -- > $ (layoutR 0.1 0.5 (relBox (2/3) 0 1 1) Nothing $ Tall 0 0.01 0.5) -- > $ (layoutAll (relBox 0 0 (1/3) 1) $ Tall 0 0.01 0.5) -- > ) ||| -- > ( (layoutN 1 (absBox (-512-200) 0 512 0) (Just $ relBox 0 0 1 1) $ simpleTabbed) -- > $ (layoutN 1 (absBox (-200) 0 0 0) Nothing $ simpleTabbed) -- > $ (layoutAll (absBox 0 0 (-512-200) 0) $ simpleTabbed) -- > ) ||| -- > ( (layoutN 1 (absBox 10 0 0 (-10)) Nothing $ Tall 0 0.01 0.5) -- > $ (layoutN 1 (absBox 0 0 200 0) Nothing $ Tall 0 0.01 0.5) -- > $ (layoutAll (absBox 10 10 0 0) $ Tall 2 0.01 0.5) -- > ) ||| Full ||| etc... -- > main = xmonad def { layoutHook = myLayout } -- -- This will produce a layout similar to DragPane, but with the possibility to have multiple windows in the left half -- and tabs that show the available windows. It will also produce a layout similar to ThreeColMid and a special layout -- created for use with a 80 columns wide Emacs window, its sidebar and a tabbed area for all other windows. -- -- The final layout is for applications that use a toolbar in a separate window, shown on a low resolution screen. It has -- a master area that cover almost the whole screen. It leaves 10 px to the left and 10 px at the bottom. To the left -- the toolbar is located and can be accessed by focusing this area. It is actually 200 px wide, but usually below the -- other windows. Similarly all other windows are tiled, but behind the master window and can be accessed by moving the -- mouse to the bottom of the screen. Everything can also be accessed by the standard focus changing key bindings. -- -- This module can be used to create many different custom layouts, but there are limitations. The primary limitation -- can be observed in the second and third example when there are only two columns with windows in them. The leftmost -- area is left blank. These blank areas can be avoided by placing the rectangles appropriately. -- -- These examples require "XMonad.Layout.Tabbed". -- -- For more detailed instructions on editing the layoutHook see: -- -- "XMonad.Doc.Extending#Editing_the_layout_hook" -- -- You may wish to add the following keybindings: -- -- > , ((modm .|. shiftMask, xK_h ), sendMessage $ IncLayoutN (-1)) -- > , ((modm .|. shiftMask, xK_l ), sendMessage $ IncLayoutN 1) -- -- For detailed instruction on editing the key binding see: -- -- "XMonad.Doc.Extending#Editing_key_bindings". -------------------------------------------------------------------------------- -- $selectWin -- -- 'Predicate' exists because layouts are required to be serializable, and -- "XMonad.Util.WindowProperties" is not sufficient (for example it does not -- allow using regular expressions). -- -- compare "XMonad.Util.Invisible" -- | Type class for predicates. This enables us to manage not only Windows, -- but any objects, for which instance Predicate is defined. -- -- Another instance exists in XMonad.Util.WindowPropertiesRE in xmonad-extras class Predicate p w where alwaysTrue :: Proxy w -> p -- ^ A predicate that is always True. checkPredicate :: p -> w -> X Bool -- ^ Check if given object (window or smth else) matches that predicate instance Predicate () a where alwaysTrue _ = () checkPredicate _ _ = return True instance Predicate Property Window where alwaysTrue _ = Const True checkPredicate = hasProperty -------------------------------------------------------------------------------- -- | Contains no actual data, but is needed to help select the correct instance -- of 'Predicate' data Proxy a = Proxy -------------------------------------------------------------------------------- -- | Information about how to split windows between layouts. data Limit p = LimitN Int -- ^ See: 'layoutN'. | LimitR (Rational, Rational) -- ^ See: 'layoutR'. | LimitP p -- ^ See: 'layoutP'. deriving (Show, Read) -------------------------------------------------------------------------------- -- | Use one layout in the specified area for a number of windows and -- possibly let another layout handle the rest. data LayoutB l1 l2 p a = LayoutB { subFocus :: Maybe a -- ^ The focused window in this layout. , nextFocus :: Maybe a -- ^ The focused window in the next layout. , limit :: Limit p -- ^ How to split windows between layouts. , box :: SubBox -- ^ Normal size of layout. , mbox :: Maybe SubBox -- ^ Size of layout when handling all windows. , sub :: l1 a -- ^ The layout to use in this box. , next :: Maybe (l2 a) -- ^ The next layout in the chain. } deriving (Show, Read) -------------------------------------------------------------------------------- -- | A variant of 'LayoutB' that can't use 'layoutP'. For backwards -- compatibility with previous versions of LayoutBuilder. type LayoutN l1 l2 a = LayoutB l1 l2 () a -------------------------------------------------------------------------------- -- | Use the specified layout in the described area for N windows and -- send the rest of the windows to the next layout in the chain. It -- is possible to supply an alternative area that will then be used -- instead, if there are no windows to send to the next layout. layoutN :: (Read a, Eq a, LayoutClass l1 a, LayoutClass l2 a, LayoutClass l3 a) => Int -- ^ The number of windows to handle -> SubBox -- ^ The box to place the windows in -> Maybe SubBox -- ^ Possibly an alternative box that is used when this layout handles all windows that are left -> l1 a -- ^ The layout to use in the specified area -> LayoutB l2 l3 p a -- ^ Where to send the remaining windows -> LayoutB l1 (LayoutB l2 l3 p) () a -- ^ The resulting layout layoutN num box mbox sub next = LayoutB Nothing Nothing (LimitN num) box mbox sub (Just next) -- | As layoutN, but the number of windows is given relative to the total number of windows remaining to be handled. The first -- argument is how much to change the ratio when using IncLayoutN, and the second is the initial ratio. layoutR :: (Read a, Eq a, LayoutClass l1 a, LayoutClass l2 a, LayoutClass l3 a) => Rational -- ^ How much to change the ratio with each IncLayoutN -> Rational -- ^ The ratio of the remaining windows to handle -> SubBox -- ^ The box to place the windows in -> Maybe SubBox -- ^ Possibly an alternative box that is used when this layout handles all windows that are left -> l1 a -- ^ The layout to use in the specified area -> LayoutB l2 l3 p a -- ^ Where to send the remaining windows -> LayoutB l1 (LayoutB l2 l3 p) p a -- ^ The resulting layout layoutR numdiff num box mbox sub next = LayoutB Nothing Nothing (LimitR (numdiff,num)) box mbox sub (Just next) -------------------------------------------------------------------------------- -- | Use the specified layout in the described area windows that match -- given predicate and send the rest of the windows to the next layout -- in the chain. It is possible to supply an alternative area that -- will then be used instead, if there are no windows to send to the -- next layout. layoutP :: (Read a, Eq a, LayoutClass l1 a, LayoutClass l2 a, LayoutClass l3 a, Predicate p a, Predicate p' a) => p -- ^ The predicate to use -> SubBox -- ^ The box to place the windows in -> Maybe SubBox -- ^ Possibly an alternative box that is used when this layout handles all windows that are left -> l1 a -- ^ The layout to use in the specified area -> LayoutB l2 l3 p' a -- ^ Where to send the remaining windows -> LayoutB l1 (LayoutB l2 l3 p') p a -- ^ The resulting layout layoutP prop box mbox sub next = LayoutB Nothing Nothing (LimitP prop) box mbox sub (Just next) -------------------------------------------------------------------------------- -- | Use the specified layout in the described area for all remaining windows. layoutAll :: (Read a, Eq a, LayoutClass l1 a) => SubBox -- ^ The box to place the windows in -> l1 a -- ^ The layout to use in the specified area -> LayoutB l1 Full () a -- ^ The resulting layout layoutAll box sub = LayoutB Nothing Nothing (LimitR (0,1)) box Nothing sub Nothing -------------------------------------------------------------------------------- -- | Change the number of windows handled by the focused layout. data IncLayoutN = IncLayoutN Int deriving Typeable instance Message IncLayoutN -------------------------------------------------------------------------------- -- | The absolute or relative measures used to describe the area a layout should be placed in. For negative absolute values -- the total remaining space will be added. For sizes, the remaining space will also be added for zeroes. Relative values -- are applied on the remaining space after the top-left corner of the box have been removed. data SubMeasure = Abs Int | Rel Rational deriving (Show,Read) -------------------------------------------------------------------------------- -- | A box to place a layout in. The stored values are xpos, ypos, width and height. data SubBox = SubBox SubMeasure SubMeasure SubMeasure SubMeasure deriving (Show,Read) -------------------------------------------------------------------------------- -- | Create a box with only absolute measurements. If the values are negative, the total remaining space will be added. For -- sizes it will also be added for zeroes. absBox :: Int -- ^ Absolute X-Position -> Int -- ^ Absolute Y-Position -> Int -- ^ Absolute width -> Int -- ^ Absolute height -> SubBox -- ^ The resulting 'SubBox' describing the area absBox x y w h = SubBox (Abs x) (Abs y) (Abs w) (Abs h) -------------------------------------------------------------------------------- -- | Create a box with only relative measurements. relBox :: Rational -- ^ Relative X-Position with respect to the surrounding area -> Rational -- ^ Relative Y-Position with respect to the surrounding area -> Rational -- ^ Relative width with respect to the remaining width -> Rational -- ^ Relative height with respect to the remaining height -> SubBox -- ^ The resulting 'SubBox' describing the area relBox x y w h = SubBox (Rel x) (Rel y) (Rel w) (Rel h) -------------------------------------------------------------------------------- instance ( LayoutClass l1 a, LayoutClass l2 a , Read a, Show a, Show p, Eq a, Typeable a, Predicate p a ) => LayoutClass (LayoutB l1 l2 p) a where -- | Update window locations. runLayout (W.Workspace _ LayoutB {..} s) rect = do (subs, nexts, subFocus', nextFocus') <- splitStack s limit subFocus nextFocus let selBox = if isJust nextFocus' then box else fromMaybe box mbox (sublist, sub', schange) <- handle sub subs (calcArea selBox rect) (nextlist, next', nchange) <- case next of Nothing -> return ([], Nothing, False) Just n -> do (res, l, ch) <- handle n nexts rect return (res, Just l, ch) let newlist = if length (maybe [] W.up s) < length (W.integrate' subs) then sublist++nextlist else nextlist++sublist newstate = if subFocus' /= subFocus || nextFocus' /= nextFocus || schange || nchange then Just $ LayoutB subFocus' nextFocus' limit box mbox sub' next' else Nothing return (newlist, newstate) where handle l s' r = do (res,ml) <- runLayout (W.Workspace "" l s') r return (res, fromMaybe l ml, isNothing ml) -- | Propagate messages. handleMessage l m | Just (IncLayoutN n) <- fromMessage m = incLayoutN l m n | Just (IncMasterN _) <- fromMessage m = sendFocus l m | Just Shrink <- fromMessage m = sendFocus l m | Just Expand <- fromMessage m = sendFocus l m | otherwise = sendBoth l m -- | Descriptive name for layout. description layout = case layout of (LayoutB _ _ _ _ _ sub Nothing) -> "layoutAll " ++ description sub (LayoutB _ _ (LimitN _) _ _ sub (Just next)) -> "layoutN " ++ description sub ++ " " ++ description next (LayoutB _ _ (LimitR _) _ _ sub (Just next)) -> "layoutR " ++ description sub ++ " " ++ description next (LayoutB _ _ (LimitP _) _ _ sub (Just next)) -> "layoutP " ++ description sub ++ " " ++ description next -------------------------------------------------------------------------------- -- | Increase the number of windows allowed in the focused layout. incLayoutN :: (LayoutClass l1 a, LayoutClass l2 a, Read a, Show a, Eq a, Typeable a) => LayoutB l1 l2 p a -> SomeMessage -> Int -> X (Maybe (LayoutB l1 l2 p a)) incLayoutN layout@LayoutB {..} message n = do incThis <- isFocus subFocus if incThis then return $ Just layout { limit = newLimit } else sendNext layout message where newLimit = case limit of LimitN oldnum -> LimitN (max 1 $ oldnum + n) LimitR (diff, oldnum) -> LimitR (diff, min 1 $ max 0 $ oldnum + fromIntegral n * diff) LimitP _ -> limit -------------------------------------------------------------------------------- sendSub :: (LayoutClass l1 a, LayoutClass l2 a, Read a, Show a, Eq a, Typeable a) => LayoutB l1 l2 p a -> SomeMessage -> X (Maybe (LayoutB l1 l2 p a)) sendSub (LayoutB subFocus nextFocus num box mbox sub next) m = do sub' <- handleMessage sub m return $ if isJust sub' then Just $ LayoutB subFocus nextFocus num box mbox (fromMaybe sub sub') next else Nothing -------------------------------------------------------------------------------- sendBoth :: (LayoutClass l1 a, LayoutClass l2 a, Read a, Show a, Eq a, Typeable a) => LayoutB l1 l2 p a -> SomeMessage -> X (Maybe (LayoutB l1 l2 p a)) sendBoth l@(LayoutB _ _ _ _ _ _ Nothing) m = sendSub l m sendBoth (LayoutB subFocus nextFocus num box mbox sub (Just next)) m = do sub' <- handleMessage sub m next' <- handleMessage next m return $ if isJust sub' || isJust next' then Just $ LayoutB subFocus nextFocus num box mbox (fromMaybe sub sub') (next' <|> Just next) else Nothing -------------------------------------------------------------------------------- sendNext :: (LayoutClass l1 a, LayoutClass l2 a, Read a, Show a, Eq a, Typeable a) => LayoutB l1 l2 p a -> SomeMessage -> X (Maybe (LayoutB l1 l2 p a)) sendNext (LayoutB _ _ _ _ _ _ Nothing) _ = return Nothing sendNext (LayoutB subFocus nextFocus num box mbox sub (Just next)) m = do next' <- handleMessage next m return $ if isJust next' then Just $ LayoutB subFocus nextFocus num box mbox sub next' else Nothing -------------------------------------------------------------------------------- sendFocus :: (LayoutClass l1 a, LayoutClass l2 a, Read a, Show a, Eq a, Typeable a) => LayoutB l1 l2 p a -> SomeMessage -> X (Maybe (LayoutB l1 l2 p a)) sendFocus l@(LayoutB subFocus _ _ _ _ _ _) m = do foc <- isFocus subFocus if foc then sendSub l m else sendNext l m -------------------------------------------------------------------------------- -- | Check to see if the given window is currently focused. isFocus :: (Show a) => Maybe a -> X Bool isFocus Nothing = return False isFocus (Just w) = do ms <- (W.stack . W.workspace . W.current) `fmap` gets windowset return $ maybe False (\s -> show w == show (W.focus s)) ms -------------------------------------------------------------------------------- calcNum :: Int -> Limit p -> Int calcNum tot num = max 1 $ case num of LimitN i -> i LimitR (_,r) -> ceiling $ r * fromIntegral tot LimitP _ -> 1 -------------------------------------------------------------------------------- -- | Split given list of objects (i.e. windows) using predicate. splitBy :: (Predicate p a) => p -> [a] -> X ([a], [a]) splitBy prop = foldM step ([], []) where step (good, bad) w = do ok <- checkPredicate prop w return $ if ok then (w:good, bad) else (good, w:bad) -------------------------------------------------------------------------------- splitStack :: forall a p. (Eq a, Predicate p a) => Maybe (W.Stack a) -- ^ Window set. -> Limit p -- ^ How to split the stack. -> Maybe a -- ^ The window that was focused in this layout. -> Maybe a -- ^ The window that was focused in the next layout. -> X (Maybe (W.Stack a), Maybe (W.Stack a), Maybe a, Maybe a) splitStack Nothing _ _ _ = return (Nothing, Nothing, Nothing, Nothing) splitStack (Just s) limit subFocus nextFocus = case limit of LimitN _ -> splitN LimitR _ -> splitN LimitP prop -> splitP prop where ws = W.integrate s n = calcNum (length ws) limit subl = take n ws nextl = drop n ws subFocus' xs = foc xs subFocus nextFocus' xs = foc xs nextFocus -- Pick a new focused window if necessary. foc :: [a] -> Maybe a -> Maybe a foc [] _ = Nothing foc l f | W.focus s `elem` l = Just (W.focus s) | maybe False (`elem` l) f = f | otherwise = listToMaybe l -- Split based on max number of windows. splitN = return ( differentiate' (subFocus' subl) subl , differentiate' (nextFocus' nextl) nextl , subFocus' subl , nextFocus' nextl ) -- Split based on a predicate. splitP prop = do (this, other) <- splitBy prop ws return ( differentiate' (subFocus' this) this , differentiate' (nextFocus' other) other , subFocus' this , nextFocus' other ) -------------------------------------------------------------------------------- calcArea :: SubBox -> Rectangle -> Rectangle calcArea (SubBox xpos ypos width height) rect = Rectangle (rect_x rect + fromIntegral xpos') (rect_y rect + fromIntegral ypos') width' height' where xpos' = calc False xpos $ rect_width rect ypos' = calc False ypos $ rect_height rect width' = calc True width $ rect_width rect - xpos' height' = calc True height $ rect_height rect - ypos' calc zneg val tot = fromIntegral $ min (fromIntegral tot) $ max 0 $ case val of Rel v -> floor $ v * fromIntegral tot Abs v -> if v<0 || (zneg && v==0) then fromIntegral tot + v else v -------------------------------------------------------------------------------- differentiate' :: Eq q => Maybe q -> [q] -> Maybe (W.Stack q) differentiate' _ [] = Nothing differentiate' Nothing w = W.differentiate w differentiate' (Just f) w | f `elem` w = Just W.Stack { W.focus = f , W.up = reverse $ takeWhile (/=f) w , W.down = tail $ dropWhile (/=f) w } | otherwise = W.differentiate w