Solved Drag & Drop Undo


I am working on a tag plugin that will have a user drag & dropping another tag onto it, the dragged tag will then be set into a link field for use later. I successfully have the drag and drop working, my issue comes in when I try to implement undo functionality for the drag and drop.

With the code below (from the sdk) I have the tag only accepting tags of the same type as the plugin itself. After the drag and drop it takes two separate undos to successfully revert the link field to its previous state. I've tried the code below with the StartUndo/EndUndo and it always takes the two undos.

#include "c4d.h"
#include "c4d_symbols.h"
#include "lib_hair.h"
#include "main.h"
#include <customgui_descproperty.h>

#include "thairsdkrendering.h"

#define TESTPLUGIN_ID 9000

class HairRenderingTag : public TagData
	INSTANCEOF(HairRenderingTag, TagData)

	virtual Bool Init(GeListNode* node);
	virtual void Free(GeListNode* node);
	virtual Bool Message(GeListNode* node, Int32 type, void* data);
	virtual Bool GetDDescription(GeListNode* node, Description* description, DESCFLAGS_DESC& flags);
	static NodeData* Alloc() { return NewObjClear(HairRenderingTag); }



Bool HairRenderingTag::Init(GeListNode* node)
	BaseContainer* bc = ((BaseList2D*)node)->GetDataInstance();
	return true;

void HairRenderingTag::Free(GeListNode* node)

Bool HairRenderingTag::GetDDescription(GeListNode* node, Description* description, DESCFLAGS_DESC& flags)
	if (!description->LoadDescription(node->GetType()))


	const DescID* singleid = description->GetSingleDescID();

	BaseContainer *dataInstance = ((BaseList2D*)node)->GetDataInstance(); // Get the container for the tag


	DescID cid = DescLevel(1000, DTYPE_GROUP, 0);
	if (!singleid || cid.IsPartOf(*singleid, nullptr))	// important to check for speedup c4d!
		BaseContainer maingroup = GetCustomDataTypeDefault(DTYPE_GROUP);
		maingroup.SetString(DESC_NAME, "Links"_s);
		maingroup.SetInt32(DESC_DEFAULT, 1);
		if (!description->SetParameter(cid, maingroup, DescLevel(TESTPLUGIN_ID)))
			return true;
	cid = DescLevel(1001, DTYPE_BASELISTLINK, 0);
	if (!singleid || cid.IsPartOf(*singleid, NULL))    //     important to check for speedup c4d!
		BaseContainer bc;
		bc = GetCustomDataTypeDefault(DTYPE_BASELISTLINK);
		BaseContainer acceptedObjects;
		acceptedObjects.InsData(TESTPLUGIN_ID, String()); // matrix

		bc.SetContainer(DESC_ACCEPT, acceptedObjects);
		bc.SetBool(DESC_SCALEH, true);

		bc.SetString(DESC_NAME, "Link Field"_s);
		if (!description->SetParameter(cid, bc, 1000))
			return TRUE;


	return SUPER::GetDDescription(node, description, flags);

Bool HairRenderingTag::Message(GeListNode* node, Int32 type, void* data)
	BaseDocument* doc = node->GetDocument();
	if (!doc)
		return TRUE;

	BaseContainer* dataInstance;
	BaseTag* tag = static_cast<BaseTag*> (node);
	if (tag == nullptr)
		return TRUE;

	dataInstance = tag->GetDataInstance();
	if (!dataInstance)
		return TRUE;

	if (type == MSG_DRAGANDDROP)
		DragAndDrop* dnd = static_cast<DragAndDrop*>(data);
		if (!dnd)
			return TRUE;
		// Drag & drop will only accept tags of the plugin type
		if (dnd->type == DRAGTYPE_ATOMARRAY)
			AtomArray* dndid = static_cast<AtomArray*>(dnd->data);

			AutoAlloc<AtomArray> dndidarr;

			if (dndidarr->GetCount() <= 0)
				return TRUE;

			if (dndidarr->GetIndex(0) != nullptr)
				if (dndidarr->GetIndex(0)->GetType() != TESTPLUGIN_ID)
					return TRUE;
				return TRUE;

			BaseObject* tagsobject = tag->GetObject();

			BaseContainer state;

			if (!GetInputState(BFM_INPUT_MOUSE, BFM_INPUT_MOUSELEFT, state))
				return TRUE;

			// Only run when the mouse is released
			if (state.GetInt32(BFM_INPUT_VALUE) == 0)
				BaseTag *attachedTag = (BaseTag*)dndidarr->GetIndex(0);
				doc->AddUndo(UNDOTYPE::CHANGE, tag);
				dataInstance->SetLink(1001, attachedTag);
				return TRUE;

	return SUPER::Message(node, type, data);

Bool RegisterRenderingTag()
	return RegisterTagPlugin(TESTPLUGIN_ID, "Link Test"_s, TAG_MULTIPLE | TAG_VISIBLE, HairRenderingTag::Alloc, "Thairsdkrendering"_s, AutoBitmap("hairrendering.tif"_s), 0);

I'm not sure if I am doing something fundementally wrong with the undos since I have had undos of other types working in other plugins.

Any help would be greatly appreciated.

John Terenece

Hello @JohnTerenece,

