packages feed

taffybar-7.2.0: src/System/Taffybar/Widget/OmniMenu.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- |
-- Module      : System.Taffybar.Widget.OmniMenu
-- Copyright   : (c) Ivan A. Malison
-- License     : BSD3-style (see LICENSE)
--
-- Maintainer  : Ivan A. Malison
-- Stability   : unstable
-- Portability : unportable
--
-- A menu button for launcher and session-control commands. The button can wrap
-- any widget, so callers can use an icon from a theme, a file image, a label, or
-- their own composed image widget as the visible trigger.
module System.Taffybar.Widget.OmniMenu
  ( OmniMenuConfig (..),
    OmniMenuItem (..),
    OmniMenuSection (..),
    defaultOmniMenuConfig,
    omniMenuNew,
    omniMenuNewFromFile,
    omniMenuNewFromIconName,
    omniMenuNewWithConfig,
  )
where

import Control.Monad (unless, void, when)
import Control.Monad.IO.Class
import Data.Foldable (foldlM)
import Data.Int (Int32)
import qualified Data.Text as T
import qualified GI.GdkPixbuf.Objects.Pixbuf as Gdk
import qualified GI.Gtk as Gtk
import System.Log.Logger
import System.Process
import System.Taffybar.Widget.Generic.AutoFillImage
import System.Taffybar.Widget.Generic.DynamicMenu
import System.Taffybar.Widget.Util
import System.Taffybar.Widget.XDGMenu.Menu

data OmniMenuItem = OmniMenuItem
  { omniMenuItemLabel :: T.Text,
    omniMenuItemCommand :: T.Text,
    omniMenuItemIcon :: Maybe T.Text,
    omniMenuItemTooltip :: Maybe T.Text
  }

data OmniMenuSection = OmniMenuSection
  { omniMenuSectionLabel :: T.Text,
    omniMenuSectionItems :: [OmniMenuItem]
  }

data OmniMenuConfig = OmniMenuConfig
  { omniMenuClickWidget :: Gtk.Widget,
    omniMenuIncludeApplications :: Bool,
    omniMenuXDGMenuPrefix :: Maybe String,
    omniMenuSections :: [OmniMenuSection]
  }

defaultOmniMenuConfig :: Gtk.Widget -> OmniMenuConfig
defaultOmniMenuConfig clickWidget =
  OmniMenuConfig
    { omniMenuClickWidget = clickWidget,
      omniMenuIncludeApplications = True,
      omniMenuXDGMenuPrefix = Nothing,
      omniMenuSections = []
    }

omniMenuNew :: (MonadIO m) => Gtk.Widget -> [OmniMenuSection] -> m Gtk.Widget
omniMenuNew clickWidget sections =
  omniMenuNewWithConfig $
    (defaultOmniMenuConfig clickWidget)
      { omniMenuSections = sections
      }

omniMenuNewFromFile :: (MonadIO m) => FilePath -> [OmniMenuSection] -> m Gtk.Widget
omniMenuNewFromFile path sections = do
  image <- Gtk.imageNewFromFile path
  clickWidget <- Gtk.toWidget image
  omniMenuNew clickWidget sections

omniMenuNewFromIconName :: (MonadIO m) => T.Text -> [OmniMenuSection] -> m Gtk.Widget
omniMenuNewFromIconName iconName sections = do
  image <-
    Gtk.imageNewFromIconName
      (Just iconName)
      (fromIntegral $ fromEnum Gtk.IconSizeMenu)
  clickWidget <- Gtk.toWidget image
  omniMenuNew clickWidget sections

omniMenuNewWithConfig :: (MonadIO m) => OmniMenuConfig -> m Gtk.Widget
omniMenuNewWithConfig OmniMenuConfig {..} = do
  menuButton <-
    dynamicMenuNew
      DynamicMenuConfig
        { dmClickWidget = omniMenuClickWidget,
          dmPopulateMenu = populateOmniMenu
        }
  _ <- widgetSetClassGI menuButton "omni-menu-button"
  return menuButton
  where
    populateOmniMenu menu = do
      applicationsAdded <-
        if omniMenuIncludeApplications
          then addApplicationsMenu menu omniMenuXDGMenuPrefix
          else pure False
      void $ foldlM (addSection menu) applicationsAdded omniMenuSections
      Gtk.widgetShowAll menu

omniMenuLog :: Priority -> String -> IO ()
omniMenuLog = logM "System.Taffybar.Widget.OmniMenu"

omniMenuImageMenuItemNew ::
  T.Text -> (Int32 -> IO (Maybe Gdk.Pixbuf)) -> IO Gtk.MenuItem
