SOLVED GeDialog lacking InitValues, when docked and folded

Hi,

I know the subject sounds strange. Unfortunately I have not yet found the time to extract it into a small and clean example code. I am a bit under pressure with my timeline and already lost the entire day, trying to find cause and workaround for the issue.

I am only posting here, because I hope, it rings a bell with somebody and I get a "Duh, you dipshit, is this the first dialog you are writing? Of course you need to do it like so".

So, here's the issue. In a customer's plugin I have a quite complex asynchronous dialog (opened from a CommandData, pretty standard in this way).
Quite a lot relies on InitValues() properly being called.

Usually we get a sequence like this:

  1. CommandData.Execute() opens the dialog.
  2. CreateLayout()
  3. InitValues()
    All fine.

Now, if the dialog is docked into the startup layout, the sequence changes slightly:

  1. CommandData.RestoreLayout()
  2. CreateLayout()
  3. InitValues()
    All fine as well.

My problems start, if the dialog is docked into the layout and folded (e.g. by middle clicking it's title bar).
Now, if C4D starts up, it looks like so:

  1. CommandData.RestoreLayout()
  2. CreateLayout() (important to note, this runs after RestoreLayout() has run completely and exited).
    And then nothing.
    If the user now unfolds the dialog, still nothing. No call to InitValues().

So, I thought, I would need to come up with my own InitValues() call.

By now I have tried the following things:

a) I tried to send a CoreMessage from RestoreLayout(), right before exiting the function. This CoreMessage is never received.
b) EVMSG_CHANGE is also no option. It doesn't happen upon unfolding the dialog. Neither is it received before.
c) As i received no CoreMessage during this phase, I thought I'd pick an arbitrary message, which happens after CreateLayout() has been called and do a one time call to InitValues() there (basically with a flag, which is set in RestoreLayout and then reset after message got received). By this approach all hell break loose. Obviously CreateLayout runs in parallel to the thread calling my Message(), so i ended up with InitValues() being called, while CreateLayout was still running. The splatter in a Dario Argento moving is nothing compared to the results I received this way.
d) This is what I currently use as a workaround, but I deem it a pretty ugly solution. Basically a small state machine. In RestoreLayout a flag is set, saying "coming from restore". At the very end of CreateLayout() in case we are "coming from restore" a flag is set "done with CreateLayout". With the next message (I arbitrarily picked one, which is coming reliably, didn't even check, which it is (id is 1651078253) I check, if I'm coming from restore and create layout is done and then initiate a call to InitValues(). This seems to work for the moment, but of course seems also like a pretty risky and "racy" approach. The "raciness" could be healed by emitting a CoreMessage in this case and then call InitValues when it is received. I dig this. But the entire approach feels so awkward to me...

I must have missed something really fundamental here. Something crucial to properly handle this situation.

Unfortunately searching the forum and browsing the SDK docs, I was not able to find any hints into the direction of my issue.

So, to wrap it up and try to summarize the question in a nutshell:
What is the correct way to get a call to InitValues() after C4D has started up for a dialog that got docked and folded in the startup layout?

Any pointers would be very welcome!
Thanks for your efforts in advance, I really hope the question is as stupid, as I fell right now.

Cheers,
Andreas

Edit: Forgot to set tags... oh, unfortunately I can't set more than seven tags. I'm testing here on Win10. It is a Python plugin. And I have this issue in all versions of C4D I tested, meaning all from R17 up to S24.

Hello Andreas,

thank you for reaching out to us. First, I am sorry for the slight delay, but S24 took its toll in the last days and your problem is not trivial to answer. I have written a little Python plugin and can confirm the behavior reported by you. It also seems not to be a regression but appears to have been present for a longer time - I have tested R20 to S24 -, as you also already did find out.

Untangling why this is happening will take me a bit longer, my suspicion would be that the argument secret provided by CommandData.RestoreLayout and used by GeDialog.Restore is malformed when the dialog is folded. But this does not help much since we cannot do anything about it from the SDK side in Python or C++. Your workaround seems acceptable, although I would agree that piggybacking onto messages is always dangerous. I would propose instead:

  1. Either simply call GeDialog.InitValues() yourself at the end of GeDialog.CreateLayout() or being lazy just set your values at the end of GeDialog.CreateLayout(). The advantage here is that you will have no delay for the values being initlaized.
  2. Or use the slightly more elaborate version I did provide below which makes use of BFM_INITVALUES. It is effectively similar to your approach, but it does not attach itself to BFM_SET_MSG_BITMASK=1651078253 but uses any incoming message and some attributes attached to the dialog. Due to making use of BFM_INITVALUES, this also leaves making the call to GeDialog.InitValues() to Cinema, i.e., is a bit closer to a conformant solution. You can find the code below. This technically could also be done with GeDialog.Timer, but put briefly, I would not see a real benefit in doing this.

Personally, I would probably just go with option one, but there is no guarantee that this won't have any terrible side effects. My educated guess is that it will not, but it is just that: A guess.

I will report back here about the outcome why this is happening and if we are going to consider this to be a bug.

Cheers,
Ferdinand

The code:

"""Example for handling a dialog and the initialization of its gadgets.

Especially handles the buggy behavior of folded dialogs which are part of a
a layout, i.e., have to be restored in this folded state.

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

ID_INT_VALUE = 1000
ID_DIALOG_COMMANDDATA = 1057137


class PluginDialog (c4d.gui.GeDialog):
    """A dialog for a CommandData plugin.
    """

    def __init__(self):
        """Initializator
        """
        # Used to keep track of having to handle InitValues() manually.
        self._hasBeenInitalized = False
        self._hasLayoutDone = False

    def CreateLayout(self):
        """Adds gadgets to the dialog.
        """
        print(f"Running {self}.CreateLayout()")
        self.SetTitle("Docked Dialog")
        self.AddEditSlider(
            id=ID_INT_VALUE, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
        # Right after this, Cinema should send the message BFM_INITVALUES,
        # as documented internally:
        #   BFM_INITVALUES   ...           // Sent after the layout is done.

        # We set these so that we know the layout has been done (as best as
        # we can do in Python)
        self._hasLayoutDone = True
        self._hasBeenInitalized = False
        return True

    def InitValues(self):
        """Initialize the dialog values.
        """
        # Get out when this is a redundant call.
        if self._hasBeenInitalized is True:
            return True

        # Initialize the dialog ...
        print(f"Running {self}.InitValues()")
        self.SetInt32(ID_INT_VALUE, 42)
        # ... and keep track of doing so.
        self._hasBeenInitalized = True
        return True

    def Message(self, msg, result):
        """
        """
        # This is me being paranoid :)
        if not isinstance(msg, c4d.BaseContainer):
            return c4d.gui.GeDialog.Message(self, msg, result)

        # When this message is not an BFM_INITVALUES message itself and we
        # have not initialized the dialog, but already created its layout,
        # then set of a custom BFM_INITVALUES.
        if (msg.GetId() != c4d.BFM_INITVALUES and
            self._hasLayoutDone is True and
                self._hasBeenInitalized is False):
            print(f"Sending custom BFM_INITVALUES")
            initMsg = c4d.BaseContainer(c4d.BFM_INITVALUES)
            initData = c4d.BaseContainer()
            self.Message(initMsg, initData)

        # The dialog handling messages are documented here:
        # 
        # https://developers.maxon.net/docs/Cinema4DCPPSDK/html/
        # page_manual_guimessages.html#page_manual_guimessages_order_gedialog

        return c4d.gui.GeDialog.Message(self, msg, result)


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

    @classmethod
    def GetPluginDialog(cls):
        """Returns the class bound instance of the plugin dialog.
        """
        print(f"Running {cls}.GetPluginDialog()")
        if cls._pluginDialog is None:
            cls._pluginDialog = PluginDialog()
        return cls._pluginDialog

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

        return result

    def RestoreLayout(self, secret):
        """Restores the dialog on layout changes.
        """
        print(f"Running {self}.RestoreLayout()")
        dialog = DialogCommandData.GetPluginDialog()
        return dialog.Restore(ID_DIALOG_COMMANDDATA, secret)


if __name__ == '__main__':
    myPlugin = DialogCommandData()
    c4d.plugins.RegisterCommandPlugin(id=ID_DIALOG_COMMANDDATA,
                                      str="Dialog CommandData",
                                      info=c4d.PLUGINFLAG_COMMAND_OPTION_DIALOG,
                                      icon=None,
                                      help="Bob's your uncle.",
                                      dat=myPlugin)

Hi Ferdinand,

no worries, I already assumed it could take a bit given the recent S24 release.

Thanks a lot for looking into it and finding out the symbol of the message I hooked into.

Given my current architecture, I'm a bit hesitant moving this into CreateLayout(), especially as I need to call InitValues() from quite a number of places myself and I certainly wouldn't want all of the stuff happening in there to happen twice, when the dialog gets initialized. But anyway, that's not your problem. Thanks for investigating it and I will play around and test a bit more to see, what will be the best solution for me. At least I do not feel as stupid anymore as I did last Friday (though a certain base level always remains...).

Cheers and greetings to the entire SDK team,
Andreas

Edit: Feel free to mark this as solved, if it helps with your administration of support cases.

Hi Andreas,

I think I will leave the thread open for now until I have had time to dig down deeper there, which will only happen next week. So that I can here produce a more definitive answer and you can come back in case there are any further questions.

My second example does prevent InitValues from being executed multiple times. Well, the calls are not prevented, but the overwriting of values is. But this is however a bit of a double-sided sword, since CreateLayout is being called more often than one would think, and it is at least for me not clear now how rigorous Cinema 4D is with (re)initializing the values after that. Or more briefly; suppressing InitValues executions could have the side effect of your dialog deviating from an interface behavior the user is accustomed to. Therefore, my second example sets self._hasBeenInitalized = False in CreateLayout to not consume erroneously such events. Successive InitValue calls will however be prevented. But this is also potentially dangerous since there is no guarantee that Cinema 4D does not make use of successive InitValues calls in some fashion you and I are not aware of. I am of course aware of the need of reducing overhead, which is why I did implement it like that.

However, in most cases such overhead will be small, and option one did work for me in the past. Which was why I did recommend it. For me it is the easier route, both from the point of view of simplicity of code as well as design, i.e., the fact that you do not have to think about managing InitValues. But that is a bit a question of taste.

Cheers,
Ferdinand

Hi Ferdinand,

I have seen and studied your examples. I certainly didn't want to sound, as if I wouldn't appreciate your examples. I really do. Yet, my situation is a wee bit more complicated and a release is imminent, so I really need to find the one individual solution, that on the one hand solves the issue, while on the other causes as little new/additional test efforts as possible. Sometimes external requirements stand in the way of elegant solutions... as much as I do regret this myself.

And I'm of course also looking forward to read about the findings of your deeper research and maybe even fixes for this issue. Unfortunately these will come to late for this very project. But this again is no critique, just the reality I face. I certainly don't want you to feel stressed in any way about this, it's nothing you can change or which lies within your responsibility. Furthermore I would need these fixes down to R17 and I am well aware, Maxon can not provide such. Really no bad feelings involved here, these are just the cruel facts. If it wasn't in a public forum, I'd probably describe life with a politically incorrect b-word...

Cheers,
Andreas

Hello @a_block,

so, I had a bit of a deeper look today. Which has been fruitless, as I would have to recompile all of Cinema 4D to get really to the bottom of this; due to the need of adding code to the implementation of GeDialog. Which is due to time-restrictions not really a viable option for me.

But I have good idea why this is happening on a higher level. The problem lies within the handling of the messages BFM_INIT and BFM_INITVALUES which both access the (protected) field createlayout (LINK) of GeDialog. BFM_INIT, i.e., CreateLayout, sets this to true and BFM_INITVALUES, i.e., InitValues(), is only being executed when createlayout is true or bails otherwise (and consumes the message). Although CreateLayout is being called for a folded dialog, something then sets this field to false. And this is the point where I would have to add code to dig deeper, as there is an endless number of calls to these methods, so it is almost impossible to really solve this just with running Cinema 4D with a debugger attached.

Due to a similar case not long ago, similar in the sense that it was also about GeDialog, I would also assume here that we will not fix this, as there is a good chance of breaking something in third party code, considering the age of this "problem" and for internal reasons I cannot disclose. I will however forward this to the developers in case they will see this differently.

Some points:

  • Both methods discussed above, i.e., the "complicated" approach via GeDialog.Message and the "lazy" one via GeDialog.CrateLayout, seem to be equally valid to me after having a bit closer look.
  • Probably not of greatest importance for you, but with S24 we did add GetFolded() to the GeDialog Python bindings, which in principle makes this "relatively clean fix" possible:
    def CreateLayout(self):
        """Adds gadgets to the dialog.
        """
        ...
        # Just circumvent all this createlayout mess
        if self.GetFolding():
            self.InitValues()
        return True

Cheers,
Ferdinand

Thanks for the additional information.
No worries, seeing the new UI evolve in the node editor, I didn't even expect to get a fix for this, nor would it be of much use to me, due to the requirement of versions to support.

I saw the GetFolding(), yep. Nice addition and indeed it would offer the chance to make my workaround a bit more stable, if I wouldn't need it to be stable in versions without GetFolding() that is... 😉

Cheers

Dear community,

this bug has been fixed and will be integrated with an upcoming release of Cinema 4D S24 (hopefully the next one).

Cheers,
Ferdinand