packages feed

taffybar-0.3.0: src/System/Taffybar/WindowSwitcher.hs

-----------------------------------------------------------------------------
-- |
-- Module      : System.Taffybar.WindowSwitcher
-- Copyright   : (c) José A. Romero L.
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : José A. Romero L. <escherdragon@gmail.com>
-- Stability   : unstable
-- Portability : unportable
--
-- Text widget that shows the title of the currently focused window and
-- that, when clicked with the mouse, displays a pop-up with a list of all
-- currently open windows that allows to switch to any of them.
--
-- N.B. If you're just looking for a drop-in replacement for the
-- "System.Taffybar.XMonadLog" widget that is clickable and doesn't require
-- DBus, you may want to see first "System.Taffybar.TaffyPager".
--
-----------------------------------------------------------------------------

module System.Taffybar.WindowSwitcher (
  -- * Usage
  -- $usage
  windowSwitcherNew
) where

import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.IORef
import Graphics.UI.Gtk
import Graphics.UI.Gtk.ModelView as M
import Graphics.X11.Xlib.Extras (Event)
import System.Information.EWMHDesktopInfo
import System.Taffybar.Pager
import System.Taffybar.Widgets.Util

-- $usage
--
-- This widget requires that the EwmhDesktops hook from the XMonadContrib
-- project be installed in your @xmonad.hs@ file:
--
-- > import XMonad.Hooks.EwmhDesktops (ewmh)
-- > main = do
-- >   xmonad $ ewmh $ defaultConfig
-- > ...
--
-- Once you've properly configured @xmonad.hs@, you can use the widget in
-- your @taffybar.hs@ file:
--
-- > import System.Taffybar.WindowSwitcher
-- > main = do
-- >   pager <- pagerNew defaultPagerConfig
-- >   let wnd = windowSwitcherNew pager
--
-- now you can use @wnd@ as any other Taffybar widget.

-- | Create a new WindowSwitcher widget that will use the given Pager as
-- its source of events.
windowSwitcherNew :: Pager -> IO Widget
windowSwitcherNew pager = do
  label <- labelNew Nothing
  let cfg = config pager
      callback = pagerCallback cfg label
  subscribe pager callback "_NET_ACTIVE_WINDOW"
  assembleWidget label

-- | Build a suitable callback function that can be registered as Listener
-- of "_NET_ACTIVE_WINDOW" standard events. It will keep track of the
-- currently focused window.
pagerCallback :: PagerConfig -> Label -> Event -> IO ()
pagerCallback cfg label _ = do
  title <- withDefaultCtx getActiveWindowTitle
  let decorate = activeWindow cfg
  postGUIAsync $ labelSetMarkup label (decorate title)

-- | Build the graphical representation of the widget.
assembleWidget :: Label -> IO Widget
assembleWidget l = do
  box <- eventBoxNew
  containerAdd box l
  eventBoxSetVisibleWindow box False
  ref <- newIORef []
  _ <- on box buttonPressEvent $ onClick [SingleClick] (toggleSelector l ref)
  widgetShowAll box
  return (toWidget box)

-- | Either create a new pop-up window (aka "selector") if none is
-- currently present, or destroy the one being currently displayed.
toggleSelector :: Label -- ^ Parent of the pop-up window to create.
               -> IORef [Window] -- ^ Last created pop-up window (if any)
               -> IO Bool
toggleSelector label ref = do
  win <- readIORef ref
  case win of
    x:_  -> killSelector x ref
    []   -> do
      selector <- createSelector ref
      case selector of
        Just sel -> do
          title <- labelGetText label
          attachPopup label title sel
          displayPopup label sel
        Nothing -> return ()
  return True

formatWindow :: [String] -> ((Int, String, a), b) -> String
formatWindow wsNames ((ws, wtitle, _), _) = wsName ++ ": " ++ wtitle
  where wsName = if 0 <= ws && ws < length wsNames
                 then wsNames !! ws
                 else "WS#" ++ show ws

-- | Build a new pop-up containing the titles of all currently open
-- windows, and assign it as a singleton list to the given IORef.
createSelector :: IORef [Window] -> IO (Maybe Window)
createSelector ref = do
  handles  <- withDefaultCtx getWindowHandles
  if null handles then return Nothing else do
    selector <- windowNew
    wsNames  <- withDefaultCtx getWorkspaceNames
    list     <- listStoreNew $ map (formatWindow wsNames) handles
    view     <- makeTreeView list
    column   <- makeColumn list

    _ <- M.treeViewAppendColumn view column
    sel <- M.treeViewGetSelection view
    _ <- M.onSelectionChanged sel $ do
      handlePick sel list handles
      killSelector selector ref
    set selector [ containerChild := view ]
    _ <- on selector deleteEvent $ killSelector selector ref >> return False
    _ <- on selector focusOutEvent $ killSelector selector ref >> return False

    writeIORef ref [selector]
    return (Just selector)

-- | Destroy given pop-up and clean-up the given IORef.
killSelector :: (MonadIO m) => Window -> IORef[Window] -> m ()
killSelector window ref = liftIO $ do
  writeIORef ref []
  postGUIAsync (widgetDestroy window)

-- | Build a new TreeView from the given ListStore containing window
-- titles.
makeTreeView :: ListStore String -> IO TreeView
makeTreeView list = do
  treeview <- M.treeViewNewWithModel list
  M.treeViewSetHeadersVisible treeview False
  return treeview

-- | Build a new TreeViewColumn from the given ListStore containing window
-- titles.
makeColumn :: ListStore String -> IO TreeViewColumn
makeColumn list = do
  col <- M.treeViewColumnNew
  renderer <- M.cellRendererTextNew
  M.cellLayoutPackStart col renderer False
  M.cellLayoutSetAttributes col renderer list $ \ind -> [M.cellText := ind]
  return col

-- | Switch to the window selected by the user in the pop-up.
handlePick :: M.TreeSelection -- ^ Pop-up selection
           -> ListStore String -- ^ List of all available windows
           -> [((Int, String, String), X11Window)] -- ^ workspace, window title, window class and window ID
           -> IO ()
handlePick selection _ handles = do
  row <- M.treeSelectionGetSelectedRows selection
  let idx = head (head row)
      wh = snd (handles !! idx)
  withDefaultCtx (focusWindow wh)
  return ()