SOLVED Linkbox Dropdown Arrow

I am struggling with getting a LinkBox UI to update when setting the content of the LinkBox from code. Specifically I can not get the arrow on the left of the LinkBox to appear that allows the user to edit the content of the LinkBox.
shader_dropdown.jpg
As soon as I interact with any other element on the UI the LinkBox refreshes and the arrow appears, but I would like to have it happen right away.

Attached is the code that creates and fills the LinkBox as well as the code that dynamically creates the Linkbox when GetDDescription is called.

Bool Material::Message(GeListNode* node, Int32 type, void* data)
{
	case MSG_DESCRIPTION_COMMAND:
        {
            DescriptionCommand* dc = (DescriptionCommand*) data;
			const Int32 msg_id = GetDescriptionCommandMessageId(dc);
			switch (msg_id)
			{
				case:
                        (MATERIALGROUP_DYNAMIC +
                         MATERIALELEMENTS::ADD_MATERIAL_BUTTON))
                        {
                    	    auto doc = mat->GetDocument();
                            if (doc == nullptr)
                            {
                                return;
                            }

                            auto* material = BaseMaterial::Alloc(MAT_ID_material);

                            if (!material)
                                return;

                            doc->StartUndo();

                            doc->InsertMaterial(material);
                            EventAdd();

                            doc->AddUndo(UNDOTYPE_NEWOBJ, material);
                            doc->EndUndo();


                            int index_base = 0;
                            {
                                index_base = MATERIALGROUP_DYNAMIC;
                                BaseContainer* bc = material->GetDataInstance();

                        		bc->SetLink(index_base + MATERIALELEMENTS::SHADER_LINK_MATERIAL, material);
                            }


                            //FAILED Attempts to get the UI to update so far below
                             C4dMainThreadDispatcher::enqueueAction([node]() {
                                                          node->SetDirty(DIRTYFLAGS::DESCRIPTION);
                                                          node->SetDirty(DIRTYFLAGS_ALL);
                                                          EventAdd(EVENT_FORCEREDRAW);
                                                      });


                    		node->SetDirty(DIRTYFLAGS::DESCRIPTION);
                    		node->SetDirty(DIRTYFLAGS_ALL);
                    		EventAdd(EVENT_FORCEREDRAW);

                            break;
                        }
			}
        }
}



Bool Material::GetDDescription(GeListNode* node, Description* description, DESCFLAGS_DESC& flags)
{
    if (!description->LoadDescription(material))
        return false;

	int group_id = MATERIALGROUP_DYNAMIC;


    BaseContainer shader_group_bc = GetCustomDataTypeDefault(DTYPE_GROUP);
	{
        shader_group_bc.SetInt32(DESC_COLUMNS, 2);
        shader_group_bc.SetInt32(DESC_SCALEH, 1);

        description->SetParameter(DescLevel(group_id + MATERIALELEMENTS::SHADER_LINK_PARENT, DTYPE_GROUP, 0), shader_group_bc, group_id);
	    
	}

    {
		BaseContainer bc = GetCustomDataTypeDefault(DTYPE_BASELISTLINK);

		bc.SetString(DESC_NAME, "Shader"_s);
		bc.SetInt32(DESC_CUSTOMGUI, CUSTOMGUI_LINKBOX);

		//list of allowed types
	    BaseContainer ac;
		ac.SetInt32(Mmaterial, 1);
		bc.SetContainer(DESC_ACCEPT, ac);
	    description->SetParameter(DescLevel(group_id + MATERIALELEMENTS::SHADER_LINK_MATERIAL, DTYPE_BASELISTLINK, 0),
                                  bc, group_id + MATERIALELEMENTS::SHADER_LINK_PARENT);
    }

	{
        BaseContainer bc = GetCustomDataTypeDefault(DTYPE_BUTTON);

        bc.SetInt32(DESC_CUSTOMGUI, CUSTOMGUI_BUTTON);
        bc.SetString(DESC_NAME, "Create Material"_s);
        bc.SetInt32(DESC_ANIMATE, DESC_ANIMATE_OFF);

        description->SetParameter(DescLevel(group_id + MATERIALELEMENTS::ADD_MATERIAL_BUTTON, DTYPE_BUTTON, 0), bc,
                                  group_id + MATERIALELEMENTS::SHADER_LINK_PARENT);
    }



    flags |= DESCFLAGS_DESC_LOADED;
    return SUPER::GetDDescription(node, description, flags);
}

