sourceafFancom::Dispatch.fan

using [java] com.jacob.com::Dispatch as JDispatch
using [java] com.jacob.com::DispatchEvents as JDispatchEvents 
using [java] com.jacob.com::Variant as JVariant

**
** Communicates to COM objects.
** 
** Dispatch Surrogates [#surrogate]
** ================================
** Dispatch Surrogates allow classes to mimic the behaviour of their peer COM components - they 
** ensure compilation type safety and consistent reuse. 
** 
** A Dispatch Surrogate is any object that conforms to the following rules. (Think of it as an 
** *implied* interface.)
** 
** A Dispatch Surrogate **must** have either:
**  - a ctor with the signature 'new makeFromDispatch(Dispatch dispatch)' or
**  - a static factory method with the signature 'static MyClass fromDispatch(Dispatch dispatch)'
** 
** A Dispatch Surrogate **must** also have either:
**  - a method with the signature 'Dispatch toDispatch()' or
**  - a field with the signature 'Dispatch dispatch'
**
** An example surrogate would look like:
**
** pre>
**   syntax: fantom
** 
**   using afFancom
**
**   class SpVoice {
**     internal Dispatch dispatch
**
**     new makeFromDispatch(Dispatch dispatch) {
**       this.dispatch = dispatch
**     }
**
**     SpObjectToken? voice {
**       get { dispatch.getProperty("Voice").asType(SpObjectToken#) }
**       set { dispatch.setProperty("Voice", it) }
**     }
** 
**     Int speak(Str text, SpeechVoiceSpeakFlags flags := SpeechVoiceSpeakFlags.SVSFDefault) {
**       dispatch.call("Speak", text, flags).asInt
**     }
**   }
** <pre
** 
** The `Variant.asType` method will happily instantiate and return Dispatch Surrogates, such as 
** 'SpObjectToken' above. 
**
** COM Events [#comEvents]
** =======================
** You can register any class to receive events from COM objects by passing it into the
** [registerForEvents()]`registerForEvents` method. When a COM event is received, Fancom will 
** look for a method on your event sink with the same name as the COM event, prefixed with 'on'.
** 
** Example: If COM fires an event called 'Recognition' your event sink should have a method called
** 'onRecognition()'. The parameters on the event handler should match those raised by COM. 
** Parameters can be the Fantom equivalents including surrogates. A detailed error message is given
** should they not match.
**
** See [Variant Types]`Variant` for parameter mappings.
** 
** pre>
**   syntax: fantom
** 
**    ...
**    dispatch.registerForEvents(this)
**    ...
** 
**    Void onRecognition(Int streamNumber, Variant streamPosition, SpeechRecognitionType recognitionType, ISpeechRecoResult result) {
**      Obj.echo(result.phraseInfo.getText)
**    }
** <pre
** 
** Event handlers may optionally return a Variant that will be 
**   
class Dispatch {
    private const static Log    log         := Utils.getLog(Dispatch#)
    
    ** The JACOB Dispatch this object wraps
    JDispatch dispatch { private set }
    
    private InvocationProxy? invocationProxy

    // ---- Constructors --------------------------------------------------------------------------
    
    internal new makeFromDispatch(JDispatch dispatch) {
        this.dispatch = dispatch
    }

    new makeFromProgId(Str programId) {
        this.dispatch = JDispatch(programId)
    }

    // ---- Properties ----------------------------------------------------------------------------
    
    ** Gets a property from the COM object. 
    Variant getProperty(Str propertyName) {
        return Variant(JDispatch.get(dispatch, propertyName)) 
    }

    // Can't use a field 'cos the get and set types are different
    ** Sets a property on the COM object.
    Void setProperty(Str propertyName, Obj? propertyValue) {
        obj := Helper.toJacobObj(propertyValue) ?: throw FancomErr(Messages.wrongJacobType(propertyValue))
        if (obj is JDispatch)
            // dunno if this is the right logic or not, but it's defo needed for 
            // ISpeechRecognizer.audioInput setProperty
            JDispatch.putRef(dispatch, propertyName, obj)
        else
            JDispatch.put(dispatch, propertyName, obj)
    }

    // ---- Methods -------------------------------------------------------------------------------

    ** Convenience for 'callList()'.
    Variant call(Str methodName, 
        Obj? a := null, Obj? b := null, Obj? c := null, Obj? d := null,
        Obj? e := null, Obj? f := null, Obj? g := null, Obj? h := null) {
        params := [a, b, c, d, e, f, g, h]
        // remove nulls from the end of the list
        while (!params.isEmpty && params.peek == null)
            params.pop
        return callList(methodName, params)
    }
    
    ** Calls a method on the COM object.
    Variant callList(Str methodName, Obj?[] params := [,]) {
        objs := params.map |param| {
            Helper.toJacobObj(param) ?: throw FancomErr(Messages.wrongJacobType(param))
        }
        vars := toVariantArray(objs)
        retVal := Variant(JDispatch.callN(dispatch, methodName, vars))
        
        if (retVal.varType == VarType.VT_ERROR) {
            error := retVal.variant.changeType(VarType.VT_I8.vt).getLong
            throw FancomErrorErr(Messages.dispatchCallReturnedError(error), error)
        }

        // see http://en.wikipedia.org/wiki/HRESULT#Using_HRESULTs
        if (retVal.varType == VarType.VT_HRESULT) {
            hresult := retVal.variant.changeType(VarType.VT_I8.vt).getLong
            if (hresult < 0)
                throw FancomHResultErr(Messages.dispatchCallReturnedInvalidHresult(hresult), hresult)
        }
        
        return retVal
    }
    
    ** Registers the given event sink to receive events from this 'Dispatch' object. Ensure your
    ** sink defines a method called 'onEventName()' for each event you wish to receive. 
    ** 
    ** Note method this will sometimes fail if this Dispatch object was not created via the 
    ** 'makeFromProgId' ctor. 
    Void registerForEvents(Obj eventSink) {
        // TODO: if ProgId was not supplied, try looking it up in from the registry
        // http://stackoverflow.com/questions/8328090/does-anyone-know-any-good-tool-to-convert-clsid-and-progid
        log.info("Registering $eventSink.typeof to receive events")
        
        if (invocationProxy == null) {
            invocationProxy = InvocationProxy()
            events := JDispatchEvents(dispatch, invocationProxy)
        }
        invocationProxy.addEventSink(eventSink)
    }
    
    // ---- Helpers -------------------------------------------------------------------------------

    internal static native JVariant[] toVariantArray(Obj[] objs)

}