hsqml-0.3.2.1: src/Graphics/QML/Engine.hs
{-# LANGUAGE
DeriveDataTypeable,
FlexibleContexts,
GeneralizedNewtypeDeriving
#-}
-- | Functions for starting QML engines, displaying content in a window.
module Graphics.QML.Engine (
-- * Engines
EngineConfig(
EngineConfig,
initialDocument,
contextObject),
defaultEngineConfig,
Engine,
runEngine,
runEngineWith,
runEngineAsync,
runEngineLoop,
-- * Event Loop
RunQML(),
runEventLoop,
requireEventLoop,
shutdownQt,
EventLoopException(),
-- * Document Paths
DocumentPath(),
fileDocument,
uriDocument
) where
import Graphics.QML.Internal.JobQueue
import Graphics.QML.Internal.Marshal
import Graphics.QML.Internal.BindPrim
import Graphics.QML.Internal.BindCore
import Graphics.QML.Marshal ()
import Graphics.QML.Objects
import Control.Applicative
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import qualified Data.Text as T
import Data.List
import Data.Traversable
import Data.Typeable
import Foreign.Ptr
import System.FilePath (isAbsolute, splitDirectories, pathSeparators)
-- | Holds parameters for configuring a QML runtime engine.
data EngineConfig = EngineConfig {
-- | Path to the first QML document to be loaded.
initialDocument :: DocumentPath,
-- | Context 'Object' made available to QML script code.
contextObject :: Maybe AnyObjRef
}
-- | Default engine configuration. Loads @\"main.qml\"@ from the current
-- working directory into a visible window with no context object.
defaultEngineConfig :: EngineConfig
defaultEngineConfig = EngineConfig {
initialDocument = DocumentPath "main.qml",
contextObject = Nothing
}
-- | Represents a QML engine.
data Engine = Engine
runEngineImpl :: EngineConfig -> IO () -> IO Engine
runEngineImpl config stopCb = do
hsqmlInit
let obj = contextObject config
DocumentPath res = initialDocument config
hndl <- sequenceA $ fmap mToHndl obj
mWithCVal (T.pack res) $ \resPtr ->
hsqmlCreateEngine hndl (HsQMLStringHandle $ castPtr resPtr) stopCb
return Engine
-- | Starts a new QML engine using the supplied configuration and blocks until
-- the engine has terminated.
runEngine :: EngineConfig -> RunQML ()
runEngine config = runEngineWith config (const $ return ())
-- | Starts a new QML engine using the supplied configuration. The \'with\'
-- function is executed once the engine has been started and after it returns
-- this function blocks until the engine has terminated.
runEngineWith :: EngineConfig -> (Engine -> RunQML a) -> RunQML a
runEngineWith config with = RunQML $ do
finishVar <- newEmptyMVar
let stopCb = putMVar finishVar ()
eng <- runEngineImpl config stopCb
let (RunQML withIO) = with eng
ret <- withIO
void $ takeMVar finishVar
return ret
-- | Starts a new QML engine using the supplied configuration and returns
-- immediately without blocking.
runEngineAsync :: EngineConfig -> RunQML Engine
runEngineAsync config = RunQML $ runEngineImpl config (return ())
-- | Conveniance function that both runs the event loop and starts a new QML
-- engine. It blocks keeping the event loop running until the engine has
-- terminated.
runEngineLoop :: EngineConfig -> IO ()
runEngineLoop config =
runEventLoop $ runEngine config
-- | Wrapper around the IO monad for running actions which depend on the Qt
-- event loop.
newtype RunQML a = RunQML (IO a) deriving (Functor, Applicative, Monad)
instance MonadIO RunQML where
liftIO = RunQML
-- | This function enters the Qt event loop and executes the supplied function
-- in the 'RunQML' monad on a new unbound thread. The event loop will continue
-- to run until all functions in the 'RunQML' monad have completed. This
-- includes both the 'RunQML' function launched by this call and any launched
-- asynchronously via 'requireEventLoop'. When the event loop exits, all
-- engines will be terminated.
--
-- It's recommended that applications run the event loop on their primordial
-- thread as some platforms mandate this. Once the event loop has finished, it
-- can be started again, but only on the same operating system thread as
-- before. If the event loop fails to start then an 'EventLoopException' will
-- be thrown.
runEventLoop :: RunQML a -> IO a
runEventLoop (RunQML runFn) = tryRunInBoundThread $ do
hsqmlInit
finishVar <- newEmptyMVar
let startCb = void $ forkIO $ do
ret <- try runFn
case ret of
Left ex -> putMVar finishVar $ throwIO (ex :: SomeException)
Right ret' -> putMVar finishVar $ return ret'
hsqmlEvloopRelease
yieldCb = if rtsSupportsBoundThreads
then Nothing
else Just yield
status <- hsqmlEvloopRun startCb processJobs yieldCb
case statusException status of
Just ex -> throw ex
Nothing -> do
finFn <- takeMVar finishVar
finFn
tryRunInBoundThread :: IO a -> IO a
tryRunInBoundThread action =
if rtsSupportsBoundThreads
then runInBoundThread action
else action
-- | Executes a function in the 'RunQML' monad asynchronously to the event
-- loop. Callers must apply their own sychronisation to ensure that the event
-- loop is currently running when this function is called, otherwise an
-- 'EventLoopException' will be thrown. The event loop will not exit until the
-- supplied function has completed.
requireEventLoop :: RunQML a -> IO a
requireEventLoop (RunQML runFn) = do
hsqmlInit
let reqFn = do
status <- hsqmlEvloopRequire
case statusException status of
Just ex -> throw ex
Nothing -> return ()
bracket_ reqFn hsqmlEvloopRelease runFn
-- | Shuts down and frees resources used by the Qt framework, preventing
-- further use of the event loop. The framework is initialised when
-- 'runEventLoop' is first called and remains initialised afterwards so that
-- the event loop can be reentered if desired (e.g. when using GHCi). Once
-- shut down, the framework cannot be reinitialised.
--
-- It is recommended that you call this function at the end of your program as
-- this library will try, but cannot guarantee in all configurations to be able
-- to shut it down for you. Failing to shutdown the framework has been known to
-- intermittently cause crashes on process exit on some platforms.
--
-- This function must be called from the event loop thread and the event loop
-- must not be running at the time otherwise an 'EventLoopException' will be
-- thrown.
shutdownQt :: IO ()
shutdownQt = do
status <- hsqmlEvloopShutdown
case statusException status of
Just ex -> throw ex
Nothing -> return ()
statusException :: HsQMLEventLoopStatus -> Maybe EventLoopException
statusException HsqmlEvloopOk = Nothing
statusException HsqmlEvloopAlreadyRunning = Just EventLoopAlreadyRunning
statusException HsqmlEvloopPostShutdown = Just EventLoopPostShutdown
statusException HsqmlEvloopWrongThread = Just EventLoopWrongThread
statusException HsqmlEvloopNotRunning = Just EventLoopNotRunning
statusException _ = Just EventLoopOtherError
-- | Exception type used to report errors pertaining to the event loop.
data EventLoopException
= EventLoopAlreadyRunning
| EventLoopPostShutdown
| EventLoopWrongThread
| EventLoopNotRunning
| EventLoopOtherError
deriving (Show, Typeable)
instance Exception EventLoopException
-- | Path to a QML document file.
newtype DocumentPath = DocumentPath String
-- | Converts a local file path into a 'DocumentPath'.
fileDocument :: FilePath -> DocumentPath
fileDocument fp =
let ds = splitDirectories fp
isAbs = isAbsolute fp
fixHead =
(\cs -> if null cs then [] else '/':cs) .
takeWhile (`notElem` pathSeparators)
mapHead _ [] = []
mapHead f (x:xs) = f x : xs
afp = intercalate "/" $ mapHead fixHead ds
rfp = intercalate "/" ds
in DocumentPath $ if isAbs then "file://" ++ afp else rfp
-- | Converts a URI string into a 'DocumentPath'.
uriDocument :: String -> DocumentPath
uriDocument = DocumentPath