SOLVED Right-mouse click on dialog tab

Hi folks,

is it possible to weave a way to catch a right-mouse click on a group of dialog tabs? C4D seems to have one when you click on tabs and other dialog-related areas. I'm referring to the menu that has "Undock" etc in it.

d5d3e696-eea4-42b8-a02c-c86be3e720f8-image.png

This menu doesn't appear in my dialog, so I'm hoping it means there's space for me to make one. I'd like to initiate a pop-up menu when the user right-clicks on one of the tabs.

WP.

Thanks for your efforts @ferdinand, and no probs with the python code, I can interpret (I see a lot of that here now!).

I've tried all sorts of things (including some things from @ferdinand python code) but just can't seem to get a clean example going. So what I've done in the meantime, is to create a bunch of BitmapButtonCustomGui buttons and replace the TabGroupsBegin() with a normal GroupBegin() and just hide/unhide elements myself. It's not really ideal, as I've had to add a bunch of extra code to make this work, but it does kind of work the way I'm after. A right-click on the project 1 button brings up the project 1 settings menu:

301e64bd-7e76-4869-8992-95acd43d0745-image.png

@ferdinand not sure what you want to do with this topic. I'm happy for you to close it if it's easier. But if there is a solution that anyone does have using the standard TabGroupBegin() layout, I'd be really keen on seeing it.

WP.

Hello @wickedp,

Thank you for reaching out to us. While I do understand your question, an important detail remains ambiguous for me; what do you mean with 'a group of dialog tabs'?

  1. A tab group opened with GeDialog::TabGroupBegin or the equivalent markup,
  2. or the tab header of a docked dialog?

For option one you will be able to retrieve command messages in GeDialog::Command when an action has occurred for the tab gadget, this should also include a right click on one of its tabs. If this fails, you could try to resort to BFM_GETCURSORINFO, but I would first try the first option, as it seems likely that it will work.

If you were talking about the second case, this is unfortunately not possible, at least I would say so. The only way to find out for sure, would be to listen to the message stream of your dialog if a message is being sent to one of the methods when the user right clicks on the dialog tab. But even if it were, you could not really open a menu, as there is already the context menu of Cinema 4D.

Cheers,
Ferdinand

Hi @ferdinand,

yep, option 1 was what I was looking at. I have a dynamic series of TabGroupBegin() tabs, and would like to open a context menu on a right-click on them.

I'd tried catching it in Command(), but it seems you can't test for mouse input there? Doing this inside the Command():

if(GetInputState(BFM_INPUT_MOUSE,BFM_INPUT_MOUSERIGHT,MB))
{
	if(MB.GetLong(BFM_INPUT_VALUE) == 1)
	{
		GePrint("Right-click menu found..");
	}
	else
	{
		GePrint("NOT found.");
	}
}

consistently returns "NOT found". I tried sending a Message() from inside Command() to the same dialog but that doesn't do it either. BFM_GETCURSORINFO will occasionally catch it, but you have to fiddle with the mouse to get the right catch.

Update edit: as an additional attempt, I tried GetItemDim() on the tab, but that returns the entire tab area, not the tab 'button' itself.

Seems like this is not really possible?

WP.

Hey @wickedp,

So, I spent some time on this but unfortunately there are also a few other things on my plate, so I did not have too much time to invest on it. Here are my findings for now:

  1. You are right, ::Command is not the right way to go here, since you are effectively unable to distinguish there between right and left mouse button clicks, as the input event already has been consumed inside that method (you have to actually release the mouse button to trigger ::Command).
  2. You then must go with the second approach I proposed, although poorly explained as I now see. You must instead move to GeDialog::Message and do the handling manually (that was what I meant with BFM_GETCURSORINFO).
  3. I tried that, only to find out that in the context of BFM_GETCURSORINFO you are unable to poll the mouse inside ::Message, when you poll for the right mouse button, it will always return 0, although the button is pressed.
  4. But you are able to do it in the context of BFM_INTERACTSTART.
  5. There is still quite a bit to be fleshed out here, the restriction part to the actual tab header thingy, for example, but this will at least open a popup menu when RMB is being pressed (without doing the evaluation all the time for that). I cannot tell you exactly how much of the remaining way is possible, I would have to poke a bit myself.

Find the example code in Python below (sorry, I know that you are on C++, but I really did not have much time today). Just tell me when you want this to be explored further, I will then invest some time next week.

Cheers,
Ferdinand

The code:

"""Opens a popup menu over a GeDialog when the right mouse button is being pressed.

This can be run in the Script Manger.
"""

import c4d

