taffybar-7.0.0: src/System/Taffybar/Widget/Util.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Widget.Util
-- Copyright : (c) Ivan Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan Malison <IvanMalison@gmail.com>
-- Stability : unstable
-- Portability : unportable
--
-- Utility functions to facilitate building GTK interfaces.
module System.Taffybar.Widget.Util where
import Control.Concurrent (forkIO)
import qualified Control.Concurrent.MVar as MV
import Control.Exception.Enclosed (catchAny)
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Bifunctor (first)
import Data.Functor (($>))
import Data.GI.Base.Overloading (IsDescendantOf)
import Data.Int
import qualified Data.Text as T
import qualified GI.Gdk as D
import qualified GI.GdkPixbuf.Objects.Pixbuf as GI
import qualified GI.GdkPixbuf.Objects.Pixbuf as PB
import GI.Gtk as Gtk
import Graphics.UI.GIGtkScalingImage (scalePixbufToSize)
import Paths_taffybar (getDataDir)
import System.Environment.XDG.DesktopEntry
import System.FilePath.Posix
import System.Log.Logger (Priority (..))
import System.Taffybar.Util
import Text.Printf
-- | Common record used for window icon widgets in workspace switchers.
data WindowIconWidget a = WindowIconWidget
{ iconContainer :: Gtk.EventBox,
iconImage :: Gtk.Widget,
iconWindow :: MV.MVar (Maybe a),
iconForceUpdate :: IO ()
}
-- | Construct the GTK widgets and CSS classes for a window icon widget, leaving
-- 'iconForceUpdate' as a placeholder to be filled in after the image widget is
-- created by 'scalingImageNew'.
--
-- The caller is responsible for creating the image widget and adding it to the
-- event box.
mkWindowIconWidgetBase :: (MonadIO m) => Maybe Int32 -> m (WindowIconWidget a)
mkWindowIconWidgetBase _mSize = liftIO $ do
windowVar <- MV.newMVar Nothing
ebox <- Gtk.eventBoxNew
_ <- widgetSetClassGI ebox "window-icon-container"
placeholder <- Gtk.toWidget ebox
return
WindowIconWidget
{ iconContainer = ebox,
iconImage = placeholder,
iconWindow = windowVar,
iconForceUpdate = return ()
}
-- | List of possible status class names for window icon widgets.
--
-- This is used to keep the style context clean by removing stale classes.
possibleStatusStrings :: [T.Text]
possibleStatusStrings = ["active", "urgent", "minimized", "normal", "inactive"]
-- | Add/remove classes on a widget while removing stale classes.
updateWidgetClasses ::
(Foldable t1, Foldable t, Gtk.IsWidget a, MonadIO m) =>
a ->
t1 T.Text ->
t T.Text ->
m ()
updateWidgetClasses widget toAdd toRemove = do
context <- Gtk.widgetGetStyleContext widget
let hasClass = Gtk.styleContextHasClass context
addIfMissing klass =
hasClass klass >>= (`when` Gtk.styleContextAddClass context klass) . not
removeIfPresent klass =
unless (klass `elem` toAdd) $
hasClass klass >>= (`when` Gtk.styleContextRemoveClass context klass)
mapM_ removeIfPresent toRemove
mapM_ addIfMissing toAdd
-- | Update a 'WindowIconWidget' with new per-slot data.
updateWindowIconWidgetState ::
(MonadIO m) =>
WindowIconWidget a ->
Maybe a ->
(a -> T.Text) ->
(a -> T.Text) ->
m ()
updateWindowIconWidgetState iconWidget windowData titleFn statusFn = do
_ <- liftIO $ MV.swapMVar (iconWindow iconWidget) windowData
Gtk.widgetSetTooltipText (iconContainer iconWidget) (titleFn <$> windowData)
liftIO $ iconForceUpdate iconWidget
let statusString = maybe "inactive" statusFn windowData
updateWidgetClasses
(iconContainer iconWidget)
[statusString]
possibleStatusStrings
-- | Wrap an icon getter to scale returned pixbufs to the requested size.
scaledPixbufGetter ::
(MonadIO m) =>
(Int32 -> a -> m (Maybe GI.Pixbuf)) ->
(Int32 -> a -> m (Maybe GI.Pixbuf))
scaledPixbufGetter getter size windowData =
getter size windowData
>>= traverse (liftIO . scalePixbufToSize size Gtk.OrientationHorizontal)
-- | Catch exceptions from a pixbuf getter and log them.
handlePixbufGetterException ::
(MonadBaseControl IO m, Show a) =>
(Priority -> String -> m ()) ->
(Int32 -> a -> m (Maybe GI.Pixbuf)) ->
Int32 ->
a ->
m (Maybe GI.Pixbuf)
handlePixbufGetterException logFn getter size windowData =
catchAny (getter size windowData) $ \e -> do
_ <-
logFn WARNING $
printf
"Failed to get window icon for %s: %s"
(show windowData)
(show e)
return Nothing
-- | Execute the given action as a response to any of the given types
-- of mouse button clicks.
onClick ::
-- | Types of button clicks to listen to.
[D.EventType] ->
-- | Action to execute.
IO a ->
D.EventButton ->
IO Bool
onClick triggers action btn = do
click <- D.getEventButtonType btn
if click `elem` triggers
then action >> return True
else return False
-- | Attach the given widget as a popup with the given title to the
-- given window. The newly attached popup is not shown initially. Use
-- the 'displayPopup' function to display it.
attachPopup ::
(Gtk.IsWidget w, Gtk.IsWindow wnd) =>
-- | The widget to set as popup.
w ->
-- | The title of the popup.
T.Text ->
-- | The window to attach the popup to.
wnd ->
IO ()
attachPopup widget title window = do
windowSetTitle window title
windowSetTypeHint window D.WindowTypeHintTooltip
windowSetSkipTaskbarHint window True
windowSetSkipPagerHint window True
transient <- getWindow
windowSetTransientFor window transient
windowSetKeepAbove window True
windowStick window
where
getWindow :: IO (Maybe Window)
getWindow = do
windowGType <- glibType @Window
Just ancestor <- Gtk.widgetGetAncestor widget windowGType
castTo Window ancestor
-- | Display the given popup widget (previously prepared using the
-- 'attachPopup' function) immediately beneath (or above) the given
-- window.
displayPopup ::
(Gtk.IsWidget w, Gtk.IsWidget wnd, Gtk.IsWindow wnd) =>
-- | The popup widget.
w ->
-- | The window the widget was attached to.
wnd ->
IO ()
displayPopup widget window = do
windowSetPosition window WindowPositionMouse
(x, y) <- windowGetPosition window
(_, natReq) <- widgetGetPreferredSize =<< widgetGetToplevel widget
y' <- getRequisitionHeight natReq
widgetShowAll window
if y > y'
then windowMove window x (y - y')
else windowMove window x y'
-- | Read current allocated widget width and height.
widgetGetAllocatedSize ::
(Gtk.IsWidget self, MonadIO m) =>
self -> m (Int, Int)
widgetGetAllocatedSize widget = do
w <- Gtk.widgetGetAllocatedWidth widget
h <- Gtk.widgetGetAllocatedHeight widget
return (fromIntegral w, fromIntegral h)
-- | Creates markup with the given foreground and background colors and the
-- given contents.
colorize ::
-- | Foreground color.
String ->
-- | Background color.
String ->
-- | Contents.
String ->
String
colorize fg bg = printf "<span%s%s>%s</span>" (attr ("fg" :: String) fg :: String) (attr ("bg" :: String) bg :: String)
where
attr name value
| null value = ""
| otherwise = printf " %scolor=\"%s\"" name value
-- | Run an action forever on a background thread.
backgroundLoop :: IO a -> IO ()
backgroundLoop = void . forkIO . forever
-- | Register an action on widget realization and return the widget.
drawOn :: (Gtk.IsWidget object) => object -> IO () -> IO object
drawOn drawArea action = Gtk.onWidgetRealize drawArea action $> drawArea
-- | Add a CSS class to a widget and return it.
widgetSetClassGI :: (Gtk.IsWidget b, MonadIO m) => b -> T.Text -> m b
widgetSetClassGI widget klass =
Gtk.widgetGetStyleContext widget
>>= flip Gtk.styleContextAddClass klass
>> return widget
-- | Standard icon-theme load flags used by icon helpers.
themeLoadFlags :: [Gtk.IconLookupFlags]
themeLoadFlags =
[ Gtk.IconLookupFlagsGenericFallback,
Gtk.IconLookupFlagsUseBuiltin
]
-- | Resolve and load an icon for a desktop entry.
getImageForDesktopEntry :: Int32 -> DesktopEntry -> IO (Maybe GI.Pixbuf)
getImageForDesktopEntry size de = getImageForMaybeIconName (T.pack <$> deIcon de) size
-- | Resolve and load an icon from an optional icon name.
getImageForMaybeIconName :: Maybe T.Text -> Int32 -> IO (Maybe GI.Pixbuf)
getImageForMaybeIconName mIconName size =
join <$> traverse (`getImageForIconName` size) mIconName
-- | Resolve and load an icon by theme name or filesystem path.
getImageForIconName :: T.Text -> Int32 -> IO (Maybe GI.Pixbuf)
getImageForIconName iconName size =
maybeTCombine
(loadPixbufByName size iconName)
( getPixbufFromFilePath (T.unpack iconName)
>>= traverse (scalePixbufToSize size Gtk.OrientationHorizontal)
)
-- | Load an icon pixbuf from the current icon theme by symbolic name.
loadPixbufByName :: Int32 -> T.Text -> IO (Maybe GI.Pixbuf)
loadPixbufByName size name = do
iconTheme <- Gtk.iconThemeGetDefault
hasIcon <- Gtk.iconThemeHasIcon iconTheme name
if hasIcon
then Gtk.iconThemeLoadIcon iconTheme name size themeLoadFlags
else return Nothing
-- | Center a widget in both axes.
alignCenter :: (Gtk.IsWidget o, MonadIO m) => o -> m ()
alignCenter widget =
Gtk.setWidgetValign widget Gtk.AlignCenter
>> Gtk.setWidgetHalign widget Gtk.AlignCenter
-- | Make a widget vertically fill and horizontally centered.
vFillCenter :: (Gtk.IsWidget o, MonadIO m) => o -> m ()
vFillCenter widget =
Gtk.widgetSetVexpand widget True
>> Gtk.setWidgetValign widget Gtk.AlignFill
>> Gtk.setWidgetHalign widget Gtk.AlignCenter
-- | Load and scale a pixbuf from file to a target height.
pixbufNewFromFileAtScaleByHeight :: Int32 -> String -> IO (Either String PB.Pixbuf)
pixbufNewFromFileAtScaleByHeight height name =
fmap (handleResult . first show) $
catchGErrorsAsLeft $
PB.pixbufNewFromFileAtScale name (-1) height True
where
handleResult = (maybe (Left "gdk function returned NULL") Right =<<)
-- | Load an icon shipped with taffybar's data files.
loadIcon :: Int32 -> String -> IO (Either String PB.Pixbuf)
loadIcon height name =
getDataDir
>>= pixbufNewFromFileAtScaleByHeight height . (</> "icons" </> name)
-- | Set a minimum widget width while preserving natural height.
setMinWidth :: (Gtk.IsWidget w, MonadIO m) => Int -> w -> m w
setMinWidth width widget = liftIO $ do
Gtk.widgetSetSizeRequest widget (fromIntegral width) (-1)
return widget
-- | Add a CSS class if it is not already present.
addClassIfMissing ::
(IsDescendantOf Widget a, MonadIO m, GObject a) => T.Text -> a -> m ()
addClassIfMissing klass widget = do
context <- Gtk.widgetGetStyleContext widget
Gtk.styleContextHasClass context klass
>>= (`when` Gtk.styleContextAddClass context klass) . not
-- | Remove a CSS class when it is currently present.
removeClassIfPresent ::
(IsDescendantOf Widget a, MonadIO m, GObject a) => T.Text -> a -> m ()
removeClassIfPresent klass widget = do
context <- Gtk.widgetGetStyleContext widget
Gtk.styleContextHasClass context klass
>>= (`when` Gtk.styleContextRemoveClass context klass)
-- | Wrap a widget with two container boxes. The inner box will have the class
-- "inner-pad", and the outer box will have the class "outer-pad". These boxes
-- can be used to add padding between the outline of the widget and its
-- contents, or for the purpose of displaying a different background behind the
-- widget.
buildPadBox :: (MonadIO m) => Gtk.Widget -> m Gtk.Widget
buildPadBox contents = liftIO $ do
innerBox <- Gtk.boxNew Gtk.OrientationHorizontal 0
outerBox <- Gtk.boxNew Gtk.OrientationHorizontal 0
Gtk.setWidgetValign innerBox Gtk.AlignFill
Gtk.setWidgetValign outerBox Gtk.AlignFill
Gtk.containerAdd innerBox contents
Gtk.containerAdd outerBox innerBox
_ <- widgetSetClassGI innerBox "inner-pad"
_ <- widgetSetClassGI outerBox "outer-pad"
Gtk.widgetShow outerBox
Gtk.widgetShow innerBox
Gtk.toWidget outerBox
-- | Wrap a widget in a @contents@ box and then in the standard pad boxes.
buildContentsBox :: (MonadIO m) => Gtk.Widget -> m Gtk.Widget
buildContentsBox widget = liftIO $ do
contents <- Gtk.boxNew Gtk.OrientationHorizontal 0
Gtk.containerAdd contents widget
_ <- widgetSetClassGI contents "contents"
Gtk.widgetShowAll contents
Gtk.toWidget contents >>= buildPadBox
-- | Combine an icon widget and a label widget in a horizontal box with
-- standardised CSS classes. The box gets class @icon-label@, the first child
-- gets @icon@, and the second child gets @label@.
buildIconLabelBox :: (MonadIO m) => Gtk.Widget -> Gtk.Widget -> m Gtk.Widget
buildIconLabelBox iconWidget labelWidget = liftIO $ do
box <- Gtk.boxNew Gtk.OrientationHorizontal 0
_ <- widgetSetClassGI iconWidget "icon"
_ <- widgetSetClassGI labelWidget "label"
Gtk.containerAdd box iconWidget
Gtk.containerAdd box labelWidget
_ <- widgetSetClassGI box "icon-label"
Gtk.widgetShowAll box
Gtk.toWidget box
-- | Build a 'Gtk.Overlay' from a base widget plus one or more overlay widgets,
-- and mark overlays as "pass through" so they don't capture clicks/scrolls.
--
-- This is useful for workspace widgets that overlay a label on top of icons.
buildOverlayWithPassThrough :: (MonadIO m) => Gtk.Widget -> [Gtk.Widget] -> m Gtk.Widget
buildOverlayWithPassThrough base overlays = liftIO $ do
overlay <- Gtk.overlayNew
Gtk.containerAdd overlay base
forM_ overlays $ \w -> do
Gtk.overlayAddOverlay overlay w
Gtk.overlaySetOverlayPassThrough overlay w True
Gtk.toWidget overlay
-- | Wrap a widget in an event box aligned to the bottom-left.
--
-- This is used by workspace widgets to overlay a label on top of icons in a
-- consistent way across different backends.
buildBottomLeftAlignedBox :: (MonadIO m) => T.Text -> Gtk.Widget -> m Gtk.Widget
buildBottomLeftAlignedBox boxClass child = liftIO $ do
ebox <- Gtk.eventBoxNew
_ <- widgetSetClassGI ebox boxClass
Gtk.widgetSetHalign ebox Gtk.AlignStart
Gtk.widgetSetValign ebox Gtk.AlignEnd
Gtk.containerAdd ebox child
Gtk.toWidget ebox
-- | Compute a window icon strip layout given min/max icon config and a sorted
-- list of items.
--
-- Returns (effectiveMinIcons, targetLen, paddedItems) where:
-- - effectiveMinIcons is @min minIcons maxIcons@ when maxIcons is set
-- - targetLen is at least effectiveMinIcons and large enough for shown items
-- - paddedItems is exactly targetLen elements long (Just items, then Nothings)
computeIconStripLayout :: Int -> Maybe Int -> [a] -> (Int, Int, [Maybe a])
computeIconStripLayout minIcons maxIcons items =
let itemCount = length items
maxNeeded = maybe itemCount (min itemCount) maxIcons
effectiveMinIcons = maybe minIcons (min minIcons) maxIcons
targetLen = max effectiveMinIcons maxNeeded
shownItems = take maxNeeded items
paddedItems =
map Just shownItems ++ replicate (targetLen - length shownItems) Nothing
in (effectiveMinIcons, targetLen, paddedItems)
-- | CSS class name for a window icon given its state.
--
-- This matches the classes used by the workspaces widgets: `active`, `urgent`,
-- `minimized`, `normal` (and `inactive` when there is no window).
windowStatusClassFromFlags :: Bool -> Bool -> Bool -> T.Text
windowStatusClassFromFlags minimized active urgent
| minimized = "minimized"
| active = "active"
| urgent = "urgent"
| otherwise = "normal"
-- | Keep a pool of widget "slots" in sync with a desired list of per-slot data.
--
-- The pool is only grown (never shrunk). Widgets within the desired length are
-- shown and updated; widgets beyond it are updated with 'Nothing' and hidden.
syncWidgetPool ::
(MonadIO m, Gtk.IsWidget child) =>
Gtk.Box ->
[w] ->
[Maybe a] ->
(Int -> m w) ->
(w -> child) ->
(w -> Maybe a -> m ()) ->
m [w]
syncWidgetPool container pool desired mkOne getChild updateOne = do
let targetLen = length desired
pool' <-
if length pool >= targetLen
then return pool
else do
let start = length pool
newOnes <- forM [start .. targetLen - 1] $ \i -> do
w <- mkOne i
liftIO $ Gtk.containerAdd container (getChild w)
return w
liftIO $ Gtk.widgetShowAll container
return (pool ++ newOnes)
forM_ (zip3 [0 :: Int ..] pool' (desired ++ repeat Nothing)) $ \(i, w, payload) ->
if i < targetLen
then do
liftIO $ Gtk.widgetShow (getChild w)
updateOne w payload
else do
updateOne w Nothing
liftIO $ Gtk.widgetHide (getChild w)
return pool'