1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
|
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE RankNTypes #-}
-- | Routines for connecting `GObject`s to signals. There are two
-- basic variants, 'on' and 'after', which correspond to
-- <https://docs.gtk.org/gobject/func.signal_connect.html g_signal_connect> and <https://docs.gtk.org/gobject/func.signal_connect_after.html g_signal_connect_after>, respectively.
--
-- Basic usage is
--
-- @ 'on' widget #signalName $ do ... @
--
-- or
--
-- @ 'after' widget #signalName $ do ... @
--
-- Note that in the Haskell bindings we represent the signal name in
-- camelCase, so a signal like <https://webkitgtk.org/reference/webkit2gtk/stable/WebKitUserContentManager.html#WebKitUserContentManager-script-message-received script-message-received> in the original API becomes <https://hackage.haskell.org/package/gi-webkit2-4.0.24/docs/GI-WebKit2-Objects-UserContentManager.html#g:16 scriptMessageReceived> in the bindings.
--
-- There are two variants of note. If you want to provide a detail
-- when connecting the signal you can use ':::', as follows:
--
-- @ 'on' widget (#scriptMessageReceived ':::' "handlerName") $ do ... @
--
-- On the other hand, if you want to connect to the "<https://hackage.haskell.org/package/gi-gobject-2.0.21/docs/GI-GObject-Objects-Object.html#g:30 notify>" signal for a property of a widget, it is recommended to use instead 'PropertyNotify', as follows:
--
-- @ 'on' widget ('PropertyNotify' #propertyName) $ do ... @
--
-- which has the advantage that it will be checked at compile time
-- that the widget does indeed have the property "@propertyName@".
module Data.GI.Base.Signals
( on
, after
, SignalProxy(..)
, SignalConnectMode(..)
, connectSignalFunPtr
, disconnectSignalHandler
, SignalHandlerId
, SignalInfo(..)
, GObjectNotifySignalInfo
, SignalCodeGenError
, resolveSignal
, connectGObjectNotify
) where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Proxy (Proxy(..))
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
#endif
import Foreign
import Foreign.C
#if !MIN_VERSION_base(4,13,0)
import Foreign.Ptr (nullPtr)
#endif
import GHC.TypeLits
import Data.Kind (Type)
import qualified Data.Text as T
import Data.Text (Text)
import Data.GI.Base.Attributes (AttrLabelProxy(..), AttrInfo(AttrLabel),
AttrGetType, attrGet,
AttrBaseTypeConstraint)
import Data.GI.Base.BasicConversions (withTextCString)
import Data.GI.Base.BasicTypes
import Data.GI.Base.GParamSpec (newGParamSpecFromPtr)
import Data.GI.Base.ManagedPtr (withManagedPtr, withTransient)
import Data.GI.Base.Overloading (ResolveSignal, ResolveAttribute,
ResolvedSymbolInfo)
import GHC.OverloadedLabels (IsLabel(..))
-- | Type of a `GObject` signal handler id.
type SignalHandlerId = CULong
-- | Support for overloaded signal connectors.
data SignalProxy (object :: Type) (info :: Type) where
-- | A basic signal name connector.
SignalProxy :: SignalProxy o info
-- | A signal connector annotated with a detail.
(:::) :: forall o info. SignalProxy o info -> Text -> SignalProxy o info
-- | A signal connector for the @notify@ signal on the given property.
PropertyNotify :: (info ~ ResolveAttribute propName o,
AttrInfo info,
pl ~ AttrLabel info, KnownSymbol pl) =>
AttrLabelProxy propName ->
SignalProxy o GObjectNotifySignalInfo
-- | A signal connector for the @notify@ signal on the given
-- property, similar to `PropertyNotify`, but it passes the new
-- value of the property to the callback for convenience.
PropertySet :: (info ~ ResolveAttribute propName o,
AttrInfo info,
AttrBaseTypeConstraint info o,
b ~ AttrGetType info,
pl ~ AttrLabel info, KnownSymbol pl) =>
AttrLabelProxy propName ->
SignalProxy o (GObjectPropertySetSignalInfo b)
-- | Support for overloaded labels.
instance (info ~ ResolveSignal slot object) =>
IsLabel slot (SignalProxy object info) where
#if MIN_VERSION_base(4,10,0)
fromLabel = SignalProxy
#else
fromLabel _ = SignalProxy
#endif
-- | Information about an overloaded signal.
class SignalInfo (info :: Type) where
-- | The type for the signal handler.
type HaskellCallbackType info :: Type
-- | Connect a Haskell function to a signal of the given `GObject`,
-- specifying whether the handler will be called before or after the
-- default handler. Note that the callback being passed here admits
-- an extra initial parameter with respect to the usual Haskell
-- callback type. This will be passed as an /implicit/ @?self@
-- argument to the Haskell callback.
connectSignal :: GObject o =>
o ->
(o -> HaskellCallbackType info) ->
SignalConnectMode ->
Maybe Text ->
IO SignalHandlerId
-- | Optional extra debug information, for `resolveSignal` below.
dbgSignalInfo :: Maybe ResolvedSymbolInfo
dbgSignalInfo = Nothing
-- | Whether to connect a handler to a signal with `connectSignal` so
-- that it runs before/after the default handler for the given signal.
data SignalConnectMode = SignalConnectBefore -- ^ Run before the default handler.
| SignalConnectAfter -- ^ Run after the default handler.
-- | Connect a signal to a signal handler.
on :: forall object info m.
(GObject object, MonadIO m, SignalInfo info) =>
object -> SignalProxy object info
-> ((?self :: object) => HaskellCallbackType info)
-> m SignalHandlerId
on o p@(PropertySet (_ :: AttrLabelProxy propName)) cb = liftIO $ do
let wrapped = wrapPropertySet (Proxy @propName) (Proxy @object) cb
cb' <- mkGObjectNotifyCallback wrapped
connectSignalFunPtr o "notify" cb' SignalConnectBefore (proxyDetail p)
on o p c =
liftIO $ connectSignal @info o w SignalConnectBefore (proxyDetail p)
where w :: object -> HaskellCallbackType info
w parent = let ?self = parent in c
-- | Wrap a @b -> IO ()@ callback into a property notify callback on
-- the C side, by adding some code that reads the current value of the
-- property before invoking the callback.
wrapPropertySet :: forall info prop obj.
(info ~ ResolveAttribute prop obj,
AttrBaseTypeConstraint info obj,
AttrInfo info,
GObject obj) =>
Proxy (prop :: Symbol) -> Proxy obj ->
((?self :: obj) => AttrGetType info -> IO ()) ->
Ptr obj -> Ptr GParamSpec -> Ptr () -> IO ()
wrapPropertySet _ _ cb objPtr _pspec _data =
withTransient objPtr $ \self -> do
val <- attrGet @(ResolveAttribute prop obj) self
let ?self = self in cb val
-- | Connect a signal to a handler, running the handler after the default one.
after :: forall object info m.
(GObject object, MonadIO m, SignalInfo info) =>
object -> SignalProxy object info
-> ((?self :: object) => HaskellCallbackType info)
-> m SignalHandlerId
after o p c =
liftIO $ connectSignal @info o w SignalConnectAfter (proxyDetail p)
where w :: object -> HaskellCallbackType info
w parent = let ?self = parent in c
-- | Given a signal proxy, determine the corresponding detail.
proxyDetail :: forall object info. SignalProxy object info -> Maybe Text
proxyDetail p = case p of
SignalProxy -> Nothing
(_ ::: detail) -> Just detail
PropertyNotify (AttrLabelProxy :: AttrLabelProxy propName) ->
Just . T.pack $ symbolVal (Proxy @(AttrLabel (ResolveAttribute propName object)))
PropertySet (AttrLabelProxy :: AttrLabelProxy propName) ->
Just . T.pack $ symbolVal (Proxy @(AttrLabel (ResolveAttribute propName object)))
-- Connecting GObjects to signals
foreign import ccall g_signal_connect_data ::
Ptr a -> -- instance
CString -> -- detailed_signal
FunPtr b -> -- c_handler
Ptr () -> -- data
FunPtr c -> -- destroy_data
CUInt -> -- connect_flags
IO SignalHandlerId
-- Releasing the `FunPtr` for the signal handler.
foreign import ccall "& haskell_gi_release_signal_closure"
ptr_to_release_closure :: FunPtr (Ptr () -> Ptr () -> IO ())
-- | Connect a signal to a handler, given as a `FunPtr`.
connectSignalFunPtr :: GObject o =>
o -> Text -> FunPtr a -> SignalConnectMode ->
Maybe Text -> IO SignalHandlerId
connectSignalFunPtr object signal fn mode maybeDetail = do
let flags = case mode of
SignalConnectAfter -> 1
SignalConnectBefore -> 0
signalSpec = case maybeDetail of
Nothing -> signal
Just detail -> signal <> "::" <> detail
withTextCString signalSpec $ \csignal ->
withManagedPtr object $ \objPtr ->
g_signal_connect_data objPtr csignal fn nullPtr ptr_to_release_closure flags
foreign import ccall g_signal_handler_disconnect :: Ptr o -> SignalHandlerId -> IO ()
-- | Disconnect a previously connected signal.
disconnectSignalHandler :: GObject o => o -> SignalHandlerId -> IO ()
disconnectSignalHandler obj handlerId =
withManagedPtr obj $ \objPtr ->
g_signal_handler_disconnect objPtr handlerId
-- | Connection information for a "notify" signal indicating that a
-- specific property changed (see `PropertyNotify` for the relevant
-- constructor).
data GObjectNotifySignalInfo
instance SignalInfo GObjectNotifySignalInfo where
type HaskellCallbackType GObjectNotifySignalInfo = GObjectNotifyCallback
connectSignal = connectGObjectNotify
-- | Type for a `GObject` "notify" callback.
type GObjectNotifyCallback = GParamSpec -> IO ()
gobjectNotifyCallbackWrapper :: GObject o =>
(o -> GObjectNotifyCallback) -> GObjectNotifyCallbackC o
gobjectNotifyCallbackWrapper cb selfPtr pspec _ = do
pspec' <- newGParamSpecFromPtr pspec
withTransient (castPtr selfPtr) $ \self -> cb self pspec'
type GObjectNotifyCallbackC o = Ptr o -> Ptr GParamSpec -> Ptr () -> IO ()
foreign import ccall "wrapper"
mkGObjectNotifyCallback :: GObjectNotifyCallbackC o -> IO (FunPtr (GObjectNotifyCallbackC o))
-- | Connect the given notify callback for a GObject.
connectGObjectNotify :: GObject o =>
o -> (o -> GObjectNotifyCallback) ->
SignalConnectMode ->
Maybe Text ->
IO SignalHandlerId
connectGObjectNotify obj cb mode detail = do
cb' <- mkGObjectNotifyCallback (gobjectNotifyCallbackWrapper cb)
connectSignalFunPtr obj "notify" cb' mode detail
data GObjectPropertySetSignalInfo (b :: Type)
instance SignalInfo (GObjectPropertySetSignalInfo b) where
type HaskellCallbackType (GObjectPropertySetSignalInfo b) = b -> IO ()
connectSignal = undefined -- We connect these separately
-- | Generate an informative type error whenever one tries to use a
-- signal for which code generation has failed.
type family SignalCodeGenError (signalName :: Symbol) :: Type where
SignalCodeGenError signalName = TypeError
('Text "The signal ‘"
':<>: 'Text signalName
':<>: 'Text "’ is not supported, because haskell-gi failed to generate appropriate bindings."
':$$: 'Text "Please file an issue at https://github.com/haskell-gi/haskell-gi/issues.")
-- | Return the fully qualified signal name that a given overloaded
-- signal resolves to (mostly useful for debugging).
--
-- > resolveSignal #childNotify button
resolveSignal :: forall object info. (GObject object, SignalInfo info) =>
object -> SignalProxy object info -> Maybe ResolvedSymbolInfo
resolveSignal _o _p = dbgSignalInfo @info
|