Thank you for reaching out to us. There are a few problems here at play before we can talk about your question, let me unpack them in logical order:

  1. In general, I would always avoid handling the undo stack in anything NodeData derived when possible. There are ways which technically work, but because manipulating a node itself modifies the undo stack, you can stumble into odd behaviors and nasty surprises. Conceptually, undo-handling is more something that is intended for dialogs and CommandData plugins.
  2. You do your undo-handling in a place which at least in principle is a valid place, NodeData::Message, but you must still make sure that you do fulfill the threading restrictions by testing GeIsMainThread() before modifying the undo stack. While Cinema 4D itself (tends to) always call NodeData::Message from the main thread, there is no absolute rule that would enforce this. Nor is there a mechanism which would prevent a rogue plugin from sending a message to your atom with C4DAtom::Message from N threads in parallel.
  3. Calling BaseDocument::AddUndo without an enclosing ::StartUndo and ::EndUndo pair is meaningless as in the best case you just push your operation in to the void. This can also be potentially dangerous when another entity does not manage its undo context properly and you then inject your operation into that stack unintentionally.

About your Question

The main problem with your question, apart from the technical problems your code has, is that your question does not make too much sense to me (no offense intended). You say that

after the drag and drop it takes two separate undos to successfully revert the link field to its previous state.

Your code only contains one AddUndo and more importantly also only one scene graph modification (i.e., "undo-worthy" operation), setting the BaseLink at ID 1001 of the tag.

So, the main question would be, what do you want to be consolidated here? There is also the problem that you are managing the undo operations of a node from within its own implementation, which is a big no-no. Node undo's are automated, you should not have to manually add an undo here for modifying the link parameter.

Have you tried the same code without any AddUndo? If that is not the problem, I would have to ask you to clarify what the difference between the actual and expected outcome is, what are the two undo operations you want to see consolidated?


MAXON SDK Specialist

Thanks for the detailed response.

I implemented the GeIsMainThread() into my code and that itself did not change any of the results I am getting which I would expect it to.

Running my code with a StartUndo and EndUndo results in this behavior StartEnd Undo.webm

I have tried the code without any AddUndo and this was the result No AddUndo.webm.

Sorry I wasn't clear in what I wanted the undos to do. As seen in both videos dropping one tag onto the other causes the dropped tag to move and the link field to change, what I am trying to do is have both of those being undone in a single undo so that the link field would be cleared and the dropped tag moved back to its original position.

I know that dragging a tag into the link field itself doesn't require an undo, that part of my code works perfectly fine.

John Terenece

Hey @JohnTerenece,

Thank you for your clarifications.

  1. I should have spotted this earlier, but R20 is far our of scope of support. R21 is the last version for which we provide LTS and direct support is currently provided up to S26. This won't change much about this topic for you, since we now already have started. But it will mean that we cannot provide code examples for this case (also applies to LTS support, we only provide code examples for direct support, i.e., up to 26 at the moment). We might also reject answering future R20 topic of yours.
  2. Checking for GeIsMainThread will not fix your problem at hand, that is why I separated these three points out, but is a general necessity. Violating the threading restrictions will sooner or later lead to access violations and with that crashes.

About your Problem

I am trying to do is have both of those being undone in a single undo so that the link field would be cleared and the dropped tag moved back to its original position.

Yeah, I was suspecting that it would be something like that. This does not work because you are not the owner of the drag and drop operation. Abstractly, it works like this:

1. User starts dragging the BaseTag instance T.
2. T is dragged over the BaseTag instance MT which is wrapping MyTagData
3. MyTagData::Message is being called.
	a. You open an undo item.
	b. You add an undo for the following modification.
	c. You change the BaseLink.
	d. You close your undo item.
4. The user releases the mouse button.
5. Cinema 4D starts an undo item.
6. Cinema 4D calls T.Remove().
7. Cinema 4D calls BaseObject::InsertTag(T) for the host of MT.
8. Cinema 4D adds an undo consolidating these two operations.
9. Cinema 4D closes its undo item.
10. The drag-and drop event has been finalized.

The same applies for the case that when the drag event has already finished you are after, it would be just a bit more complicated to describe. Polling the mouse device on your own to determine when the drag operation has finished, is also not the cleanest thing to do here either, because that information should be conveyed by the drag event on its own via DragAndDrop.msg (unless a message is not provided in this case). So, you cannot join the BaseTag.Remove and BaseObject::InsertTag operations into your operation because you do not own the operations.

What you could try is rejecting a drag and drop event to then carry out the relocation of the tag yourself. Which would then enable you to consolidate things in the manner you want to. But I am not so sure if you can reject drag-and-drop events on this very fundamental level. Because you want to stop moving or copying tags which is not intended to be rejectable. And I am also not sure if R20 yet had all the tools you need for doing this.

Looking at the closest thing, the R21 docs, we can see that there was already the flag DRAGANDDROP_FLAG_FORBID, which would be one part of the puzzle. You should have also a look at DRAGANDDROP_FLAG_MSGVALID and DragAndDrop.msg as it will contain the underlying GUI drag and drop data. You can also have a look at this thematically unrelated Asset API example (for 2023 and higher), where I handled a drag and drop event on a GUI level, i.e., what I did there is related to the content of DragAndDrop.msg.


MAXON SDK Specialist

Thanks for the response.

I figured that something like this would be the case, will have to look into the options you mentioned.

John Terenece