Solved PreferenceData plugin, Text disappears when directory is set

Hello everyone,

I am having this problem that when I try to use a Filename parameter to get the path of a directory,

It either, in case I don't override the GetDParameter and SetDParameter functions does not save the path,
or, in case I do override these functions and set and get the plugin container myself, the path text in the UI disappears.

If someone can have a look I will greatly appreciate it. I created a self contained project illustrating the problem. Just unzip it in the plugins folder and it should work.

I am also going to go ahead and paste the python code here, in case some one can find something just by looking at the code, there is also of course accompanying resource files that are included in the zip file.

Thank you,
Alamgir Nasir

import c4d

PREFERENCE_PLUGIN_ID = 1040402
PREFERENCE_RENDER_PATH = 1000
def GetPreferenceContainer():
  world = c4d.GetWorldContainerInstance()
  if world is None:
    return None

  bc = world.GetContainerInstance(PREFERENCE_PLUGIN_ID)
  if bc is None:
    world.SetContainer(PREFERENCE_PLUGIN_ID, c4d.BaseContainer())
    bc = world.GetContainerInstance(PREFERENCE_PLUGIN_ID)
    if bc is None:
      return None

  return bc

class TestPreference(c4d.plugins.PreferenceData):

  def GetDParameter(self, node, id, flags):
    bc = GetPreferenceContainer()
    if bc is None:
      return False

    # Retrieves either check or number preference value
    paramID = id[0].id
    if paramID == PREFERENCE_RENDER_PATH:
      return (True, bc.GetFilename(PREFERENCE_RENDER_PATH), flags | c4d.DESCFLAGS_GET_PARAM_GET)
    return False

  def SetDParameter(self, node, id, data, flags):
    bc = GetPreferenceContainer()
    if bc is None:
      logger.error("SetDParameter: bc is none.")
      return False

    # Changes either check or number preference value
    paramID = id[0].id
    if paramID == PREFERENCE_RENDER_PATH:
      bc.SetFilename(PREFERENCE_RENDER_PATH, data)
      return (True, flags | c4d.DESCFLAGS_SET_PARAM_SET)
    return False

  def Register(self):
    print("Registered test preferences")
    return c4d.plugins.RegisterPreferencePlugin(
      id=PREFERENCE_PLUGIN_ID, g=TestPreference, name="TEST",
      description="testpreference", parentid=0, sortid=0)

TestPreference().Register()

Hi @potashalum, first of all, welcome in the plugincafe community!

Regarding the issue, as you may know, in Python there is no Filename object. So when you call

return (True, bc.GetFilename(PREFERENCE_RENDER_PATH), flags | c4d.DESCFLAGS_GET_PARAM_GET)

bc.GetFilename(PREFERENCE_RENDER_PATH) returns str, so it set a string as a parameter while the description expects a Filename.

