xmonad-contrib/XMonad/Layout/LayoutBuilder.hs
Joan Milev f732082fdc Remove all derivations of Typeable
Typeable has been automatically derived for every type since GHC 7.10,
so remove these obsolete derivations.  This also allows us to get rid of
the `DeriveDataTypeable` pragma quite naturally.

Related: https://github.com/xmonad/xmonad/pull/299 (xmonad/xmonad@9e5b16ed8a)
Related: bd5b969d9ba24236c0d5ef521c0397390dbc4b37
Fixes: https://github.com/xmonad/xmonad-contrib/issues/548
2021-06-18 14:10:23 +02:00

462 lines
22 KiB
Haskell

{-# 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 XMonad
import XMonad.Prelude (foldM, (<|>), isJust, fromMaybe, isNothing, listToMaybe)
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.
newtype IncLayoutN = IncLayoutN Int
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, Typeable 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 <$> 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