As you can see I've tried calling EventAdd both on the C4D main thread as well as directly as well es setting some dirty flags that should trigger a redraw. From debugoutput I can tell, that GetDDescription does get called even multiple times after clicking on the "add Material" button.

Any suggestions are welcome 🙂

cheers,
Lorenz

Hello @lorenzjaeger ,

thank you for reaching out to us.

What jumps to the eye in your code is that you modify the scene graph by adding a material, add events and so on in Material::Message, but then have this C4dMainThreadDispatcher::enqueueAction, which I assume wraps around our ExecuteOnMainThread. This makes little sense to me as you are either on the main thread or not. If you are not, modifying the scene graph, adding undo's, adding events and so on are off limits and could be the cause for your problems. Normally you are within the main thread for NodeData::Message calls, but there is no guarantee for that. I would safeguard the code there with GeIsMainThread and/or defer the execution to the main thread if you are not.

Another probable reason could be that you are using in the snippet below the material's BaseContainer to access its parameters.

{
  index_base = MATERIALGROUP_DYNAMIC;
  BaseContainer* bc = material->GetDataInstance();
  bc->SetLink(index_base + MATERIALELEMENTS::SHADER_LINK_MATERIAL, material);
}

I would replace this with C4DAtom::SetParamter() (Link) to ensure that the change of the parameter is propagated through the material.

If this all does not help, I would ask you to provide code that can be compiled by us, as I would then have to take a look myself. Your code does currently contain ids which are not defined (which would be fixable by us but is also not ideal) and the static function call enqueueAction. We can of course guess and assume about its nature, but that makes the process more time-consuming and ambiguous.

Thank you for your understanding,
Ferdinand

This post is deleted!

@ferdinand thank you for your quick reply! I have tried your suggestions unfortunately to no avail.
I have uploaded a repro case here based on the c4d_sdk simplematerial. I hope this is an easily compilable version for you.

kind regards,
Lorenz

Hello @LorenzJaeger,

thanks, I will have a look.

Cheers,
Ferdinand

Hello @LorenzJaeger,

so, I did some digging and this is unfortunately a bug which cannot be fixed easily by yourself. The little arrow thing needs to be refreshed by the underlying LinkboxGui (or more specifically by its internal implementation). There are two methods which handle setting links, iLinkBoxGui::SetLink and the generic iLinkBoxGui::SetData. The former is called when a user starts a pick session (with the pointer button) and the latter is being called when a LinkboxGui is being set programmatically via C4DAtom::SetParamter() or BaseContainer::SetLink(). Both functions will try to refresh the GUI after setting the data of the GUI, but ::SetData fails in doing so.

As you have already figured out yourself, when the user then either switches tabs in the description of the node or changes the attribute manager mode, the arrow will appear for such an erroneously rendered link box. So, I tried to emulate this via ActiveObjectManager_SetMode() with no luck either, since the state of the attribute manger and node is still the same, i.e., the function will fail (internally this method is a bool) and postponing its execution does not help either. I also did go through some messages and flags (DESCFLAGS_SET::USERINTERACTION, MSG_DESCRIPTION_CHECKUPDATE and GeUpdateUI()) in the hopes that this would give the attribute manager a nudge, with no luck either.

The only way I currently see to solve this is to implement your own CustomGui that includes a LinkboxGui and a Button. When the button is being pressed, you can create your material and then call LinkboxGui::SetLink to set the link and if necessary also call LinkboxGui::Redraw() (but it should not be necessary). Implementing a new custom GUI is a rather labor intensive task.

But you could also wait for this being fixed. I have filed this as a bug in our bug-tracker, but I cannot give you an ETA when this will be fixed.

Cheers,
Ferdinand

Hi @ferdinand,

thank you very much for looking into my issue this thoroughly and providing such a helpful reply 🙂
I will wait and see if/when the bug gets fixed. Meanwhile I will look into creating a custom class.

kind regards,
Lorenz

Hey @LorenzJaeger,

I hope you have not spent too much time on this, but there is an easy fix to this problem. You must send the core message COREMSG_CINEMA_FORCE_AM_UPDATE, i.e., in the context of your code:

              int index_base = 0;
              {
                  index_base = SIMPLEMATERIAL_PARENT_GROUP_DYNAMIC;
                  target_material->SetParameter(index_base + 2, material_to_add, DESCFLAGS_SET::NONE);
                  EventAdd();
                  SpecialEventAdd(COREMSG_CINEMA_FORCE_AM_UPDATE);
              }