class TabDialog (c4d.gui.GeDialog):
    """Provides a dialog with two tabs.
    """
    ID_GRP_MAIN: int = 1000
    ID_GRP_TABS: int = 2000
    ID_GRP_TAB_FOO: int = 3000
    ID_GRP_TAB_BAR: int = 3001

    def CreateLayout(self) -> bool:
        """Adds the gadgets to the dialog.
        """
        flags: int = c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT

        self.GroupBegin(TabDialog.ID_GRP_MAIN, flags, 1)
        self.TabGroupBegin(TabDialog.ID_GRP_TABS, flags, c4d.TAB_TABS)

        self.GroupBegin(TabDialog.ID_GRP_TAB_FOO, flags, 1, 0, "Foo")
        self.GroupEnd()  # ID_GRP_TAB_FOO
        self.GroupBegin(TabDialog.ID_GRP_TAB_BAR, flags, 1, 0, "Bar")
        self.GroupEnd()  # ID_GRP_TAB_BAR

        self.GroupEnd()  # ID_GRP_TABS
        self.GroupEnd()  # ID_GRP_MAIN

        return super().CreateLayout()

    def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
        """Opens the popup menu.
        """
        # Bail when this not the start of a GUI interaction.
        if msg.GetId() != c4d.BFM_INTERACTSTART:
            return super().Message(msg, result)

        # Poll for the right mouse button being pressed.
        state: c4d.BaseContainer = c4d.BaseContainer()
        if not c4d.gui.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSERIGHT, state):
            raise RuntimeError("Could not poll input state.")

        # Bail when RMB is not being pressed.
        if state[c4d.BFM_INPUT_VALUE] != 1:
            return super().Message(msg, result)

        # Open a popup menu at the cursor.
        x: int = int(state[c4d.BFM_INPUT_X])
        y: int = int(state[c4d.BFM_INPUT_Y])

        bc: c4d.BaseContainer = c4d.BaseContainer()
        bc.InsData(c4d.FIRST_POPUP_ID, f"Cube&i{c4d.Ocube}&")
        bc.InsData(c4d.FIRST_POPUP_ID + 1, f"Sphere&i{c4d.Osphere}&")
        res = c4d.gui.ShowPopupDialog(self, bc, x, y)

        return super().Message(msg, result)


if __name__ == '__main__':
    dialog: TabDialog = TabDialog()
    dialog.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE, defaultw=500, defaulth=500)

Thanks for your efforts @ferdinand, and no probs with the python code, I can interpret (I see a lot of that here now!).

I've tried all sorts of things (including some things from @ferdinand python code) but just can't seem to get a clean example going. So what I've done in the meantime, is to create a bunch of BitmapButtonCustomGui buttons and replace the TabGroupsBegin() with a normal GroupBegin() and just hide/unhide elements myself. It's not really ideal, as I've had to add a bunch of extra code to make this work, but it does kind of work the way I'm after. A right-click on the project 1 button brings up the project 1 settings menu:

301e64bd-7e76-4869-8992-95acd43d0745-image.png

@ferdinand not sure what you want to do with this topic. I'm happy for you to close it if it's easier. But if there is a solution that anyone does have using the standard TabGroupBegin() layout, I'd be really keen on seeing it.

WP.

Hey @wickedp,

well, it depends on how you view the subject. If it is a case of "it would be interesting to see", this would reduce the priority of it for me. The general idea would be to use groups to mask out gadgets, so that you can evaluate where the mouse cursor is hovering over. For the tabs itself, you probably will have to get hacky and divide the width of the tab container by the number of tabs in it or something like this.

Regarding your solution, I thought this form of tabs gadget was a requirement. If it is not, I would recommend having a look at the QuickTabCustomGui. It is basically what you are trying to do with the BitmapButtonCustomGui. It will give you a row of buttons (of which only one can be active) and you must do the actual showing and hiding of the tab body yourself. It is what the Attribute Manger uses for example for its tabs. So, for clarity: QuickTabCustomGui and a TabGroup are not the same and both can be added to a dialog. Using QuickTabCustomGui, you can probably get rid of at least some of the boilerplate code of your BitmapButtonCustomGui solution.

PS: Unless you say differently, I will set your own answer as the solution for this thread.

Cheers,
Ferdinand

Hi @ferdinand,

I was wondering about dividing by the count as well. But not sure if that would work for tabs where there are different widths?

I'll have a think about the gui design and layout. It probably doesn't matter how it looks, so long as it works. Thanks for the QuickTabCustomGui tip as well, I had completely forgotten they existed!

I can probably live with the DIY bitmapbutton approach. It's still a tab system I guess, just not out of the box. But I can get a custom context menu for each 'tab' this way, so it kind of provides me with a solution, albeit with a bit more coding effort on my part.

You can set answer and mark as solved. If I have any follow-up questions I'll pop back in.

WP.

Hey,

You can set answer and mark as solved. If I have any follow-up questions I'll pop back in.

Thanks for the reply!

I was wondering about dividing by the count as well. But not sure if that would work for tabs where there are different widths?

Right, I admittedly did not think of that, but what you could do is measure the width of each tab string with GeClipMap:: or GeUserArea::GetTextWidth and then normalize these values. After that you can apply that to the tab widths you want to calculate. As pseudo-code:

GetItemDim(ID_MY_TABGADGET, ..., gadgetWidth)
averageTabWidth = gadgetWidth / tabCount

titleWidths = (clipmap.GetTextWidth(s) for s in tabTitles)
maxTitleWidth = max(titleWidths)
normalizedTitleWidths = (t / maxTitleWidth for t in titleWidths)

absoluteTabWidths = (averageTabWidth * ntw for ntw in normalizedTitleWidths)

This would not be pixel-perfect and way hackier than at least I would be comfortable with. But if you are stubborn enough, you can make everything work 🙂

Cheers,
Ferdinand