{-# LANGUAGE TypeFamilies, DeriveDataTypeable, TypeSynonymInstances #-} module Phi.Widgets.Taskbar ( IconStyle , idIconStyle , desaturateIconStyle , TaskStyle(..) , DesktopStyle(..) , TaskbarConfig(..) , defaultTaskbarConfig , taskbar ) where import Control.Concurrent import Control.Monad import Control.Monad.State import Control.Monad.Trans import Data.Array.MArray import Data.Bits import Data.Char import Data.Function import Data.List import Data.Maybe import Data.Typeable import Data.Word import qualified Data.Map as M import Foreign.C.Types import Graphics.Rendering.Cairo import Graphics.Rendering.Pango.Cairo import Graphics.Rendering.Pango.Enums (PangoRectangle(..)) import Graphics.Rendering.Pango.Layout import Graphics.Rendering.Pango.Font import Graphics.X11.Xlib (Window) import qualified Graphics.X11.Xlib as Xlib import qualified Graphics.X11.Xlib.Extras as XExtras import Codec.Binary.UTF8.String import Phi.Phi import Phi.Types import Phi.Border import Phi.Widget import Phi.X11.Atoms type IconStyle = Surface -> Render () instance Show IconStyle where show _ = "IconStyle " idIconStyle :: IconStyle idIconStyle = flip withPatternForSurface setSource desaturateIconStyle :: Double -> IconStyle desaturateIconStyle v icon = do w <- imageSurfaceGetWidth icon h <- imageSurfaceGetHeight icon renderWithSimilarSurface ContentColorAlpha w h $ \surface -> do renderWith surface $ do setOperator OperatorAdd withPatternForSurface icon setSource paint setSourceRGB 0 0 0 paint setOperator OperatorHslSaturation setSourceRGBA 0 0 0 (1-v) paint setOperator OperatorDestIn withPatternForSurface icon setSource paint withPatternForSurface surface setSource downscaled :: Double -> Surface -> Render () downscaled s surface = do case True of _ | s < 0.5 -> do w <- imageSurfaceGetWidth surface h <- imageSurfaceGetHeight surface renderWithSimilarSurface ContentColorAlpha (ceiling (fromIntegral w*s)) (ceiling (fromIntegral h*s)) $ \surface' -> do renderWith surface' $ do scale 0.5 0.5 downscaled (2*s) surface paint withPatternForSurface surface' setSource | otherwise -> do scale s s withPatternForSurface surface setSource data TaskStyle = TaskStyle { taskFont :: !String , taskColor :: !Color , taskBorder :: !BorderConfig , taskIconStyle :: !IconStyle } deriving Show data DesktopStyle = DesktopStyle { desktopFont :: !String , desktopLabelWidth :: !Int , desktopLabelGap :: !Int , desktopColor :: !Color , desktopBorder :: !BorderConfig } deriving Show data TaskbarConfig = TaskbarConfig { taskMaxSize :: !Int , normalTaskStyle :: !TaskStyle , activeTaskStyle :: !TaskStyle , desktopStyle :: !(Maybe (DesktopStyle, DesktopStyle)) } deriving Show defaultStyle :: TaskStyle defaultStyle = TaskStyle { taskFont = "Sans 8" , taskColor = (0, 0, 0, 1) , taskBorder = defaultBorderConfig { backgroundColor = (0.75, 0.75, 0.75, 1) } , taskIconStyle = idIconStyle } defaultTaskbarConfig :: TaskbarConfig defaultTaskbarConfig = TaskbarConfig { taskMaxSize = 150 , normalTaskStyle = defaultStyle , activeTaskStyle = defaultStyle {taskBorder = defaultBorderConfig { borderColor = (1, 1, 1, 1) }} , desktopStyle = Nothing } data Taskbar = Taskbar TaskbarConfig deriving Show instance Show Surface where show _ = "Surface " data TaskbarState = TaskbarState { taskbarActiveWindow :: !Window , taskbarDesktopCount :: !Int , taskbarCurrentDesktop :: !Int , taskbarWindows :: ![Window] , taskbarWindowStates :: !(M.Map Window WindowState) , taskbarWindowIcons :: !(M.Map Window [(Int, Surface)]) , taskbarWindowScreens :: !(M.Map Window Xlib.Rectangle) } deriving Show data WindowState = WindowState { windowTitle :: !String , windowDesktop :: !Int , windowVisible :: !Bool } deriving (Show, Eq) data TaskbarMessage = WindowListUpdate [Xlib.Window] (M.Map Window WindowState) (M.Map Window [(Int, Surface)]) (M.Map Window Xlib.Rectangle) | DesktopCountUpdate Int | CurrentDesktopUpdate Int | ActiveWindowUpdate Window deriving (Show, Typeable) instance WidgetClass Taskbar where type WidgetData Taskbar = TaskbarState initWidget (Taskbar _) phi dispvar = do forkIO $ taskbarRunner phi dispvar return $ TaskbarState 0 0 (-1) [] M.empty M.empty M.empty minSize _ _ _ = 0 weight _ = 1 render (Taskbar config) TaskbarState { taskbarActiveWindow = activeWindow , taskbarDesktopCount = desktopCount , taskbarCurrentDesktop = currentDesktop , taskbarWindows = windows , taskbarWindowStates = windowStates , taskbarWindowIcons = windowIcons , taskbarWindowScreens = windowScreens } w h screen = do let screenWindows = filter ((== Just screen) . flip M.lookup windowScreens) windows desktopNumbers = take desktopCount [0..] desktops = map (\desktop -> (desktop, filter (fromMaybe False . fmap (windowOnDesktop desktop) . flip M.lookup windowStates) screenWindows)) desktopNumbers windowCount = sum $ map (length . snd) $ desktops dstyle d = fmap (if d == currentDesktop then snd else fst) $ desktopStyle config dlabelwidth d = fromMaybe 0 $ fmap desktopLabelWidth $ dstyle d gap d ds = if null (snd $ desktops !! d) then 0 else desktopLabelGap ds dleftwidth d = fromMaybe 0 $ fmap (\ds@DesktopStyle {desktopBorder = border} -> (borderLeft $ margin border) + (borderWidth border) + (borderLeft $ padding border) + dlabelwidth d + gap d ds) $ dstyle d dwidth d = fromMaybe 0 $ fmap (\ds@DesktopStyle {desktopBorder = border} -> (borderH $ margin border) + 2*(borderWidth border) + (borderH $ padding border) + dlabelwidth d + gap d ds) $ dstyle d desktopsWidth = sum $ map dwidth desktopNumbers windowWidth = if windowCount == 0 then 0 else min (taskMaxSize config) ((w - desktopsWidth) `div` windowCount) flip (flip foldM_ 0) desktops $ \nwindows (desktop, desktopWindows) -> do let dstyle' = dstyle desktop dx = dleftwidth desktop + (sum $ map dwidth $ take desktop [0..]) + nwindows*windowWidth case dstyle' of Just ds -> do let (r, g, b, a) = desktopColor ds save drawBorder (desktopBorder ds) (dx - dleftwidth desktop) 0 (dwidth desktop + windowWidth * length desktopWindows) h clip setSourceRGBA r g b a renderText (desktopFont ds) (fromIntegral (dx - dlabelwidth desktop - gap desktop ds)) 0 (dlabelwidth desktop) h $ show (desktop+1) restore _ -> return () forM_ (zip [0..] desktopWindows) $ \(i, window) -> do let style = (if window == activeWindow then activeTaskStyle else normalTaskStyle) config (r, g, b, a) = taskColor style leftBorder = (borderLeft $ margin $ taskBorder style) + (borderWidth $ taskBorder style) + (borderLeft $ padding $ taskBorder style) rightBorder = (borderRight $ margin $ taskBorder style) + (borderWidth $ taskBorder style) + (borderRight $ padding $ taskBorder style) h' = h - (borderV $ margin $ taskBorder style) mstate = M.lookup window windowStates micons = M.lookup window windowIcons x = dx + i*windowWidth case (mstate, micons) of (Just state, Just icons) -> do save drawBorder (taskBorder style) x 0 windowWidth h clip setSourceRGBA r g b a renderText (taskFont style) (fromIntegral (x + leftBorder + h' + 3)) 0 (windowWidth - leftBorder - h' - 3 - rightBorder) h $ windowTitle state restore case bestIcon h' icons of Just icon -> do save translate (fromIntegral $ x + leftBorder) (fromIntegral $ borderTop $ margin $ taskBorder style) imageW <- imageSurfaceGetWidth icon imageH <- imageSurfaceGetHeight icon let scalef = (fromIntegral h')/(fromIntegral $ max imageW imageH) renderWithSimilarSurface ContentColorAlpha h' h' $ \surface -> do renderWith surface $ do downscaled scalef icon paint taskIconStyle style surface paint restore Nothing -> return () _ -> return () return $ nwindows + length desktopWindows handleMessage _ priv m = case (fromMessage m) of Just (WindowListUpdate windows windowStates icons screens) -> priv {taskbarWindows = windows, taskbarWindowStates = windowStates, taskbarWindowIcons = icons, taskbarWindowScreens = screens} Just (DesktopCountUpdate count) -> priv {taskbarDesktopCount = count} Just (CurrentDesktopUpdate current) -> priv {taskbarCurrentDesktop = current} Just (ActiveWindowUpdate window) -> priv {taskbarActiveWindow = window} _ -> priv renderText :: String -> Int -> Int -> Int -> Int -> String -> Render () renderText font x y w h text = do layout <- createLayout "" (_, PangoRectangle _ _ textWidth textHeight) <- liftIO $ do layoutSetMarkup layout $ "" ++ (escapeMarkup text) ++ "" layoutSetWidth layout $ Just $ fromIntegral w layoutSetEllipsize layout EllipsizeEnd layoutGetExtents layout moveTo ((fromIntegral x) + ((fromIntegral w) - textWidth)/2) ((fromIntegral y) + ((fromIntegral h) - textHeight)/2) showLayout layout bestIcon :: Int -> [(Int, Surface)] -> Maybe Surface bestIcon h icons = fmap snd . listToMaybe $ sortBy compareIcons icons where compareIcons = flip (compare `on` fst) windowOnDesktop :: Int -> WindowState -> Bool windowOnDesktop desktop state = (windowVisible state) && (desktop == windowDesktop state) taskbarRunner :: Phi -> Display -> IO () taskbarRunner phi dispvar = do let screens = getScreens dispvar (windows, states, icons, windowScreens) <- liftIO $ withDisplay dispvar $ \disp -> do (windows, states, icons, windowScreens) <- getWindowStates disp screens (getAtoms dispvar) [] M.empty M.empty M.empty desktopCount <- getDesktopCount disp (getAtoms dispvar) current <- getCurrentDesktop disp (getAtoms dispvar) activeWindow <- getActiveWindow disp (getAtoms dispvar) sendMessage phi $ WindowListUpdate windows states icons windowScreens sendMessage phi $ DesktopCountUpdate desktopCount sendMessage phi $ CurrentDesktopUpdate current sendMessage phi $ ActiveWindowUpdate activeWindow return (windows, states, icons, windowScreens) sendMessage phi Repaint flip evalStateT (windows, states, icons, windowScreens) $ forever $ do m <- receiveMessage phi case (fromMessage m) of Just event -> handleEvent phi dispvar event _ -> return () handleEvent :: Phi -> Display -> XExtras.Event -> StateT ([Window], M.Map Window WindowState, M.Map Window [(Int, Surface)], M.Map Window Xlib.Rectangle) IO () handleEvent phi dispvar XExtras.PropertyEvent {XExtras.ev_atom = atom, XExtras.ev_window = window} = do let atoms = getAtoms dispvar let screens = getScreens dispvar when (elem atom $ Xlib.wM_NAME : map ($ atoms) [ atom_NET_ACTIVE_WINDOW , atom_NET_NUMBER_OF_DESKTOPS , atom_NET_CURRENT_DESKTOP , atom_NET_CLIENT_LIST , atom_NET_WM_ICON , atom_NET_WM_NAME , atom_NET_WM_DESKTOP , atom_NET_WM_STATE ]) $ withDisplay dispvar $ \disp -> do let rootwin = Xlib.defaultRootWindow disp if (window == rootwin) then do when (atom == atom_NET_ACTIVE_WINDOW atoms) $ do activeWindow <- liftIO $ getActiveWindow disp atoms sendMessage phi $ ActiveWindowUpdate activeWindow sendMessage phi Repaint when (atom == atom_NET_NUMBER_OF_DESKTOPS atoms) $ do desktopCount <- liftIO $ getDesktopCount disp atoms sendMessage phi $ DesktopCountUpdate desktopCount sendMessage phi Repaint when (atom == atom_NET_CURRENT_DESKTOP atoms) $ do current <- liftIO $ getCurrentDesktop disp atoms sendMessage phi $ CurrentDesktopUpdate current sendMessage phi Repaint when (atom == atom_NET_CLIENT_LIST atoms) $ do (windows, windowStates, icons, windowScreens) <- get (windows', windowStates', icons', windowScreens') <- liftIO $ getWindowStates disp screens atoms windows windowStates icons windowScreens when (windows /= windows') $ do sendMessage phi $ WindowListUpdate windows' windowStates' icons' windowScreens' sendMessage phi Repaint put (windows', windowStates', icons', windowScreens') else do (windows, windowStates, icons, windowScreens) <- get when (elem window windows) $ do when (atom == atom_NET_WM_ICON atoms) $ do icon <- liftIO $ getWindowIcons disp atoms window let icons' = M.insert window icon icons sendMessage phi $ WindowListUpdate windows windowStates icons' windowScreens sendMessage phi Repaint put (windows, windowStates, icons', windowScreens) when (atom /= atom_NET_WM_ICON atoms) $ do let windowState = M.lookup window windowStates windowState' <- liftIO $ getWindowState disp atoms window when (windowState /= (Just windowState')) $ do let windowStates' = M.insert window windowState' windowStates sendMessage phi $ WindowListUpdate windows windowStates' icons windowScreens sendMessage phi Repaint put (windows, windowStates', icons, windowScreens) handleEvent phi dispvar XExtras.ConfigureEvent {XExtras.ev_window = window} = do let screens = getScreens dispvar (windows, windowStates, icons, windowScreens) <- get when (elem window windows) $ withDisplay dispvar $ \disp -> do let screen = M.lookup window windowScreens screen' <- liftIO $ getWindowScreen disp screens window when (screen /= (Just screen')) $ do let windowScreens' = M.insert window screen' windowScreens sendMessage phi $ WindowListUpdate windows windowStates icons windowScreens' sendMessage phi Repaint put (windows, windowStates, icons, windowScreens') handleEvent _ _ _ = return () getDesktopCount :: Xlib.Display -> Atoms -> IO Int getDesktopCount disp atoms = liftM (fromIntegral . fromMaybe 0 . join . fmap listToMaybe) $ XExtras.getWindowProperty32 disp (atom_NET_NUMBER_OF_DESKTOPS atoms) $ Xlib.defaultRootWindow disp getCurrentDesktop :: Xlib.Display -> Atoms -> IO Int getCurrentDesktop disp atoms = liftM (fromIntegral . fromMaybe (-1) . join . fmap listToMaybe) $ XExtras.getWindowProperty32 disp (atom_NET_CURRENT_DESKTOP atoms) $ Xlib.defaultRootWindow disp getActiveWindow :: Xlib.Display -> Atoms -> IO Window getActiveWindow disp atoms = liftM (fromIntegral . fromMaybe 0 . join . fmap listToMaybe) $ XExtras.getWindowProperty32 disp (atom_NET_ACTIVE_WINDOW atoms) $ Xlib.defaultRootWindow disp getWindowStates :: Xlib.Display -> [Xlib.Rectangle] -> Atoms -> [Window] -> M.Map Window WindowState -> M.Map Window [(Int, Surface)] -> M.Map Window Xlib.Rectangle -> IO ([Window], M.Map Window WindowState, M.Map Window [(Int, Surface)], M.Map Window Xlib.Rectangle) getWindowStates disp screens atoms oldWindows windowStates windowIcons windowScreens = do windows <- getWindowList disp atoms oldWindows let windowStates' = map (\w -> (w, M.lookup w windowStates)) windows windowIcons' = map (\w -> (w, M.lookup w windowIcons)) windows windowScreens' = map (\w -> (w, M.lookup w windowScreens)) windows newWindowStates <- mapM getWindowState' windowStates' newWindowIcons <- mapM getWindowIcons' windowIcons' newWindowScreens <- mapM getWindowScreen' windowScreens' return (windows, M.fromList newWindowStates, M.fromList newWindowIcons, M.fromList newWindowScreens) where getWindowState' (window, Just windowState) = return (window, windowState) getWindowState' (window, Nothing) = do Xlib.selectInput disp window $ Xlib.propertyChangeMask .|. Xlib.structureNotifyMask windowState <- getWindowState disp atoms window return (window, windowState) getWindowIcons' (window, Just icons) = return (window, icons) getWindowIcons' (window, Nothing) = do icons <- getWindowIcons disp atoms window return (window, icons) getWindowScreen' (window, Just screen) = return (window, screen) getWindowScreen' (window, Nothing) = do screen <- getWindowScreen disp screens window return (window, screen) getWindowState :: Xlib.Display -> Atoms -> Window -> IO WindowState getWindowState disp atoms window = do netwmname <- liftM (fmap (decode . map fromIntegral)) $ XExtras.getWindowProperty8 disp (atom_NET_WM_NAME atoms) window wmname <- case netwmname of Just name -> return name Nothing -> liftM (map unsignedChr . fromMaybe []) $ XExtras.getWindowProperty8 disp Xlib.wM_NAME window workspace <- liftM (fromIntegral . fromMaybe 0xFFFFFFFF . join . fmap listToMaybe) $ XExtras.getWindowProperty32 disp (atom_NET_WM_DESKTOP atoms) window visible <- showWindow disp atoms window return $ WindowState wmname workspace visible where unsignedChr = chr . fromIntegral . (fromIntegral :: (CChar -> CUChar)) getWindowIcons :: Xlib.Display -> Atoms -> Window -> IO [(Int, Surface)] getWindowIcons disp atoms window = XExtras.getWindowProperty32 disp (atom_NET_WM_ICON atoms) window >>= readIcons . fromMaybe [] readIcons :: [CLong] -> IO [(Int, Surface)] readIcons (width:height:iconData) = do if ((fromIntegral $ length iconData) < (width*height)) then return [] else do let (thisIcon, rest) = splitAt (fromIntegral (width*height)) iconData icon <- createImageSurface FormatARGB32 (fromIntegral width) (fromIntegral height) surfaceData <- imageSurfaceGetPixels icon :: IO (SurfaceData Int Word32) forM_ (zip thisIcon [0..]) $ \(e, i) -> writeArray surfaceData i $ premultiply $ fromIntegral e surfaceMarkDirty icon moreIcons <- readIcons rest return $ (fromIntegral $ max width height, icon):moreIcons readIcons _ = return [] premultiply :: Word32 -> Word32 premultiply c = a .|. r .|. g .|. b where amask = 0xFF000000 rmask = 0x00FF0000 gmask = 0x0000FF00 bmask = 0x000000FF a = c .&. amask pm mask = (((c .&. mask) * (a `shiftR` 24)) `div` 0xFF) .&. mask r = pm rmask g = pm gmask b = pm bmask getWindowScreen :: Xlib.Display -> [Xlib.Rectangle] -> Window -> IO Xlib.Rectangle getWindowScreen disp screens window = do (_, _, _, width, height, _, _) <- Xlib.getGeometry disp window (ret, x, y, _) <- Xlib.translateCoordinates disp window (Xlib.defaultRootWindow disp) 0 0 case ret of True -> do let windowRect = Xlib.Rectangle x y width height screen = maximumBy (compare `on` unionArea windowRect) screens return screen False -> return $ head screens showWindow :: Xlib.Display -> Atoms -> Window -> IO Bool showWindow disp atoms window = do states <- liftM (map fromIntegral . fromMaybe []) $ XExtras.getWindowProperty32 disp (atom_NET_WM_STATE atoms) window transientForHint <- XExtras.getTransientForHint disp window windowType <- liftM (fromMaybe (atom_NET_WM_WINDOW_TYPE_NORMAL atoms) . fmap fromIntegral . join . fmap listToMaybe) $ XExtras.getWindowProperty32 disp (atom_NET_WM_STATE atoms) window return $ not $ or [ elem (atom_NET_WM_STATE_SKIP_TASKBAR atoms) states , transientForHint /= Nothing , elem windowType $ map ($ atoms) [ atom_NET_WM_WINDOW_TYPE_DOCK , atom_NET_WM_WINDOW_TYPE_DESKTOP , atom_NET_WM_WINDOW_TYPE_TOOLBAR , atom_NET_WM_WINDOW_TYPE_MENU , atom_NET_WM_WINDOW_TYPE_SPLASH ] ] getWindowList :: Xlib.Display -> Atoms -> [Window] -> IO [Window] getWindowList disp atoms windows = do newWindows <- liftM (map fromIntegral . join . maybeToList) $ XExtras.getWindowProperty32 disp (atom_NET_CLIENT_LIST atoms) $ Xlib.defaultRootWindow disp return $ (filter (flip elem newWindows) windows) ++ (filter (not . flip elem windows) newWindows) taskbar :: TaskbarConfig -> Widget taskbar config = do Widget $ Taskbar config