I am not sure if we will consider this a bug or an intended limitation, because the lack of an GUI refresh when setting a link parameter might be tied to the threading limitations of the message system. I will keep this thread updated.

Cheers,
Ferdinand

The result:
material.gif

The full method in the context of your code:

Bool SimpleMaterial::Message(GeListNode* node, Int32 type, void* data)
{
	if (type == MSG_UPDATE)
		updatecount++;

	switch (type)
    {
        case MATPREVIEW_GET_OBJECT_INFO:
        {
            MatPreviewObjectInfo* info = (MatPreviewObjectInfo*)data;
            info->bHandlePreview = true;    // own preview handling
            info->bNeedsOwnScene = true;    // we need our own entry in the preview scene cache
            info->bNoStandardScene = false; // we modify the standard scene
            info->lFlags = 0;
            return true;
            break;
        }
        case MATPREVIEW_MODIFY_CACHE_SCENE:
            // modify the preview scene here. We have a pointer to a scene inside the preview scene cache.
            // our scene contains the object
            {
                MatPreviewModifyCacheScene* scene = (MatPreviewModifyCacheScene*)data;
                // get the striped plane from the preview
                BaseObject* plane = scene->pDoc->SearchObject(String("Polygon"));
                if (plane)
                    plane->SetRelScale(Vector(0.1)); // scale it a bit
                return true;
                break;
            }
        case MATPREVIEW_PREPARE_SCENE:
            // let the preview handle the rest...
            return true;
            break;
        case MATPREVIEW_GENERATE_IMAGE:
        {
            MatPreviewGenerateImage* image = (MatPreviewGenerateImage*)data;
            if (image->pDoc)
            {
                Int32 w = image->pDest->GetBw();
                Int32 h = image->pDest->GetBh();
                BaseContainer bcRender = image->pDoc->GetActiveRenderData()->GetData();
                bcRender.SetFloat(RDATA_XRES, w);
                bcRender.SetFloat(RDATA_YRES, h);
                bcRender.SetInt32(RDATA_ANTIALIASING, ANTI_GEOMETRY);
                if (image->bLowQuality)
                    bcRender.SetInt32(RDATA_RENDERENGINE, RDATA_RENDERENGINE_PREVIEWHARDWARE);
                image->pDest->Clear(0, 0, 0);
                // image->lResult = RenderDocument(image->pDoc, bcRender, nullptr, nullptr, image->pDest,
                // RENDERFLAGS::EXTERNAL | RENDERFLAGS::PREVIEWRENDER, image->pThread);
            }
            return true;
            break;
        }
        case MSG_DESCRIPTION_COMMAND:
        {
          BaseMaterial* mat = reinterpret_cast<BaseMaterial*>(node);
          if (mat == nullptr)
            return false;
          
          auto doc = mat->GetDocument();
          if (doc == nullptr)
            return false;

          auto target_material = static_cast<BaseMaterial*>(node);
          if (target_material == nullptr)
            return false;

          if (GeIsMainThread())
          {
            auto* material_to_add = BaseMaterial::Alloc(ID_SIMPLEMAT);
            if (!material_to_add)
              return false;

              doc->StartUndo();
              doc->InsertMaterial(material_to_add);
              EventAdd();
              doc->AddUndo(UNDOTYPE::NEWOBJ, material_to_add);
              doc->EndUndo();

              int index_base = 0;
              {
                  index_base = SIMPLEMATERIAL_PARENT_GROUP_DYNAMIC;
                  target_material->SetParameter(index_base + 2, material_to_add, DESCFLAGS_SET::NONE);
                  EventAdd();
                  SpecialEventAdd(COREMSG_CINEMA_FORCE_AM_UPDATE);
              }
          }
          break;
        }
        case MATPREVIEW_GET_PREVIEW_ID:
        {
            *((Int32*)data) = SIMPLEMATERIAL_MAT_PREVIEW;
            return true;
            break;
        }
    }
  return SUPER::Message(node, type, data);

Hi,

sorry for the late reply. Just got to test out your fix and everything works now. Thank you very much for your persistence!

cheers,
Lorenz