SOLVED Send Message to CommandData

Hello,

I try to send a message to a CommandData plugin like this :

class MyPlugin(plugins.CommandData):

    def Message(self, type, data):
        print("test", type, data)
        return True

if __name__ == "__main__" :

    plugins.RegisterCommandPlugin(id=1057321,
                                  str="test",
                                  info=0,
                                  help="",
                                  dat=MyPlugin(),
                                  icon=None
    )

And then send the message like this in the console :

c4d.plugins.FindPlugin(1057321).Message(123)

It returns True but nothing print. Do you know why ?

Here is some example code of what I mean:

# Python 2.7
import c4d
from c4d import plugins
from c4d import bitmaps
import os

CMD_PLUGIN_ID = 1000001
TRIGGER_PLUGIN_ID = 1000002

kBaseMessageID = c4d.ID_TREEVIEW_FIRST_NEW_ID;
kTriggerMessageID = kBaseMessageID + 1
kAnotherTriggerMessageID = kBaseMessageID + 2
# ... etc

# =============== CommandData class =============
	
class TestCommandData(c4d.plugins.CommandData):
	
	def Execute(self, doc):
		return True
		
	def ExecuteSubID(self, doc, subid):
		if subid == kTriggerMessageID:
			# we got triggered .. do something
			print "TestCommandData was trigger for ", subid
		return True

class TriggerCommandData(c4d.plugins.CommandData):
	
	def Execute(self, doc):
		print "TriggerCommandData calls TestCommandData.ExecuteSubID with value", kTriggerMessageID
		c4d.CallCommand(CMD_PLUGIN_ID, kTriggerMessageID)
		return True
		
# =============== Main =============
		
def PluginMain():
	bmp = c4d.bitmaps.BaseBitmap()
	dir, file = os.path.split(__file__)
	fn = os.path.join(dir, "res", "test.png")
	bmp.InitWith(fn)
	plugins.RegisterCommandPlugin(
		id=CMD_PLUGIN_ID,
		str="TestCommand",
		info=0,
		icon=bmp, 
		help="TestCommand",
		dat=TestCommandData())
	plugins.RegisterCommandPlugin(
		id=TRIGGER_PLUGIN_ID,
		str="TriggerCommand",
		info=0,
		icon=bmp, 
		help="TriggerCommand",
		dat=TriggerCommandData())

	return
		
if __name__ == "__main__":
	PluginMain()

The above registers 2 CommandData plugins: "TestCommandData" and " TriggerCommandData".
When you select the "TestCommandData" from the plugin menu -> nothing happens.
When you select the "TriggerCommandData" it will call the "TestCommandData" with a subid.
This mimics sending the CommandData a message.
In TestCommandData.ExecuteSubID you then react to the particular "custom message ID". You can have as many of these as you want, and can react differently to each.

NOTE: the source of the trigger does not need to be another CommandData, it can be anything you like. The only thing you need to do is to perform a c4d.CallCommand(<pluginid>, <trigger-messageid>) from where you want to trigger your CommandData, and provide the trigger-messageid you want the CommandData to react on.

Now, if you also want to pass data ... that's another story, indeed.
Maybe have a look at c4d.SpecialEventAdd (but I guess this is a CoreMessage which does only get sent to MessageData type of plugins)

Not sure if this the reason but according to the documentation the Message from CommandData responds to messages of type MSG_COMMAND. Since you're sending a message with type 123, I can only imagine the CommandData does not react to this type of message.

What I have done in the past to trigger a CommandData is use the ExecuteSubID.
I define a value for a custom message, making sure to use a value higher than ID_TREEVIEW_FIRST_NEW_ID
and perform a CallCommand(<command-pluginID>, <custom-message-value>).
In the CommandData I then react to the specific custom-message-value passed in ExecuteSubID.
Granted, there might be other workarounds to message a CommandData, but the way I have done worked for me.

Note: Make sure to use a value higher than ID_TREEVIEW_FIRST_NEW_ID in order not to conflict with internal values defined for menus and handled in the CommandData GetSubContainer

Mm, it did not work for me, the message was still not triggered even with a message ID higher than ID_TREEVIEW_FIRST_NEW_ID.

I managed in a different way with PluginMessage and c4d.GePluginMessage

But contrarely to what say doc, it's not the easiest way if you want to pass some data lol, the data received from PluginMessage is not an object but a PyCapsule object.

Here is how I retreived the original data object, it's not very elegant but it works :

import ctypes
import _ctypes

def getObjectFromCapsule(capsule):
    ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
    ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
    pointer = ctypes.pythonapi.PyCapsule_GetPointer(capsule, None)
    return _ctypes.PyObj_FromPtr(pointer)

def PluginMessage(id, data):
    if id == MY_UNIQUE_ID :
        o = getObjectFromCapsule(data)
        o['foo'] = 'bar'
        return True
    return False

And then in my other plugin :