omniMenuImageMenuItemNew labelText pixbufGetter = do
  box <- Gtk.boxNew Gtk.OrientationHorizontal 6
  iconSlot <- Gtk.boxNew Gtk.OrientationHorizontal 0
  label <- Gtk.labelNew $ Just labelText
  image <- autoFillImageNew pixbufGetter Gtk.OrientationHorizontal
  item <- Gtk.menuItemNew
  Gtk.widgetSetSizeRequest iconSlot omniMenuIconSize omniMenuIconSize
  Gtk.containerAdd iconSlot image
  Gtk.containerAdd box iconSlot
  Gtk.containerAdd box label
  Gtk.containerAdd item box
  Gtk.widgetSetHalign box Gtk.AlignStart
  Gtk.widgetSetHalign iconSlot Gtk.AlignCenter
  Gtk.widgetSetValign iconSlot Gtk.AlignCenter
  Gtk.widgetSetHalign image Gtk.AlignCenter
  Gtk.widgetSetValign image Gtk.AlignCenter
  Gtk.widgetSetValign label Gtk.AlignCenter
  Gtk.widgetSetValign box Gtk.AlignFill
  return item

omniMenuIconSize :: Int32
omniMenuIconSize = 16

addApplicationsMenu :: (Gtk.IsMenuShell menuShell) => menuShell -> Maybe String -> IO Bool
addApplicationsMenu menuShell menuPrefix = do
  xdgMenu <- buildMenu menuPrefix
  if isEmptyXDGMenu xdgMenu
    then pure False
    else do
      item <- Gtk.menuItemNewWithLabel "Applications"
      submenu <- Gtk.menuNew
      Gtk.menuItemSetSubmenu item (Just submenu)
      addXDGMenuContents submenu xdgMenu
      Gtk.menuShellAppend menuShell item
      pure True

isEmptyXDGMenu :: Menu -> Bool
isEmptyXDGMenu Menu {..} =
  null fmEntries && all isEmptyXDGMenu fmSubmenus

addXDGMenuContents :: (Gtk.IsMenuShell menuShell) => menuShell -> Menu -> IO ()
addXDGMenuContents menuShell Menu {..} = do
  mapM_ (addXDGSubmenu menuShell) fmSubmenus
  mapM_ (addXDGEntry menuShell) fmEntries

addXDGSubmenu :: (Gtk.IsMenuShell menuShell) => menuShell -> Menu -> IO ()
addXDGSubmenu menuShell menu@Menu {..} =
  unless (null fmEntries && null fmSubmenus) $ do
    item <-
      omniMenuImageMenuItemNew
        (T.pack fmName)
        (getImageForMaybeIconName (T.pack <$> fmIcon))
    submenu <- Gtk.menuNew
    Gtk.menuItemSetSubmenu item (Just submenu)
    addXDGMenuContents submenu menu
    Gtk.menuShellAppend menuShell item

addXDGEntry :: (Gtk.IsMenuShell menuShell) => menuShell -> MenuEntry -> IO ()
addXDGEntry menuShell MenuEntry {..} = do
  item <- omniMenuImageMenuItemNew feName (getImageForMaybeIconName feIcon)
  Gtk.widgetSetTooltipText item (Just feComment)
  Gtk.menuShellAppend menuShell item
  void $ Gtk.onMenuItemActivate item $ do
    omniMenuLog DEBUG $ "Launching '" ++ feCommand ++ "'"
    void $ spawnCommand feCommand

addSection :: (Gtk.IsMenuShell menuShell) => menuShell -> Bool -> OmniMenuSection -> IO Bool
addSection menuShell hasPrevious OmniMenuSection {..} =
  if null omniMenuSectionItems
    then pure hasPrevious
    else do
      when hasPrevious $ addSeparator menuShell
      addSectionHeader menuShell omniMenuSectionLabel
      mapM_ (addCommandItem menuShell) omniMenuSectionItems
      pure True

addSeparator :: (Gtk.IsMenuShell menuShell) => menuShell -> IO ()
addSeparator menuShell = do
  separator <- Gtk.separatorMenuItemNew
  Gtk.menuShellAppend menuShell separator

addSectionHeader :: (Gtk.IsMenuShell menuShell) => menuShell -> T.Text -> IO ()
addSectionHeader menuShell label = do
  item <- Gtk.menuItemNewWithLabel label
  Gtk.widgetSetSensitive item False
  Gtk.menuShellAppend menuShell item

addCommandItem :: (Gtk.IsMenuShell menuShell) => menuShell -> OmniMenuItem -> IO ()
addCommandItem menuShell OmniMenuItem {..} = do
  item <-
    omniMenuImageMenuItemNew
      omniMenuItemLabel
      (getImageForMaybeIconName omniMenuItemIcon)
  Gtk.widgetSetTooltipText item omniMenuItemTooltip
  Gtk.menuShellAppend menuShell item
  void $ Gtk.onMenuItemActivate item $ do
    omniMenuLog DEBUG $ "Launching '" ++ T.unpack omniMenuItemCommand ++ "'"
    void $ spawnCommand $ T.unpack omniMenuItemCommand