-- This file is part of Qtah.
--
-- Copyright 2015-2020 The Qtah Authors.
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Lesser General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public License
-- along with this program. If not, see .
{-# LANGUAGE CPP #-}
module Graphics.UI.Qtah.Generator.Module (
AModule (..),
aModuleHoppyModules,
QtModule,
makeQtModule,
makeQtModuleWithMinVersion,
qtModulePath,
qtModuleQtExports,
qtModuleHoppy,
) where
import Control.Monad (unless)
import Control.Monad.Except (throwError)
import Data.Char (toLower)
import Data.Foldable (forM_)
import Data.List (find, intersperse, sort)
import Data.Maybe (isJust)
#if !MIN_VERSION_base(4,13,0)
import Data.Monoid (mconcat)
#endif
import Foreign.Hoppy.Generator.Language.Cpp (chunkContents, execChunkWriter, sayType)
import Foreign.Hoppy.Generator.Language.Haskell (
Generator,
HsTypeSide (HsHsSide),
addExport,
addExport',
addExports,
addExtension,
addImports,
askInterface,
cppTypeToHsTypeAndUse,
getClassHaskellConversion,
getModuleForExtName,
getModuleName,
indent,
inFunction,
ln,
prettyPrint,
sayLn,
saysLn,
toHsFnName',
)
import Foreign.Hoppy.Generator.Spec (
Class,
Constness (Const, Nonconst),
Ctor,
ExtName,
FnName (FnName),
ForeignLanguage (Haskell),
Function,
Method,
MethodImpl (RealMethod),
Module,
Type,
addAddendumHaskell,
callbackParams,
castExport,
classCtors,
classEntityForeignName,
classExtName,
classHaskellConversionFromCppFn,
classHaskellConversionToCppFn,
classMethods,
ctorExtName,
enumValueMapNames,
fnExtName,
fromExtName,
getPrimaryExtName,
hsImport1,
hsImports,
hsWholeModuleImport,
makeModule,
methodExtName,
methodImpl,
moduleAddExports,
moduleAddHaskellName,
moduleModify',
parameterType,
varGetterExtName,
varIsConst,
varSetterExtName,
)
import Foreign.Hoppy.Generator.Spec.Callback (callbackT)
import Foreign.Hoppy.Generator.Spec.Class (
toHsCastMethodName',
toHsDataTypeName',
toHsDownCastMethodName',
toHsPtrClassName',
toHsValueClassName',
)
import Foreign.Hoppy.Generator.Spec.Enum (enumGetOverriddenEntryName, enumValues, toHsEnumTypeName')
import Foreign.Hoppy.Generator.Types (objT)
import Graphics.UI.Qtah.Generator.Config (Version, qrealFloat, qtVersion)
import Graphics.UI.Qtah.Generator.Common (fromMaybeM)
import Graphics.UI.Qtah.Generator.Flags (
flagsEnum,
toHsFlagsBindingName',
toHsFlagsTypeName',
toHsFlagsTypeclassName',
)
import Graphics.UI.Qtah.Generator.Types (
QtExport (
QtExport,
QtExportClassAndSignals,
QtExportEvent,
QtExportFnRenamed,
QtExportSceneEvent,
QtExportSpecials
),
Signal,
qtExportToExports,
signalCallback,
signalClass,
signalCName,
signalHaskellName,
signalListenerClass,
)
import Graphics.UI.Qtah.Generator.Interface.Imports
import Language.Haskell.Syntax (
HsName (HsIdent),
HsQName (UnQual),
HsQualType (HsQualType),
HsType (HsTyApp, HsTyCon, HsTyFun, HsTyVar),
)
-- | A union of Hoppy and Qt modules.
data AModule = AHoppyModule Module | AQtModule QtModule
aModuleHoppyModules :: AModule -> [Module]
aModuleHoppyModules (AHoppyModule m) = [m]
aModuleHoppyModules (AQtModule qm) = [qtModuleHoppy qm, qtModuleHoppyWrapper qm]
-- | A @QtModule@ (distinct from a Hoppy 'Module'), is a description of a
-- Haskell module in the @Graphics.UI.Qtah.Q@ namespace that:
--
-- 1. reexports 'Export's from a Hoppy module, dropping @ClassName_@
-- prefixes from the reexported names.
-- 2. generates Signal definitions for Qt signals.
data QtModule = QtModule
{ qtModulePath :: [String]
, qtModuleQtExports :: [QtExport]
-- ^ A list of exports whose generated Hoppy bindings will be re-exported in
-- this module.
, qtModuleHoppy :: Module
, qtModuleHoppyWrapper :: Module
}
makeQtModule :: [String] -> [QtExport] -> QtModule
makeQtModule [] _ = error "makeQtModule: Module path must be nonempty."
makeQtModule modulePath@(_:moduleNameParts) qtExports =
let lowerName = map toLower $ concat moduleNameParts
in QtModule
{ qtModulePath = modulePath
, qtModuleQtExports = qtExports
, qtModuleHoppy =
moduleModify' (makeModule lowerName
(concat ["b_", lowerName, ".hpp"])
(concat ["b_", lowerName, ".cpp"])) $ do
moduleAddHaskellName $ "Generated" : modulePath
moduleAddExports $ concatMap qtExportToExports qtExports
, qtModuleHoppyWrapper =
addAddendumHaskell (sayWrapperModule modulePath qtExports) $
moduleModify' (makeModule (lowerName ++ "wrap")
(concat ["b_", lowerName, "_w.hpp"])
(concat ["b_", lowerName, "_w.cpp"])) $
moduleAddHaskellName modulePath
}
-- | Creates a 'QtModule' (a la 'makeQtModule') that has a minimum version
-- applied to all of its contents. If Qtah is being built against a version of
-- Qt below this minimum version, then the module will still be generated, but
-- it will be empty; the exports list will be replaced with an empty list.
makeQtModuleWithMinVersion :: [String] -> Version -> [QtExport] -> QtModule
makeQtModuleWithMinVersion modulePath minVersion qtExports =
makeQtModule modulePath $
if qtVersion >= minVersion then qtExports else []
sayWrapperModule :: [String] -> [QtExport] -> Generator ()
sayWrapperModule modulePath qtExports = inFunction "" $ do
addExtension "NoMonomorphismRestriction"
-- As in generated Hoppy bindings, avoid non-qualified Prelude uses in
-- generated code here.
addImports $ hsImports "Prelude" []
-- Import the underlying Hoppy module wholesale.
case concatMap qtExportToExports qtExports of
[] -> return ()
export:_ -> importWholeModuleForExtName $ getPrimaryExtName export
-- Generate bindings for all of the exports.
mapM_ (sayQtExport modulePath) qtExports
getFnImportName :: Function -> String
getFnImportName = toHsFnName' . fnExtName
getFnReexportName :: Function -> String
getFnReexportName = getFnImportName
classUpCastReexportName :: String
classUpCastReexportName = "cast"
classUpCastConstReexportName :: String
classUpCastConstReexportName = "castConst"
classDownCastReexportName :: String
classDownCastReexportName = "downCast"
classDownCastConstReexportName :: String
classDownCastConstReexportName = "downCastConst"
classEncodeReexportName :: String
classEncodeReexportName = "encode"
classDecodeReexportName :: String
classDecodeReexportName = "decode"
getCtorReexportName :: Ctor -> String
getCtorReexportName = toHsFnName' . ctorExtName
getMethodReexportName :: Method -> String
getMethodReexportName = toHsFnName' . methodExtName
sayClassEncodingFnReexports :: Class -> Generator ()
sayClassEncodingFnReexports cls = inFunction "sayClassEncodingFnReexports" $ do
let conv = getClassHaskellConversion cls
forM_ (classHaskellConversionToCppFn conv) $ \_ -> do
hsHsType <- cppTypeToHsTypeAndUse HsHsSide (objT cls)
let dataTypeName = toHsDataTypeName' Nonconst cls
ptrHsType = HsTyCon $ UnQual $ HsIdent dataTypeName
encodeFnType = HsTyFun hsHsType $ HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahP.IO") ptrHsType
addImports $ mconcat [importForPrelude, importForRuntime]
ln
saysLn [classEncodeReexportName, " :: ", prettyPrint encodeFnType]
saysLn [classEncodeReexportName, " = QtahFHR.encodeAs (QtahP.undefined :: ", dataTypeName, ")"]
forM_ (classHaskellConversionFromCppFn conv) $ \_ -> do
hsHsType <- cppTypeToHsTypeAndUse HsHsSide (objT cls)
let constPtrClassName = toHsPtrClassName' Const cls
thisTyVar = HsTyVar $ HsIdent "this"
decodeFnType = HsQualType [(UnQual $ HsIdent constPtrClassName, [thisTyVar])] $
HsTyFun thisTyVar $
HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahP.IO") hsHsType
addImports $ mconcat [importForPrelude, importForRuntime]
ln
saysLn [classDecodeReexportName, " :: ", prettyPrint decodeFnType]
saysLn [classDecodeReexportName, " = QtahFHR.decode QtahP.. ", toHsCastMethodName' Const cls]
handleEventKind :: [String] -> String -> Class -> Generator ()
handleEventKind path eventKind cls = do
let typeName = toHsDataTypeName' Nonconst cls
addImports $ hsImport1 "Prelude" "($)"
ln
saysLn ["instance Qtah", eventKind, ".", eventKind, " ", typeName, " where"]
indent $ do
saysLn ["on", eventKind, " receiver' handler' = Qtah", eventKind,
".onAny", eventKind, " receiver' $ \\_ qevent' ->"]
indent $
if path == ["Core", "QEvent"]
then sayLn "handler' qevent'"
else do
addImports $ mconcat [hsImport1 "Prelude" "(==)",
importForPrelude,
importForRuntime]
saysLn ["let event' = ", classDownCastReexportName, " qevent'"]
sayLn "in if event' == QtahFHR.nullptr then QtahP.return QtahP.False else handler' event'"
sayQtExport :: [String] -> QtExport -> Generator ()
sayQtExport path qtExport = case qtExport of
QtExport export -> doExport export
QtExportFnRenamed fn rename -> do
addExport rename
sayBind rename $ getFnImportName fn
QtExportClassAndSignals cls sigs -> do
sayExportClass cls
mapM_ sayExportSignal sigs
QtExportEvent cls -> do
sayExportClass cls
addImports $ mconcat [importForEvent, importForSceneEvent]
handleEventKind path "Event" cls
handleEventKind path "SceneEvent" cls
QtExportSceneEvent cls -> do
sayExportClass cls
addImports importForSceneEvent
handleEventKind path "SceneEvent" cls
QtExportSpecials -> do
-- Generate a type synonym for qreal.
addImports importForPrelude
addExport "QReal"
ln
saysLn ["type QReal = ", if qrealFloat then "QtahP.Float" else "QtahP.Double"]
where doExport export = case castExport export of
Just c -> doExportClass c
Nothing -> case castExport export of
Just e -> doExportEnum e
Nothing -> case castExport export of
Just flags -> doExportFlags flags
Nothing -> case castExport export of
Just f -> doExportFunction f
Nothing -> case castExport export of
Just v -> doExportVariable v
Nothing -> return ()
doExportClass cls = sayExportClass cls
doExportEnum e = do
let spec = toHsEnumTypeName' e ++ " (..)"
addExport spec
doExportFlags flags = do
let enum = flagsEnum flags
typeName = toHsFlagsTypeName' flags
typeclassName = toHsFlagsTypeclassName' flags
-- Re-export the data type and typeclass.
addExport typeName
addExport' typeclassName
-- Re-export the entries' bindings.
forM_ (enumValueMapNames $ enumValues enum) $ \words -> do
let words' = enumGetOverriddenEntryName Haskell enum words
addExport $ toHsFlagsBindingName' flags words'
doExportFunction f = addExport $ getFnReexportName f
doExportVariable v = do
addExport $ toHsFnName' $ varGetterExtName v
unless (varIsConst v) $ addExport $ toHsFnName' $ varSetterExtName v
sayExportClass :: Class -> Generator ()
sayExportClass cls = do
addExports $
(toHsValueClassName' cls ++ " (..)") :
(toHsPtrClassName' Const cls ++ " (..)") :
(toHsPtrClassName' Nonconst cls ++ " (..)") :
toHsDataTypeName' Const cls :
toHsDataTypeName' Nonconst cls :
classUpCastConstReexportName :
classUpCastReexportName :
classDownCastConstReexportName :
classDownCastReexportName :
concat [ if isJust $ classHaskellConversionToCppFn $ getClassHaskellConversion cls
then [classEncodeReexportName]
else []
, if isJust $ classHaskellConversionFromCppFn $ getClassHaskellConversion cls
then [classDecodeReexportName]
else []
, sort $ map getCtorReexportName $ classCtors cls
, sort $ map getMethodReexportName $ classMethods cls
]
ln
sayBind classUpCastConstReexportName $ toHsCastMethodName' Const cls
sayBind classUpCastReexportName $ toHsCastMethodName' Nonconst cls
sayBind classDownCastConstReexportName $ toHsDownCastMethodName' Const cls
sayBind classDownCastReexportName $ toHsDownCastMethodName' Nonconst cls
sayClassEncodingFnReexports cls
-- Class constructors and methods don't need to be rebound, because their
-- names don't change.
-- | Generates and exports a @Signal@ definition. We create the signal from
-- scratch in this module, rather than reexporting it from somewhere else.
sayExportSignal :: Signal -> Generator ()
sayExportSignal signal = inFunction "sayExportSignal" $ do
let name = signalCName signal
cls = signalClass signal
ptrClassName = toHsPtrClassName' Nonconst cls
varName = toSignalBindingName signal
addExport varName
let listenerClass = signalListenerClass signal
importWholeModuleForExtName $ classExtName listenerClass
-- Find the listener constructor that only takes a callback.
listenerCtor <-
fromMaybeM (throwError $ concat
["Couldn't find an appropriate ",
show (fromExtName $ classExtName listenerClass),
" constructor for signal ", show name]) $
flip find (classCtors listenerClass) $ \ctor -> fromExtName (ctorExtName ctor) == "new"
let callback = signalCallback signal
paramTypes = map parameterType $ callbackParams callback
-- Also find the 'isValid' method.
isValidMethod <-
fromMaybeM (throwError $ concat
["Couldn't find the isValid method in ",
show listenerClass, " for signal ", show name]) $
find ((RealMethod (FnName "isValid") ==) . methodImpl) $ classMethods listenerClass
callbackHsType <- cppTypeToHsTypeAndUse HsHsSide $ callbackT callback
let varType = HsQualType [(UnQual $ HsIdent ptrClassName, [HsTyVar $ HsIdent "object"])] $
HsTyApp (HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahSignal.Signal") $
HsTyVar $ HsIdent "object")
callbackHsType
internalName = concat
[ fromExtName $ classExtName cls
, "::"
, name
, " ("
, fromExtName $ classExtName listenerClass
, ")"
]
addImports $ mconcat [hsImports "Prelude" ["($)", "(>>)"],
importForPrelude,
importForRuntime,
importForSignal]
ln
saysLn [varName, " :: ", prettyPrint varType]
saysLn [varName, " = QtahSignal.Signal"]
indent $ do
sayLn "{ QtahSignal.internalConnectSignal = \\object' fn' -> do"
indent $ do
saysLn ["listener' <- ",
toHsFnName' $ classEntityForeignName listenerClass listenerCtor,
" object' ",
show (toSignalConnectName signal paramTypes),
" fn'"]
saysLn ["valid' <- ",
toHsFnName' $ classEntityForeignName listenerClass isValidMethod,
" listener'"]
sayLn "if valid'"
indent $ do
sayLn "then QtahP.fmap QtahP.Just $ QtahSignal.internalMakeConnection listener'"
sayLn "else QtahFHR.delete listener' >> QtahP.return QtahP.Nothing"
saysLn [", QtahSignal.internalName = ", show internalName]
sayLn "}"
sayBind :: String -> String -> Generator ()
sayBind name value = saysLn [name, " = ", value]
toSignalBindingName :: Signal -> String
toSignalBindingName = (++ "Signal") . signalHaskellName
toSignalConnectName :: Signal -> [Type] -> String
toSignalConnectName signal paramTypes =
concat $
"2" : -- This is a magic code added by the SIGNAL() macro.
signalCName signal :
"(" :
intersperse "," (map (chunkContents . execChunkWriter . sayType Nothing) paramTypes) ++
[")"]
importWholeModuleForExtName :: ExtName -> Generator ()
importWholeModuleForExtName extName = do
iface <- askInterface
addImports . hsWholeModuleImport . getModuleName iface =<< getModuleForExtName extName