diff --git a/XMonad/Actions/ToggleFullFloat.hs b/XMonad/Actions/ToggleFullFloat.hs new file mode 100644 index 0000000000..0f176b5206 --- /dev/null +++ b/XMonad/Actions/ToggleFullFloat.hs @@ -0,0 +1,122 @@ +-- | +-- Module : XMonad.Actions.ToggleFullFloat +-- Description : Fullscreen (float) a window while remembering its original state. +-- Copyright : (c) 2022 Tomáš Janoušek +-- License : BSD3 +-- Maintainer : Tomáš Janoušek +-- +module XMonad.Actions.ToggleFullFloat ( + -- * Usage + -- $usage + toggleFullFloatEwmhFullscreen, + toggleFullFloat, + fullFloat, + unFullFloat, + gcToggleFullFloat, + ) where + +import qualified Data.Map.Strict as M + +import XMonad +import XMonad.Prelude +import XMonad.Hooks.EwmhDesktops (setEwmhFullscreenHooks) +import XMonad.Hooks.ManageHelpers +import qualified XMonad.StackSet as W +import qualified XMonad.Util.ExtensibleState as XS + +-- --------------------------------------------------------------------- +-- $usage +-- +-- The main use-case is to make 'ewmhFullscreen' (re)store the size and +-- position of floating windows instead of just unconditionally sinking them +-- into the floating layer. To enable this, you'll need this in your +-- @xmonad.hs@: +-- +-- > import XMonad +-- > import XMonad.Actions.ToggleFullFloat +-- > import XMonad.Hooks.EwmhDesktops +-- > +-- > main = xmonad $ … . toggleFullFloatEwmhFullscreen . ewmhFullscreen . ewmh . … $ def{…} +-- +-- Additionally, this "smart" fullscreening can be bound to a key and invoked +-- manually whenever one needs a larger window temporarily: +-- +-- > , ((modMask .|. shiftMask, xK_t), withFocused toggleFullFloat) + +newtype ToggleFullFloat = ToggleFullFloat{ fromToggleFullFloat :: M.Map Window (Maybe W.RationalRect) } + deriving (Show, Read) + +instance ExtensionClass ToggleFullFloat where + extensionType = PersistentExtension + initialValue = ToggleFullFloat mempty + +-- | Full-float a window, remembering its state (tiled/floating and +-- position/size). +fullFloat :: Window -> X () +fullFloat = windows . appEndo <=< runQuery doFullFloatSave + +-- | Restore window to its remembered state. +unFullFloat :: Window -> X () +unFullFloat = windows . appEndo <=< runQuery doFullFloatRestore + +-- | Full-float a window, if it's not already full-floating. Otherwise, +-- restore its original state. +toggleFullFloat :: Window -> X () +toggleFullFloat w = ifM (isFullFloat w) (unFullFloat w) (fullFloat w) + +isFullFloat :: Window -> X Bool +isFullFloat w = gets $ (Just fullRect ==) . M.lookup w . W.floating . windowset + where + fullRect = W.RationalRect 0 0 1 1 + +doFullFloatSave :: ManageHook +doFullFloatSave = do + w <- ask + liftX $ do + f <- gets $ M.lookup w . W.floating . windowset + -- @M.insertWith const@ = don't overwrite stored original state + XS.modify' $ ToggleFullFloat . M.insertWith const w f . fromToggleFullFloat + doFullFloat + +doFullFloatRestore :: ManageHook +doFullFloatRestore = do + w <- ask + mf <- liftX $ do + mf <- XS.gets $ M.lookup w . fromToggleFullFloat + XS.modify' $ ToggleFullFloat . M.delete w . fromToggleFullFloat + pure mf + doF $ case mf of + Just (Just f) -> W.float w f -- was floating before + Just Nothing -> W.sink w -- was tiled before + Nothing -> W.sink w -- fallback when not found in ToggleFullFloat + +-- | Install ToggleFullFloat garbage collection hooks. +-- +-- Note: This is included in 'toggleFullFloatEwmhFullscreen', only needed if +-- using the 'toggleFullFloat' separately from the EWMH hook. +gcToggleFullFloat :: XConfig a -> XConfig a +gcToggleFullFloat c = c { startupHook = startupHook c <> gcToggleFullFloatStartupHook + , handleEventHook = handleEventHook c <> gcToggleFullFloatEventHook } + +-- | ToggleFullFloat garbage collection: drop windows when they're destroyed. +gcToggleFullFloatEventHook :: Event -> X All +gcToggleFullFloatEventHook DestroyWindowEvent{ev_window = w} = do + XS.modify' $ ToggleFullFloat . M.delete w . fromToggleFullFloat + mempty +gcToggleFullFloatEventHook _ = mempty + +-- | ToggleFullFloat garbage collection: restrict to existing windows at +-- startup. +gcToggleFullFloatStartupHook :: X () +gcToggleFullFloatStartupHook = withWindowSet $ \ws -> + XS.modify' $ ToggleFullFloat . M.filterWithKey (\w _ -> w `W.member` ws) . fromToggleFullFloat + +-- | Hook this module into 'XMonad.Hooks.EwmhDesktops.ewmhFullscreen'. This +-- makes windows restore their original state (size and position if floating) +-- instead of unconditionally sinking into the tiling layer. +-- +-- ('gcToggleFullFloat' is included here.) +toggleFullFloatEwmhFullscreen :: XConfig a -> XConfig a +toggleFullFloatEwmhFullscreen = + setEwmhFullscreenHooks doFullFloatSave doFullFloatRestore . + gcToggleFullFloat diff --git a/XMonad/Hooks/EwmhDesktops.hs b/XMonad/Hooks/EwmhDesktops.hs index c4161aa8f3..84cd7afbe8 100644 --- a/XMonad/Hooks/EwmhDesktops.hs +++ b/XMonad/Hooks/EwmhDesktops.hs @@ -250,6 +250,9 @@ setEwmhActivateHook h = XC.modifyDef $ \c -> c{ activateHook = h } -- stateless: windows are fullscreened by turning them into fullscreen floats, -- and reverted by sinking them into the tiling layer. This behaviour can be -- configured by supplying a pair of 'ManageHook's to 'setEwmhFullscreenHooks'. +-- +-- See "XMonad.Actions.ToggleFullFloat" for a pair of hooks that store the +-- original state of floating windows. -- | Set (replace) the hooks invoked when clients ask to add/remove the -- $_NET_WM_STATE_FULLSCREEN@ state. The defaults are 'doFullFloat' and diff --git a/xmonad-contrib.cabal b/xmonad-contrib.cabal index 942e48b7a2..72a9b6da07 100644 --- a/xmonad-contrib.cabal +++ b/xmonad-contrib.cabal @@ -147,6 +147,7 @@ library XMonad.Actions.SwapWorkspaces XMonad.Actions.TagWindows XMonad.Actions.TiledWindowDragging + XMonad.Actions.ToggleFullFloat XMonad.Actions.TopicSpace XMonad.Actions.TreeSelect XMonad.Actions.UpdateFocus