o = {}
print(c4d.GePluginMessage(MY_UNIQUE_ID, o)) # {'foo': 'bar'}

@césar-vonc said in Send Message to CommandData:

Mm, it did not work for me, the message was still not triggered ...

Sorry to have confused you.
The solution I mentioned was not to trigger the Message function of a CommandData. But the explicitly call its ExecuteSubID.

Here is some example code of what I mean:

# Python 2.7
import c4d
from c4d import plugins
from c4d import bitmaps
import os

CMD_PLUGIN_ID = 1000001
TRIGGER_PLUGIN_ID = 1000002

kBaseMessageID = c4d.ID_TREEVIEW_FIRST_NEW_ID;
kTriggerMessageID = kBaseMessageID + 1
kAnotherTriggerMessageID = kBaseMessageID + 2
# ... etc

# =============== CommandData class =============
	
class TestCommandData(c4d.plugins.CommandData):
	
	def Execute(self, doc):
		return True
		
	def ExecuteSubID(self, doc, subid):
		if subid == kTriggerMessageID:
			# we got triggered .. do something
			print "TestCommandData was trigger for ", subid
		return True

class TriggerCommandData(c4d.plugins.CommandData):
	
	def Execute(self, doc):
		print "TriggerCommandData calls TestCommandData.ExecuteSubID with value", kTriggerMessageID
		c4d.CallCommand(CMD_PLUGIN_ID, kTriggerMessageID)
		return True
		
# =============== Main =============
		
def PluginMain():
	bmp = c4d.bitmaps.BaseBitmap()
	dir, file = os.path.split(__file__)
	fn = os.path.join(dir, "res", "test.png")
	bmp.InitWith(fn)
	plugins.RegisterCommandPlugin(
		id=CMD_PLUGIN_ID,
		str="TestCommand",
		info=0,
		icon=bmp, 
		help="TestCommand",
		dat=TestCommandData())
	plugins.RegisterCommandPlugin(
		id=TRIGGER_PLUGIN_ID,
		str="TriggerCommand",
		info=0,
		icon=bmp, 
		help="TriggerCommand",
		dat=TriggerCommandData())

	return
		
if __name__ == "__main__":
	PluginMain()

The above registers 2 CommandData plugins: "TestCommandData" and " TriggerCommandData".
When you select the "TestCommandData" from the plugin menu -> nothing happens.
When you select the "TriggerCommandData" it will call the "TestCommandData" with a subid.
This mimics sending the CommandData a message.
In TestCommandData.ExecuteSubID you then react to the particular "custom message ID". You can have as many of these as you want, and can react differently to each.

NOTE: the source of the trigger does not need to be another CommandData, it can be anything you like. The only thing you need to do is to perform a c4d.CallCommand(<pluginid>, <trigger-messageid>) from where you want to trigger your CommandData, and provide the trigger-messageid you want the CommandData to react on.

Now, if you also want to pass data ... that's another story, indeed.
Maybe have a look at c4d.SpecialEventAdd (but I guess this is a CoreMessage which does only get sent to MessageData type of plugins)

Aah ok, I misunderstood indeed. Thanks a lot for your complete answer. 🙂
I take a look on all of this.

Hello @César-Vonc,

thank you for reaching out to us and thank you @C4DS for taking the time to answer the question. I think the solution by @C4DS is quite clever, and I do not see speaking much against it. But let us unpack some other stuff first.

@césar-vonc said in Send Message to CommandData:

class MyPlugin(plugins.CommandData)

MyPlugin is an implementation CommandData with an implementation of CommandData.Message attached to it. CommandData is derived from c4d.plugins.BaseData. Which is just a dummy interface to bind all the plugin interfaces together, i.e., does nothing from a user point of view.

@césar-vonc said in Send Message to CommandData:

c4d.plugins.FindPlugin(1057321).Message(123)

c4d.plugins.FindPlugin however, will return a c4d.plugins.BasePlugin. So this is not an instance of your MyPlugin implementation, but a surrogate for it. This is not something that only does happen for CommandData plugins, but for all plugin types: They are separated into an implementation layer and an interface layer. For instantiable plugin types this is a bit more straight forward, as the implementation layer is then for example ShaderData and the tangible interface layer is a BaseShader floating around somewhere in a document. It is an intentional design that the user has no access to the implementation layer, but only access to the interface layer. For a shader, or any other kind of node, this means that you are restricted to sending messages to the implementation, while the implementation has full access to the interface layer, to the node(s) which make use of that implementation.

When you call Message() on MyShaderInstanceObj then you are not calling directly MyShaderData.Message(), but Message() on a BaseList2D which is representing your implementation. But for a node, Cinema 4D will pipe behind the scenes the call MyShaderInstanceObj.Message() to MyShaderData.Message(). For a CommandData plugin that is not the case. When you invoke c4d.plugins.FindPlugin(1057321).Message(123), c4d.plugins.FindPlugin(1057321) will be a BasePlugin which then says "Message ID 123, never heard of it, next pls.".