To do so simply replace by GetCustomDataType (note it will print an error, but it's actually working, so you will need a try/except block. But I will investigate the error and fix it for a future release).

Then with that's said your GetPreferenceContainer is actually wrong. I replaced it with GetContainer.
Here the full code working

import c4d

PREFERENCE_PLUGIN_ID = 1040402
PREFERENCE_RENDER_PATH = 1000

def GetContainer(node=None):
  bc = None
  if node is None:
    plug = c4d.plugins.FindPlugin(PREFERENCE_PLUGIN_ID, c4d.PLUGINTYPE_PREFS)
    if plug is None:
        return
    bc = plug.GetDataInstance()
  
  else:
    bc = node.GetDataInstance()

  return bc

class TestPreference(c4d.plugins.PreferenceData):

  def GetDParameter(self, node, id, flags):
    bc = GetContainer(node)
    if bc is None:
      return False

    # Retrieves either check or number preference value
    paramID = id[0].id
    if paramID == PREFERENCE_RENDER_PATH:
      try:
        return (True, bc.GetCustomDataType(PREFERENCE_RENDER_PATH), flags | c4d.DESCFLAGS_GET_PARAM_GET)
      except:
        return False
    return False

  def SetDParameter(self, node, id, data, flags):
    bc = GetContainer()
    if bc is None:
      print ("SetDParameter: bc is none.")
      return False

    # Changes either check or number preference value
    paramID = id[0].id
    if paramID == PREFERENCE_RENDER_PATH:
      bc.SetFilename(PREFERENCE_RENDER_PATH, data)
      return (True, flags | c4d.DESCFLAGS_SET_PARAM_SET)
    return False

  def Register(self):
    print("Registered test preferences")
    return c4d.plugins.RegisterPreferencePlugin(
      id=PREFERENCE_PLUGIN_ID, g=TestPreference, name="TEST",
      description="testpreference", parentid=0, sortid=0)

TestPreference().Register()

I will add a note in the documentation about Filename.

If you have any question, please let me know.
Cheers,
Maxime.

Thank you for your help @m_adam, but I am still having the same problem.

Using GetCustomDataType doesn't give me any errors but it doesn't work either. It still clears the string immediately after I set the field.

It does seem like the problem is still with the GetDParameter, because if I comment it out everything works except that the filename isn't loaded when cinema is restarted.

Hi @potashalum did you update your GetPreferenceContainer function as well?

By copy/pasting the code provided you also get the issue?
Cheers,
Maxime.

Hi @m_adam,

I updated the GetPreferenceContainer function as well, it did help in that the SetDParameter function works now, which didn't before. So that is definitely better. But the GetDParameter function didn't work, even if I only use your code.

Could it be a problem in the res files? Would you mind taking a look please. I tried my best to get those right but maybe I still have an error there.

Regards,
Alamgir

Hi @potashalum, I'm terribly sorry I missed your reply when I came back from holiday.

After digging more into this, it's currently not possible to handle Filename from python in GetDParameter of a world preference.
A workaround would be to store parameter as a string, display a string instead of a FileName and make a button to define this string parameter.

Cheers,
Maxime.

Hi @m_adam,

No problem, I hope you had great holidays and thank you for confirming the problem.

Regards,
Alamgir

Hi @m_adam ,

I also have a problem with filename in the preference . It has the same problem as @potashalum did, and I look for the Github py-preference example , I notice that a note Filename parameter type are not supported. .

But I fine some build-in res file has the FILENAME attribute (like ...\description\prefsbrowser.res) , and it can actually read and set filename in preference , Is it fixed now and miss some information that I failed to do this in python😞

For the suggestion of the example, I try to fake it with a bitmap , but I failed draw a bitmap in res file ,could you please update the Github example a little for this ,or some tricks and tips?

Thanks!

Hi @Dunhou, the issue is only a python one since in python there is no Filename type so there is no way for you to return a default value, or you will have no way to read/write the value and therefore store them and value will be lost after each restart.

Regarding bitmap you need to define a BITMAPBUTTON in the res then in the Message method of your NodeData you should react to the MSG_DESCRIPTION_GETBITMAP and fed the bitmap icon.

So with that's said here an example adapted from py-preference plugin
The .pyp file:

import c4d

# Unique plugin ID obtained from www.plugincafe.com
PLUGIN_ID = 1039699

# Unique plugin ID for world preference container obtained from www.plugincafe.com
WPREF_PYPREFERENCE = 1039700

# ID for the World Preference Container parameter
WPREF_PYPREFERENCE_STRING = 1000
WPREF_PYPREFERENCE_BUTTON = 1001


class PreferenceHelper(object):

    @staticmethod
    def GetPreferenceContainer():
        """Helper method to retrieve or create the WPREF_PYPREFERENCE container instance stored in the world container.

        Returns:
            c4d.BaseContainer: The container instance stored in the world container.

        Raises:
            RuntimeError: The BaseContainer can't be retrieved.
            MemoryError: The BaseContainer can't be created.
        """
        # Retrieves the world container instance
        world = c4d.GetWorldContainerInstance()
        if world is None:
            raise RuntimeError("Failed to retrieve the world container instance.")

        # Retrieves the container of our plugin, stored in the world container instance
        # Parameter values will be stored in this container.
        bc = world.GetContainerInstance(WPREF_PYPREFERENCE)

        # If there is no container, creates one
        if bc is None:
            # Defines an empty container
            world.SetContainer(WPREF_PYPREFERENCE, c4d.BaseContainer())

            # Retrieves this empty container instance
            bc = world.GetContainerInstance(WPREF_PYPREFERENCE)
            if bc is None:
                raise MemoryError("Failed to create a BaseContainer.")

        return bc

    def InitValues(self, descId, description=None):
        """Helper method to define type and default value of parameter

        Args:
            descId (c4d.DescID): The parameter ID describing the type and the ID of the parameter you want to initialize.
            description (c4d.Description, optional): The description of the PreferenceData. Defaults to None.

        Returns:
            True if success otherwise False.
        """
        # Retrieves the world BaseContainer of this preference, where values have to be defined
        bc = self.GetPreferenceContainer()

        # Defines default values
        paramId = descId[0].id
        if paramId == c4d.PYPREFERENCE_STRING:
            self.InitPreferenceValue(WPREF_PYPREFERENCE_STRING, "File", description, descId, bc)

        return True


class Preference(c4d.plugins.PreferenceData, PreferenceHelper):

    def Init(self, node):
        """Called by Cinema 4D on the initialization of the PreferenceData, the place to define the type of object.

        Args:
            node (c4d.GeListNode): The instance of the PreferenceData.

        Returns:
            True if the initialization success, otherwise False will not create the object.
        """
        # Init default values
        bc = self.GetPreferenceContainer()
        self.InitValues(c4d.DescID(c4d.DescLevel(c4d.PYPREFERENCE_STRING, c4d.DTYPE_STRING, 0)))

        return True

    def SetDParameter(self, node, id, data, flags):
        """Called by Cinema 4D, when SetParameter is call from the node.

        The main purpose is to store the data in the world container.

        Args:
            node (c4d.GeListNode): The instance of the PreferenceData.
            id (c4d.DescID): The parameter Id.
            data (Any): the data, the user defines and we have to store.
            flags (DESCFLAGS_SET): The input flags passed to define the operation.

        Returns:
            Union[Bool, tuple(bool, Any, DESCFLAGS_SET)]: The success status or the data to be returned.
        """

        # Retrieves the world BaseContainer of this preference, where values have to be defined
        bc = self.GetPreferenceContainer()
        # Retrieves the parameter ID changed
        paramID = id[0].id

        # Store the values in the World Container
        if paramID == c4d.PYPREFERENCE_STRING:
            bc.SetString(WPREF_PYPREFERENCE_STRING, data)
            return True, flags | c4d.DESCFLAGS_SET_PARAM_SET

        if paramID == c4d.PYPREFERENCE_BUTTON:
            return True, flags | c4d.DESCFLAGS_SET_PARAM_SET
        return True

    def GetDParameter(self, node, id, flags):
        """Called by Cinema 4D, when GetParameter is call from the node.
        
        The main purpose is to return the data from the world container.

        Args:
            node (c4d.GeListNode): The instance of the PreferenceData.
            id (c4d.DescID): The parameter Id.
            flags (DESCFLAGS_GET): The input flags passed to define the operation.

        Returns:
            Union[Bool, tuple(bool, Any, DESCFLAGS_GET)]: The success status or the data to be returned.
        """
        # Retrieves the world BaseContainer of this preference, where values have to be retrieved
        bc = self.GetPreferenceContainer()
        # Retrieves the parameter ID asked
        paramID = id[0].id

        # Returns the values from the World Container
        if paramID == c4d.PYPREFERENCE_STRING:
            return True, bc.GetString(WPREF_PYPREFERENCE_STRING), flags | c4d.DESCFLAGS_GET_PARAM_GET

        # Instantiate a BitmapButtonStruct to be used by the Bitmap parameter
        if paramID == c4d.PYPREFERENCE_BUTTON:
            bbs = c4d.BitmapButtonStruct(node, id, 0)
            return True, bbs, flags | c4d.DESCFLAGS_GET_PARAM_GET
        return True

    def Message(self, node, type, data):
        # Determine the icon to use for the WPREF_PYPREFERENCE_BUTTON
        if type == c4d.MSG_DESCRIPTION_GETBITMAP:
            if data['id'][0].id == c4d.PYPREFERENCE_BUTTON:

                iconOld = c4d.gui.GetIcon(1039689)
                icon = c4d.IconData()
                icon.bmp = iconOld['bmp']
                icon.x = iconOld['x']
                icon.y = iconOld['y']
                icon.w = iconOld['w']
                icon.h = iconOld['h']
                icon.flags = c4d.ICONDATAFLAGS_NONE
                data['bmp'] = icon.GetClonePart()
                data['bmpflags'] = c4d.ICONDATAFLAGS_NONE
                return True

        # When a user click on the Button
        if type == c4d.MSG_DESCRIPTION_COMMAND:
            if data['id'][0].id == c4d.PYPREFERENCE_BUTTON:
                path = c4d.storage.LoadDialog(c4d.FILESELECTTYPE_ANYTHING, "Select a file", c4d.FILESELECT_LOAD)
                if path:
                    # Retrieves the world BaseContainer of this preference, where values have to be stored and save the path
                    bc = self.GetPreferenceContainer()
                    bc.SetString(WPREF_PYPREFERENCE_STRING, path)
                return True

        return True


if __name__ == '__main__':
    c4d.plugins.RegisterPreferencePlugin(id=PLUGIN_ID,
                                         g=Preference,
                                         name="Py-Preference",
                                         description="pypreference",
                                         parentid=0,
                                         sortid=0)

The pypreference.str

STRINGTABLE pypreference
{
	pypreference "Py-Preference";

	PYPREFERENCE_STRING					"URL";
	PYPREFERENCE_BUTTON					"";
}

The pyprefrence.h

#ifndef PYPREFERENCE_H__
#define PYPREFERENCE_H__

enum
{
	PYPREFERENCE_MAIN_GROUP					= 999,
	PYPREFERENCE_STRING						= 1000,
	PYPREFERENCE_BUTTON						= 1001,

	PYPREFERENCE_DUMMY
};

#endif // PYPREFERENCE_H__

The pyprefrence.res

CONTAINER pypreference
{
	NAME pypreference;

	GROUP PYPREFERENCE_MAIN_GROUP
	{
		DEFAULT 1;
		COLUMNS 2;

		STRING PYPREFERENCE_STRING		    { }
		BITMAPBUTTON PYPREFERENCE_BUTTON	{ }
	}
}

Just a side note previously, I don't remember exactly when it happened but the button was a text "..." and not a bitmap as it is right now.

Cheers,
Maxime.

@m_adam Thanks a lot for this example 👏 A big shout out !