SOLVED Detect new project in Python plugin

Hi all, is there a callback or MessageData ID that my Python plugin can attach to detect new projects? I would like to execute some code every time a user executes File > New Project.
Thanks! Michael

Hello @mheberlein,

thank you for reaching out to us. For what you are trying to do there is unfortunately no convenient path for implementation in Python, but it is certainly possible to do.

There is the message family MSG_DOCUMENTINFO being broadcasted to some atoms of in a scene graph. It is related to the sub messages MSG_DOCUMENTINFO_TYPE which will tell you if a document has been loaded, saved and more. To make use of that message, you will need a NodeData plugin that receives that message in its Message() method. There are unfortunately no good candidate in Python which you could use in your case. In C++ you would use a SceneHookData plugin, but that is not available in Python.

You could use other NodeData derived plugin interfaces like ObjectData or MaterialData which will also retrieve that message, but that probably does not work very well in your setup. So, the best route is probably a timer. Note that not all NodeData derived plugins will receive MSG_DOCUMENTINFO, e.g., you cannot setup shop in a PreferenceData plugin for example, which is also a node, as it won't receive that message.

To take the timer route, implement a GeDialog or MessageData plugin and overwrite their GetTimer() method to receive periodic MSG_TIMER core messages for that timer interval in their CoreMessage() methods. E.g., returning 1000 in GetTimer() will result in a MSG_TIMER core message being sent to that interface in an interval of roughly one second. There you could call a function which manually evaluates if the document has changed by comparing the current document against an identifier for a cached document. Note that I wrote here 'identifier' and not object, as objects get reallocated quite often in Cinema 4D. So, attempting to store an object itself as a reference won't work. Since BaseDocument is a type of node itself, you should use C4DAtom.FindUniqueID() in conjunction with the MAXON_CREATOR_ID id. The snippet below demonstrates the approach in a pseudo-code fashion.

import c4d

class MyMessageData (c4d.plugins.MessageData):
    """A message data implementation.

    I will not show the actual implementation of the message data here, but
    how to securely store a reference to a node (a document) via its UUID.
    """

    def __init__(self):
        """Initializes the attribute storing the UUID of the active document.
        """
        # The UUID of the document this message data has seen last.
        self._cachedDocumentUuid = None

    @staticmethod
    def GetActiveDocumentUuid() -> bytes:
        """Returns the MAXON_CREATOR_ID UUID for the active document.
        """
        doc = c4d.GetActiveDocument()
        if doc is None:
            raise RuntimeError("Could not access active document.")

        uuid = doc.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if uuid is None:
            raise RuntimeError(f"{doc} has no maxon creation marker.")

        return bytes(uuid)

    def CompareCachedUuid(self, uuid: bytes) -> bool:
        """Checks if the passed UUID is different than the cached one.

        Returns #True when they match and #False when they do not. When there
        is a mismatch, the cached UUID will also be updated to the passed on.  
        """
        self._cachedDocumentUuid != uuid:
            self._cachedDocumentUuid = uuid
            return False
        return True

    def CompareDocuments(self) -> None:
        """Called by CoreMessage() on a MSG_TIMER event.
        """
        currentUuid = MyMessageData.GetActiveDocumentUuid()
        if not self.CompareCachedUuid(currentUuid):
            print ("Active document has changed.")
        else:
            print ("Active document is the same.")

You could also mix the MSG_DOCUMENTINFO with the timer approach by injecting a dummy node, a hidden object into the active document via a controlling MessageData to offload the checking work to it and get access to the more elaborate events in the context MSG_DOCUMENTINFO. For a simple usage with a moderate timer value the simple approach should be fine. Note that when you set the timer callback to a low value like 40ms, that your code will then really run all the time, possibly impacting the performance of Cinema 4D.

Cheers,
Ferdinand

Hello @mheberlein,

thank you for reaching out to us. For what you are trying to do there is unfortunately no convenient path for implementation in Python, but it is certainly possible to do.