Which brings us to the second problem. The name space for node messages is curated by Maxon. Which means that you cannot just send any message ID unless you get hold of the specific object you want to send messages to (which is in most cases impossible due to the separation of layers). Cinema 4D will simply not propagate any messages that are not defined in the SDK. I.e., you can send them to a node, usually some BaseList2D in a scene graph, but the message chain will then immediately stop with that node. And due to that node being just some generic BaseList2D, it will then also not know what to do with that message.

For nodes there some message IDs which are intended to be used in such cases, and due to being defined by Maxon will also be propagted through the message chain. Most notably MSG_BASECONTAINER and MSG_RETRIEVEPRIVATEDATA. The problem is - and I just tried it - that Cinema 4D won't propagate these from a BasePlugin representing a CommandData to the CommandData.Message implementation.

So long story short: You cannot really send custom messages to a CommandData.Message(). You can:

  1. Use @C4DS quite elegant work-around.
  2. Use core messages when you have an opened dialog.
  3. Shove data around "manually" within the visibility scope of the implementation.

Option two requires an opened plugin GeDialog which receives core messages. Due to having direct access to the CommandData implementation, you then can pipe these messages through. See example at the end for details. Option three just means that within your implementation you can push data around as please. So everything that knows what a MyCommandDataImplementation is, or even knows the concrete instance of it, can communicate with it. This would also allow for a more complex version of option two, ussing a MessageData plugin instead of a GeDialog for recieving the core messages (and therefore does not require an opened dialog).

A lot of text for very little help 😞 But I hope this clears a bit up why this is not working.

Cheers,
Ferdinand

The implementation:

"""Example for using core messages to send messages to a CommandData 
implementation.

As discussed in:
    https://plugincafe.maxon.net/topic/13316/
"""
import c4d

ID_INT_VALUE = 1000

class PluginDialog (c4d.gui.GeDialog):
    """A dialog for a CommandData plugin.
    """
    def __init__(self, host):
        """The dialog is being inited with a binding to the hosting plugin 
        instance.
        """
        if not isinstance(host, c4d.plugins.CommandData):
            raise TypeError(f"Expected an instance of CommandData as 'host'. "
                            f"Received: {type(host)}")
        self._host = host

    def CoreMessage(self, mid, mdata):
        """We react to core messages under our plugin ID and pipe them
        though to the CommandData implementation.
        """
        if mid == self._host.ID_PLUGIN:
            self._host.Message(mid, mdata)
        return c4d.gui.GeDialog.CoreMessage(self, mid, mdata)


class DialogCommandData(c4d.plugins.CommandData):
    """A CommandData plugin with a dialog.
    """
    ID_PLUGIN = 1057346

    def __init__(self):
        """
        """
        self._pluginDialog = None

    def GetPluginDialog(self):
        """Returns the instance bound instance of the plugin dialog.

        Usually I like CommandData plugin dialogs to be bound to the class
        and not the instance, i.e., this then would be a class method. But
        for this approach we have to attach the dialog to an instance, since
        we need to pass it to the dialog, so that it can call Message() -
        which is not a classmethod, i.e., requires an instance reference.
        """
        if self._pluginDialog is None:
            self._pluginDialog = PluginDialog(host=self)
        return self._pluginDialog

    def Message(self, mid, mdata):
        """React to messages.
        """
        # We receive a core message from the dialog.
        if mid == DialogCommandData.ID_PLUGIN:
            print (mid, mdata)
        return True

    def ExecuteOptionID(self, doc, plugid, subid):
        """Opens the dialog.
        """
        result = True
        dialog = self.GetPluginDialog()
        if not dialog.IsOpen():
            result = dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC,
                                 pluginid=DialogCommandData.ID_PLUGIN)

        return result

    def RestoreLayout(self, secret):
        """Restores the dialog on layout changes.
        """
        dialog = self.GetPluginDialog()
        return dialog.Restore(DialogCommandData.ID_PLUGIN, secret)


if __name__ == '__main__':
    myPlugin = DialogCommandData()
    c4d.plugins.RegisterCommandPlugin(
        id=DialogCommandData.ID_PLUGIN,
        str="CommandData Message Examples",
        info=c4d.PLUGINFLAG_COMMAND_OPTION_DIALOG,
        icon=None,
        help="CommandData Message Examples.",
        dat=myPlugin)

You can then send a core message to it from anywhere like shown below. But for this to work, the GeDialog must be opened and visible, otherwise you need the more complex approach with a MessageData plugin.

import c4d
ID_MYPLUGIN = 1057346
# Sends the message data `1`
c4d.SpecialEventAdd(ID_MYPLUGIN, 1)

Hello @César-Vonc,

without any further questions or replies, we will consider this topic as solved by Wednesday and flag it accordingly.

Thank you for your understanding,
Ferdinand