There is the message family MSG_DOCUMENTINFO being broadcasted to some atoms of in a scene graph. It is related to the sub messages MSG_DOCUMENTINFO_TYPE which will tell you if a document has been loaded, saved and more. To make use of that message, you will need a NodeData plugin that receives that message in its Message() method. There are unfortunately no good candidate in Python which you could use in your case. In C++ you would use a SceneHookData plugin, but that is not available in Python.

You could use other NodeData derived plugin interfaces like ObjectData or MaterialData which will also retrieve that message, but that probably does not work very well in your setup. So, the best route is probably a timer. Note that not all NodeData derived plugins will receive MSG_DOCUMENTINFO, e.g., you cannot setup shop in a PreferenceData plugin for example, which is also a node, as it won't receive that message.

To take the timer route, implement a GeDialog or MessageData plugin and overwrite their GetTimer() method to receive periodic MSG_TIMER core messages for that timer interval in their CoreMessage() methods. E.g., returning 1000 in GetTimer() will result in a MSG_TIMER core message being sent to that interface in an interval of roughly one second. There you could call a function which manually evaluates if the document has changed by comparing the current document against an identifier for a cached document. Note that I wrote here 'identifier' and not object, as objects get reallocated quite often in Cinema 4D. So, attempting to store an object itself as a reference won't work. Since BaseDocument is a type of node itself, you should use C4DAtom.FindUniqueID() in conjunction with the MAXON_CREATOR_ID id. The snippet below demonstrates the approach in a pseudo-code fashion.

import c4d

class MyMessageData (c4d.plugins.MessageData):
    """A message data implementation.

    I will not show the actual implementation of the message data here, but
    how to securely store a reference to a node (a document) via its UUID.
    """

    def __init__(self):
        """Initializes the attribute storing the UUID of the active document.
        """
        # The UUID of the document this message data has seen last.
        self._cachedDocumentUuid = None

    @staticmethod
    def GetActiveDocumentUuid() -> bytes:
        """Returns the MAXON_CREATOR_ID UUID for the active document.
        """
        doc = c4d.GetActiveDocument()
        if doc is None:
            raise RuntimeError("Could not access active document.")

        uuid = doc.FindUniqueID(c4d.MAXON_CREATOR_ID)
        if uuid is None:
            raise RuntimeError(f"{doc} has no maxon creation marker.")

        return bytes(uuid)

    def CompareCachedUuid(self, uuid: bytes) -> bool:
        """Checks if the passed UUID is different than the cached one.

        Returns #True when they match and #False when they do not. When there
        is a mismatch, the cached UUID will also be updated to the passed on.  
        """
        self._cachedDocumentUuid != uuid:
            self._cachedDocumentUuid = uuid
            return False
        return True

    def CompareDocuments(self) -> None:
        """Called by CoreMessage() on a MSG_TIMER event.
        """
        currentUuid = MyMessageData.GetActiveDocumentUuid()
        if not self.CompareCachedUuid(currentUuid):
            print ("Active document has changed.")
        else:
            print ("Active document is the same.")

You could also mix the MSG_DOCUMENTINFO with the timer approach by injecting a dummy node, a hidden object into the active document via a controlling MessageData to offload the checking work to it and get access to the more elaborate events in the context MSG_DOCUMENTINFO. For a simple usage with a moderate timer value the simple approach should be fine. Note that when you set the timer callback to a low value like 40ms, that your code will then really run all the time, possibly impacting the performance of Cinema 4D.

Cheers,
Ferdinand

Thank you for your reply, Ferdinand.
I had a similar timer workaround in mind when I decided that there must be a better, official way to do this.
Will implement it this way now, running every 3-4 seconds should be enough.

Hello @mheberlein,

without any further questions or postings, we will consider this thread as solved by Friday the 4th, February 2022.

Thank you for your understanding,
Ferdinand