Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Get pressed keys in ObjectData plugin

    Cinema 4D SDK
    c++ 2023
    4
    17
    2.9k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • CJtheTigerC
      CJtheTiger
      last edited by

      My ObjectData plugin provides handles for the user to do things. When pressing a certain key (like Ctrl) I want to change how the handles are displayed (changing the color) as well as change how they behave. I do not know how to get pressed keys.

      Calling GetInputState fails on the Draw call probably because it's not being executed on the main thread.

      I'm on C++ (even though I imagine it being the same for Python).

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @CJtheTiger
        last edited by

        Hello @CJtheTiger,

        Thank you for reaching out to us. Without you sharing code things will get quite speculative here. Please show us how you call GetInputState, your ::Draw method and the other object handle relevant methods. It could be that there is a draw thread restriction on GetInputState, but that seems a bit unlikely. And you are on the main thread in ::Draw, it just that you are on the drawing part of it. So, GeIsMainThread will be true, but GeIsMainThreadAndNoDrawThread will be not.´

        Please refer to Support Procedures: Confidential Data in case you cannot share your code publicly.

        Cheers,
        Ferdinand

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • CJtheTigerC
          CJtheTiger
          last edited by ferdinand

          Thanks @ferdinand.

          The code isn't confidential so here we go:

          DRAWRESULT MyPlugin::Draw(BaseObject* op, DRAWPASS drawpass, BaseDraw* bd, BaseDrawHelp* bh)
          {
              if (drawpass != DRAWPASS::HANDLES)
                  return DRAWRESULT::SKIP;
          
              BaseContainer* data = op->GetDataInstance();
          
              Vector handleColor;
          
              bd->SetMatrix_Screen();
          
              BaseContainer* keyContainer;
              bool isPressed = false;
          
              // This is the line that fails.
              if (GetInputState(BFM_INPUT_KEYBOARD, KEY_CONTROL, *keyContainer))
              {
                  isPressed = keyContainer->GetBool(BFM_INPUT_VALUE);
              }
          
              DiagnosticOutput("Is pressed: @"_s, isPressed);
          
              for (int i = 0; i < GetHandleCount(op); i++)
              {
                  GetHandle(op, i, info);
          
                  if (isPressed)
                  {
                      highlightHandle = true;
                  }
                  else
                  {
                      highlightHandle = false;
                  }
          
                  if (highlightHandle)
                      handleColor = GetViewColor(VIEWCOLOR_SELECTION_PREVIEW);
                  else
                      handleColor = GetViewColor(VIEWCOLOR_ACTIVEPOINT);
          
                  bd->SetPen(handleColor);
          
                  Vector handleScreenSpaceCoordinate = bd->WS(info.position);
          
                  bd->DrawLine2D(handleScreenSpaceCoordinate, bd->WS(mainController->GetAbsPos()));
                  bd->DrawSphere(handleScreenSpaceCoordinate, Vector(10), handleColor, BDRAW_DRAW_SPHERE_FLAGS_NO_SHADING);
              }
          
              return DRAWRESULT::OK;
          }
          

          I also tried wrapping the call to GetInputState in a maxon::ExecuteOnMainThread but that also failed. I don't even know how to get a useful error message out of the exception break.

          Please note that I haven't done any refactoring yet. 🐵 Getting things to work comes first.

          Also please note that highlighting the handles like this is also just to complete this proof of concept. Once I have what I need to make it work pretty much all of this is going to change.

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @CJtheTiger
            last edited by ferdinand

            Hey @CJtheTiger,

            please excuse the short delay, things are piling up left and right right now. And there is also no need to explain the state of your code. We not only actually prefer simple or simplified code, as we then do not have to cut through endless production code, we do not discriminate by good/bad code or beginner/expert developer. All code is welcome and taken seriously 🙂

            About your Problem

            Hm, for me the Python approximation of your code runs fine. Are you sure that the same code does not work in draw, but does work in another main thread method?

            // This is the line that fails.
            if (GetInputState(BFM_INPUT_KEYBOARD, KEY_CONTROL, *keyContainer))
            {
                isPressed = keyContainer->GetBool(BFM_INPUT_VALUE);
            }
            

            What is "failing" here?. GetInputState returns false and your input value polling therefore never happens, or isPressed remains false although it shouldn't? BFM_INPUT_VALUE is also of type Int32, and not bool, so you are calling the wrong BaseContainer method here. The documentation is a bit dodgy on the type of BFM_INPUT_VALUE because it indicates both Int32 and bool, but I would try my luck with BaseContainer::GetInt32 instead because that is the type that is literally mentioned.

            Cheers,
            Ferdinand

            Result:

            GetInputState(KEY_CONTROL): bc[c4d.BFM_INPUT_VALUE] == 1 = True
            GetInputState(KEY_CONTROL): bc[c4d.BFM_INPUT_QUALIFIER] == c4d.QUALIFIER_CTRL = True
            

            Code:

            """Simple example for querying for the CTRL key.
            
            Run the example, press the button and press CTRL while doing so.
            """
            import c4d
            
            class SimpleDialog (c4d.gui.GeDialog):
                """Example dialog that does nothing.
                """
                ID_BTN_BLAH: int = 1000
            
                def CreateLayout(self) -> bool:
                    """Called by Cinema 4D to populate the dialog with gadgets.
                    """
                    self.SetTitle("SimpleDialog")
                    self.AddButton(self.ID_BTN_BLAH, flags=c4d.BFH_SCALEFIT|c4d.BFV_SCALEFIT, name="Blah")
                    return True
                
                def Command(self, mid: int, msg: any) -> bool:
                    """
                    """
                    if mid == self.ID_BTN_BLAH:
                        bc: c4d.BaseContainer = c4d.BaseContainer()
                        if not c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.KEY_CONTROL, bc):
                            raise RuntimeError("Failed to query input events.")
                        
                        print (f"GetInputState(KEY_CONTROL): {bc[c4d.BFM_INPUT_VALUE] == 1 = }")
                        print (f"GetInputState(KEY_CONTROL): "
                               f"{bc[c4d.BFM_INPUT_QUALIFIER] == c4d.QUALIFIER_CTRL = }")
            
                    return True
            
            if __name__ == '__main__':
                dlg: SimpleDialog = SimpleDialog()
                dlg.Open(c4d.DLG_TYPE_ASYNC, 0, defaulth=100, default=100)
            

            MAXON SDK Specialist
            developers.maxon.net

            CJtheTigerC 1 Reply Last reply Reply Quote 1
            • CJtheTigerC
              CJtheTiger @ferdinand
              last edited by CJtheTiger

              @ferdinand don't worry about the delay. I'm super grateful we're getting qualified expert help basically for free.

              By "failing" I meant that a breakpoint is being executed:

              a5ef87af-9f55-4751-93a3-78678720e306-image.png

              I'm actually not positive that this line works on the main thread either. I tried invoking it using maxon::ExecuteOnMainThread but that failed in the same way.

              Changing the type to Int32 did not make a difference.

              I can confirm your Python snippet working (once I change the typo from default to defaultw in the last line) which makes me think that this might have something to do with me being in the Draw of an ObjectData plugin? I wasn't able to get the dialog working in C++ (Open returns false; haven't done a custom dialog in C++ before though so I'd have to read into that first).

              Thanks for your relentless support! ❤

              ferdinandF 2 Replies Last reply Reply Quote 0
              • ferdinandF
                ferdinand @CJtheTiger
                last edited by ferdinand

                Hey @CJtheTiger ,

                hm, I looked up your breakpoint/exception. I think it is this, because that is the only crit stop I found when I followed the call chain of the core function EwBfGetInputEvent:

                Bool BlahInterface::BfGetInputEvent(Int32 askdevice, BaseContainer *res) const
                {
                  // ...
                
                  // we got alow of crashes under macos where we tried to ask for the keystate from a thread.
                  // it's not allowed under macos to poll keyevents,
                  if (!GeIsMainThread())
                  {
                    CriticalStop();
                    return false;
                  }
                

                So, this seems to be indeed a main thread thing. But you should be there on the main thread in ::Draw, or at least I thought so. And escaping there to the main thread from whatever thread you are drawing there in with maxon::ExecuteOnMainThread would probably be a bad idea anyways, as you would then tie these two threads together. Which is probably not want you want to do with drawing routines.

                To be sure, I would simply call GeIsMainThread and GeIsMainThreadAndNoDrawThread in your ::Draw. I would have expected the first one to be true, and the second one to be false. But apparently GeIsMainThread is not true. But that would be odd, since it would mean that we are doing drawing stuff outside of the drawing stage of the main thread.

                As a super ugly hack in the meantime:

                1. Define a field Bool _ctrl_pressed = false; on your ObjectData hook.
                2. In its NodeData::Message, without checking for any message ID.
                  • Check if node is selected, i.e., call BaseList2D::GetBit(BIT_ACTIVE) on it.
                    • If not, set _ctrl_pressed to false and bail.
                  • Otherwise:
                    • Check for being on the main thread.
                    • Poll the ctrl key set _ctrl_pressed to the result.
                3. In your ::Draw, make use of _ctrl_pressed.

                This is of course a super ugly hack, but it would allow you to at least progress for now. I will give the problem a spin next week in C++ myself and then come back here.

                Cheers,
                Ferdinand

                PS: Had a look myself with Python regarding the thread. Will have to dig deeper what is going on there next week, that is not what I would have expected at all. I am also not quite sure that it has been always like that.

                Screenshot 2023-05-26 at 11.14.12.png

                MAXON SDK Specialist
                developers.maxon.net

                1 Reply Last reply Reply Quote 1
                • ferdinandF
                  ferdinand @CJtheTiger
                  last edited by ferdinand

                  Hello @CJtheTiger,

                  Please excuse the long waiting time. I had a look at your problem.

                  1. I was misinformed about the nature of GeIsMainThreadAndNoDrawThread() and ObjectData::Draw, it is normal that ::Draw is executed off main thread from time to time.
                  2. What you are trying to do here is not intended by the developers.
                  3. There is also another problem which I totally overlooked until I gave it spin myself, ::Draw is not the drawing routine but the routine to fill the draw-instructions buffer. I.e., it is not called 20, 50, 60 times per second, but only when the object is dirty.

                  I modified the roundedtube.cpp so that it draws green points on all the vertices of its cache when CTRL is not pressed and red points when it is pressed.

                  f87c2e35-2370-4759-8d2e-043a4054899d-image.png

                  The code is fairly straight forward:

                  // WARNING: THIS CODE DOES NOT DO WHAT THE THREAD AUTHOR WANTS TO DO.
                  
                  /// @brief Gets the current CTRL key state or the cached value.
                  Bool RoundedTube::GetCtrlKeyState()
                  {
                  	if (!GeIsMainThread())
                  		return _ctrlKeyIsPressed;
                  
                  
                  	BaseContainer bc = BaseContainer();
                  	if (GetInputState(BFM_INPUT_KEYBOARD, KEY_CONTROL, bc))
                  		_ctrlKeyIsPressed = bc.GetBool(BFM_INPUT_VALUE);
                  
                  	return _ctrlKeyIsPressed;
                  }
                  
                  /// @brief Draws points on the vertices of the cache of #op.
                  DRAWRESULT RoundedTube::Draw(BaseObject* op, DRAWPASS drawpass, BaseDraw* bd, BaseDrawHelp* bh)
                  {
                  	iferr_scope_handler
                  	{
                  		return DRAWRESULT::SKIP;
                  	};
                  
                  	// Get the generator/deform cache of #op.
                  	if (drawpass != DRAWPASS::OBJECT)
                  		return DRAWRESULT::SKIP;
                  
                  	BaseObject* cache = op->GetCache();
                  	if (MAXON_UNLIKELY(!cache))
                  		return DRAWRESULT::SKIP;
                  
                  	if (cache->GetDeformCache())
                  		cache = cache->GetDeformCache();
                  	if (MAXON_UNLIKELY(cache->GetType() != Opolygon))
                  		return DRAWRESULT::SKIP;
                  
                  	// Get the points of in the cache, we have to copy stuff because the Vector type does not match.
                  	const PolygonObject* const poly = static_cast<const PolygonObject*>(cache);
                  	const Int32 pointCount = poly->GetPointCount();
                  	const Vector* pointData = poly->GetPointR();
                  	maxon::BaseArray<maxon::Vector32> points;
                  	for (Int32 i = 0; i < poly->GetPointCount(); i++)
                  	{
                  		points.Append(Vector32(pointData[i])) iferr_return;
                  	}
                  
                  	// Draw in object space on top of polygons and pick a color.
                  	bd->SetMatrix_Matrix(op, op->GetMg(), 5);
                  	bd->SetPointSize(5.0);
                  	bd->SetPen(GetCtrlKeyState() ? Vector(1, 0, 0) : Vector(0, 1, 0));
                  	bd->DrawPointArray(pointCount, points.GetFirst());
                  
                  	return DRAWRESULT::OK;
                  }
                  

                  But as indicated before, this will not do what you want to be done, or at least what I assume you want to do. Because ::Draw is not called for each frame, but only when Cinema 4D thinks it needs new drawing instructions, which for the most part is when node is dirty. I tried some tricks with flagging myself as dirty when CTRL is pressed, but Cinema 4D does not let itself to be tricked that easily and omits C4DAtom::SetDirty instructions when the data does not actually reflect them.

                  So, before we go into possible solutions, I would point out again, that we are here in "not intended" territory which can range from an uphill battle to straight out impossible.

                  What can I do?

                  The most important question would be: What is the thing/event of which you want to change the color?

                  • If it is just abstract data, as for example the vertices in my example, your life will be tougher.
                  • When you want to color in the handles of your object in handle drag operations, or at least color something in alongside such drag operation, then this is much easier because handle changes will invalidate caches and by the force the draw instruction buffers to be rebuilt.

                  If you do not have that luxury and want to do what my example sketched out, you have little options:

                  • Introduce a dummy parameter ID_DUMMY (with no UI) and set that parameter to a different value each time the CTRL state changes. You would have to poll in an unbound fashion in ::Message and then change ID_DUMMY once you have detected a press/release. This is so hacky I would not touch it with a ten-foot pole. From the poor and uncontrolled update frequency of ::Message up to nasty feedback loops and performance problems, this is a bad idea.
                  • The same as above but with an external MessageData as command and control plugin which informs all objects of type T in a fixed interval of the current CTRL state. A little bit better but still a bad idea.
                  • Implement a SceneHookData that draws your information. So instead of each object of type T drawing its own information, you draw in a centralized fashion in the scene hook. SceneHookData::Draw is called relatively frequently when the user interacts with the viewport but only infrequently when he/she does not. SceneHookData::KeyboardInput cannot be used to draw directly, but here you can probably get the closest.

                  drawhook.gif

                  Bool DrawingSceneHook::Draw(BaseSceneHook* node, BaseDocument* doc, BaseDraw* bd, 
                  	BaseDrawHelp* bh, BaseThread* bt, SCENEHOOKDRAW flags)
                  {
                  	if (flags != SCENEHOOKDRAW::DRAW_PASS)
                  		return true;
                  
                  	const BaseBitmap* const bmp = GetTextBitmap(
                  		maxon::UniversalDateTime::GetNow().ToString(), DRAWTEXTURE_WIDTH, DRAWTEXTURE_HEIGHT);
                  	if (!bmp)
                  		return true;
                  
                  	bd->SetMatrix_Screen();
                  	bd->DrawTexture(bmp, DRAWTEXTURE_POINTS, DRAWTEXTURE_COLORS, DRAWTEXTURE_NORMALS,
                  		DRAWTEXTURE_UVS, 4, DRAW_ALPHA::NONE, DRAW_TEXTUREFLAGS::NONE);
                  
                  	return true;
                  }
                  

                  So, long story short: In an ObjectData plugin, it is quite tricky to have the core logic key stroke event -> redraw. I know that this is often not the advice people want to hear but I would not go down that rabbit hole if I could avoid it.

                  Cheers,
                  Ferdinand

                  MAXON SDK Specialist
                  developers.maxon.net

                  CJtheTigerC 1 Reply Last reply Reply Quote 1
                  • CJtheTigerC
                    CJtheTiger @ferdinand
                    last edited by

                    Hello @ferdinand,

                    I'm so grateful that you're looking into this and providing me with so much information. Step by step I'm getting a grasp on how to work with C4D. Also thank you so much for pointing out bad hacks since those prevent me from getting bad ideas as well as help me understand why they are bad. This is so valuable if I ever want to be able to make things completely on my own.

                    What I actually want to do

                    My ObjectData plugin shows a number of Nulls which are ordered sequentially. All of them should show handles to control an internal value for orientation and size. Think of the controls on a spline point when working with the tangents:

                    40dd41f0-3828-4635-8087-243952065b09-image.png

                    Let's say I have five Nulls. I want each Null to have a handle like a spline point tangent, except for the first and last Nulls, those should only show a "single sided tangent handle". This looks something like this:

                    48483976-014a-4e10-b23f-387910a70d58-image.png
                    566daf78-e2dc-4357-9f5f-971408b307a3-image.png

                    Nulls 0 and 4 therefore have a single handle, while Nulls 1 to 3 each have two handles, one for either side of the imaginary tangent.

                    0cd6a91b-d954-404a-84b9-af9f3de7e096-image.png

                    When hovering over a handle I want to highlight it along with the neighboring handle if there is one:

                    8b6d045f-287b-4d1a-91ec-34b61f01878b-image.png

                    Signalling to the user that he's going to manipulate both of these handles at the same time.

                    And this is where my requirement comes in: When holding Ctrl (or Shift, haven't decided yet) only the hovered handle should be highlighted (and later on also manipulated) while the neighboring should not, so as to indicate that manipulating this handle will not affect the neighboring handle.

                    e76ad4c9-03e1-4def-9b83-11e63b3d30dd-image.png

                    It will then behave similiar to a tangent handle on a spline point when holding Shift.

                    Also let me state that simply using a Spline is not an option due to other features I'd like to provide. Like better spline handles because dear lord I despise those tiny lines and points on the default Spline handles. But that's a story for another time.

                    So there we go, this is my goal.

                    Using a SceneHookData

                    From what I understand (and I have to admit I haven't read much into it yet) this will allow me to catch those keyboard inputs. So my next attempt will be to do just that. However I'm not too fond of performing the draw in there as you suggested. Drawing the handles should still be the responsibility of the ObjectData itself.

                    If this was C# I'd implement a simple observer pattern so the SceneHookData provides a list to which any object implementing a certain interface may subscribe, and whenever one of those keys is pressed the SceneHookData will call a function on those objects to let them know. I'm not sure if I'm fluent enough in C++ to attempt this just yet but that's my preferred way as of know. You know, something like this:

                    // Pseudo-code
                    interface IInputObserver
                    {
                        void InputChanged(InputChangedEventArgs e);
                    }
                    
                    class InputObserverSceneHook : SceneHookData
                    {
                      List<IInputObserver> Observers;
                    
                      override Bool KeyboardInput ()
                      {
                        foreach (IInputObserver observer in this.Observers)
                        {
                          observer.InputChanged(...);
                        }
                      }
                    }
                    
                    class MyObjectDataPlugin : ObjectData, IInputObserver
                    {
                      void Init()
                      {
                        InputObserverSceneHook sceneHook = getInputObserverSceneHook();
                    
                        if (sceneHook != null)
                        {
                          sceneHook.Observers.Add(this);
                        }
                      }
                    }
                    

                    I will update this thread once I get around to do it. Unless you're stopping me in my tracks.

                    PS

                    Just a little personal rant so feel free to skip. 😉

                    The fact that there's not a clean way to do this makes me think that nobody asked for it yet which makes me think that I'm looking for something that noone else is looking for which makes me think that I'm doing something wrong. Which is rather frustrating. Even more so because I wish this wasn't such an issue. My main job (which is not related to C4D) includes designing business processes which make the lifes of users easier and to achieve that I have to think outside the box and come up with new ways to save inputs and "mouse mileage". So I'm really stubborn when it comes to realizing my ideas because once I got a clear view ahead of me I hate making compromises. I know that having this option to use Ctrl will make the user super happy so I'm having trouble accepting that programming this is not intended and that I should look for another approach. But if that's how it's gotta be that's how it's gotta be. 😮‍💨

                    Best regards,

                    CJ

                    ferdinandF 2 Replies Last reply Reply Quote 0
                    • ferdinandF
                      ferdinand @CJtheTiger
                      last edited by

                      Hey @CJtheTiger,

                      just as an FYI, I have not overlooked your answer here, I just had a crazy week. This is the next thing on my desk 🙂

                      Cheers and happy coding,
                      Ferdinand

                      MAXON SDK Specialist
                      developers.maxon.net

                      1 Reply Last reply Reply Quote 1
                      • ferdinandF
                        ferdinand @CJtheTiger
                        last edited by ferdinand

                        Hey @CJtheTiger ,

                        Please excuse the long waiting time.

                        1. As indicated earlier by me, doing what you want to do, is somewhat possible when overwriting the custom handle handling methods of ObjectData instead of using the automated ones.
                        2. There are, however, still cases where this does not work 100%.
                          • For example, when a user clicks a handle, and then presses CTRL to break it, and then releases the mouse button while moving away from the handle also keeping CTRL pressed, the CTRL state will linger even the user now releases the CTRL button. The reason is that in that case there is no event where we can rest our state the handle handling is nor running anymore.
                          • For most cases this will work well enough, but that last inch is probably hard to conquer. But it might be possible when one is stubborn enough. Tying the color of scene elements to a keyboard state is still not really intended in an ObjectData plugin.
                        3. Regarding SceneHookData. What I was suggesting works the other way around, instead of objects registering as observers to an observable SceneHook to get informed about key press events, I was proposing that a SceneHookData collects objects of type T in a scene to conduct all drawing operations for them. The objects are not drawing themselves, but the scene hook is. But since you have tangible events since you want to move handles, you do not have to go that crazy route. The whole idea was here that when you are "draw-event-starved" in your ObjectData hook, you simply set up shop in another hook where you are not (SceneHookData) and do your drawing from there.
                        4. I get what you mean, when I look at your pseudo-code I can hear .NET shouting that it wants its paradigms and code styling back, so I can imagine the kind of code you are used to. This is of course another world and classic API Cinema 4D code is a different beast and Maxon's approach to C++ in general is special. I personally like simple old-school code where you do not have three million observables, delegates, MVP/MVC layers, and ofc, machine learning, to toggle a button from on to off. Our maxon API is such more modern API and uses meta-programming and other fancy things, there we have for example concepts like observables, managed memory, and graceful error handling. But that API currently focuses on backend logic and not stuff like the GUI core or the plugin hooks.

                        Find my take on the problem below.

                        Cheers,
                        Ferdinand

                        File: example.ohandlenull.zip - This is the full project, you just have to link it in a solution and you can compile it.

                        Result:

                        When the tangents turn orange, I am pressing CTRL to break them.

                        Code:

                        /*
                          example.ohandlenull
                          (C) MAXON Computer GmbH, 2023
                        
                          Author: Ferdinand Hoppe
                          Date: 20/06/2023
                        
                          Demonstrates customized handle handling in an object plugin in order to color code handles based
                          on user actions.
                        */
                        
                        #include "c4d_baselist.h"
                        #include "c4d_basedraw.h"
                        #include "c4d_basebitmap.h"
                        #include "c4d_baseobject.h"
                        #include "c4d_general.h"
                        #include "c4d_objectdata.h"
                        #include "lib_description.h"
                        
                        #include "c4d_symbols.h"
                        #include "ohandlenull.h"
                        #include "ohandlenulldata.h"
                        
                        // -------------------------------------------------------------------------------------------------
                        
                        Bool RegisterOhandleNull()
                        {
                          return RegisterObjectPlugin(PID_OHANDLENULL, "ohandlenull"_s, OBJECT_GENERATOR,
                                                      OhandleNullData::Alloc, "ohandlenull"_s,
                                                      InitResourceBitmap(Onull), 0);
                        }
                        
                        BaseObject* OhandleNullData::GetVirtualObjects(BaseObject* op, HierarchyHelp* hh)
                        {
                          return BaseObject::Alloc(Onull);
                        }
                        
                        // -------------------------------------------------------------------------------------------------
                        
                        Bool OhandleNullData::Init(GeListNode* node)
                        {
                          if (!node)
                            return false;
                          // Nothing too exciting here, we just init the three parameters we use.
                          Bool res = node->SetParameter(DescID(ID_OHANDLENULL_TANGENT_LEFT),
                                                        GeData(Vector(-100, 0, 0)), DESCFLAGS_SET::NONE);
                          res &= node->SetParameter(DescID(ID_OHANDLENULL_TANGENT_RIGHT),
                                                    GeData(Vector(100, 0, 0)), DESCFLAGS_SET::NONE);
                          res &= node->SetParameter(DescID(ID_OHANDLENULL_TANGENT_LINK),
                                                    GeData(true), DESCFLAGS_SET::NONE);
                          
                          return res;
                        }
                        
                        DRAWRESULT OhandleNullData::Draw(BaseObject *op, DRAWPASS drawpass, BaseDraw *bd, BaseDrawHelp *bh)
                        {
                          // We add drawing instructions for our handles to the handle pass when we are asked to do so.
                          static const Vector nullVector = Vector(0.);
                          
                          if (drawpass != DRAWPASS::HANDLES)
                            return DRAWRESULT::SKIP;
                          
                          // Get our parameters to draw.
                          BaseContainer& data = op->GetDataInstanceRef();
                          Vector tLeft = data.GetVector(ID_OHANDLENULL_TANGENT_LEFT);
                          Vector tRight = data.GetVector(ID_OHANDLENULL_TANGENT_RIGHT);
                          
                          // Select the color to draw with based on #_handleState.
                          Vector color = Vector(1., .75, .25);
                          if (_handleState & HANDLESTATE::CTRL)
                            color += Vector(.0, -.5, .0);
                          if (_handleState & HANDLESTATE::MOUSEOVER)
                            color += Vector(.0, .25, .25);
                          
                          // Prepare the drawing operations by setting a color and point size (for the handles) to draw
                          // with, as well as coordinate system to draw in. We set our system to be equal to the global
                          // matrix of our node, i.e., we draw in its local coordinates, all points will be relative to the
                          // origin and orientation of #op.
                          bd->SetPen(color);
                          bd->SetPointSize(10.0);
                          bd->SetMatrix_Matrix(op, op->GetMg(), 0);
                          
                          // Draw the handle lines ...
                          bd->DrawLine(nullVector, tLeft, NOCLIP_D);
                          bd->DrawLine(nullVector, tRight, NOCLIP_D);
                          
                          // and the dots left and right ...
                          bd->DrawHandle(tLeft, DRAWHANDLE::CUSTOM, NOCLIP_Z);
                          bd->DrawHandle(tRight, DRAWHANDLE::CUSTOM, NOCLIP_Z);
                          
                          // as well as a dot for the origin in light gray.
                          bd->SetPen(Vector(.7));
                          bd->DrawHandle(nullVector, DRAWHANDLE::CUSTOM, NOCLIP_Z);
                          
                          return DRAWRESULT::OK;
                        }
                        
                        // -------------------------------------------------------------------------------------------------
                        
                        void OhandleNullData::GetHandle(BaseObject *op, Int32 i, HandleInfo &info)
                        {
                          // When being asked for a handle, we simply return the respective parameter value. We index our
                          // handles with the parameter indices ID_OHANDLENULL_TANGENT_LEFT and ID_OHANDLENULL_TANGENT_RIGHT.
                          BaseContainer& data = op->GetDataInstanceRef();
                          const Bool isLeft = i == ID_OHANDLENULL_TANGENT_LEFT;
                          const Vector pos = data.GetVector(isLeft ? ID_OHANDLENULL_TANGENT_LEFT:
                                                                     ID_OHANDLENULL_TANGENT_RIGHT);
                          
                          info.position = pos;
                          info.direction.x = 1.0;
                          info.type = HANDLECONSTRAINTTYPE::LINEAR;
                        }
                        
                        Int32 OhandleNullData::DetectHandle (BaseObject *op, BaseDraw *bd, Int32 x, Int32 y,
                                                             QUALIFIER qualifier)
                        {
                          // Here happens the meat of the logic. Cinema 4D asks us what is what regarding if the user is
                          // hovering one of our handles. We use the occasion to reestablish our handle state. Conveniently
                          // we here get already a #QUALIFIER state being passed in.
                          _handleState = HANDLESTATE::NONE;
                          HandleInfo info;
                          Int32 res = NOTOK;
                          
                          // Check if we are hitting one of our handles.
                          for (auto i: {ID_OHANDLENULL_TANGENT_LEFT, ID_OHANDLENULL_TANGENT_RIGHT})
                          {
                            GetHandle(op, i, info);
                            if (bd->PointInRange(op->GetMg() * info.position, x, y))
                            {
                              // We do, enable the mouseover state.
                              _handleState = HANDLESTATE::MOUSEOVER;
                              res = i;
                            }
                          }
                          
                          // When CTRL is being pressed, set the CTRL state. We could also or things together here so that
                          // the state is then MOUSEVER & CTRL or NONE & CTRL.
                          if (qualifier & QUALIFIER::CTRL)
                            _handleState = HANDLESTATE::CTRL;
                        
                          return res;
                        }
                        
                        Bool OhandleNullData::MoveHandle (BaseObject *op, BaseObject *undo, const Vector &mouse_pos,
                                                          Int32 hit_id, QUALIFIER qualifier, BaseDraw *bd)
                        {
                          if (!bd || !bd->GetDocument())
                            return false;
                          
                          // Here we primarily carry out what we determined in our custom handle handling before.
                          HandleInfo info;
                          GetHandle(op, hit_id, info);
                          
                          // Get the document the editor window we are drawing into is attached to and the camera the user
                          // is using.
                          const BaseDocument* const doc = bd->GetDocument();
                          const BaseObject* const camera = bd->GetSceneCamera(doc) ? bd->GetSceneCamera(doc) :
                                                                                     bd->GetEditorCamera();
                          
                          // Project our mouse pointer into the plane defined by our object and the camera normal. We
                          // transform that result then by the inverse of our object matrix #mg so that the resulting point
                          // is in the local coordinate system of #op, i.e., the coordinate system our tangents operate in.
                          const Matrix mg = op->GetMg();
                          const Vector q = ~mg * bd->ProjectPointOnPlane(mg.off,                      // Point in plane
                                                                         -(camera->GetMg().sqmat.v3), // Normal of plane
                                                                         mouse_pos.x, mouse_pos.y);   // Point to project
                          
                          // Fiddle the information into place where we have to write #q.
                          BaseContainer& data = op->GetDataInstanceRef();
                          const Bool islinked = data.GetBool(ID_OHANDLENULL_TANGENT_LINK);
                          const Bool isLeft = hit_id == ID_OHANDLENULL_TANGENT_LEFT;
                          
                          Int32 mouseOverTangent = isLeft ? ID_OHANDLENULL_TANGENT_LEFT : ID_OHANDLENULL_TANGENT_RIGHT;
                          Int32 linkedTangent = !isLeft ? ID_OHANDLENULL_TANGENT_LEFT : ID_OHANDLENULL_TANGENT_RIGHT;
                          
                          // Write the data of the currently mouse overed and dragged tangent and optionally drag the other
                          // tangent along by writing inverse orientation information to it but keeping its length.
                          data.SetVector(mouseOverTangent, q);
                          if (islinked && !(_handleState & HANDLESTATE::CTRL))
                            data.SetVector(linkedTangent, -(q.GetNormalized()) * data.GetVector(linkedTangent).GetLength());
                          else
                            data.SetBool(ID_OHANDLENULL_TANGENT_LINK, false);
                          
                          return true;
                        }
                        

                        MAXON SDK Specialist
                        developers.maxon.net

                        CJtheTigerC 1 Reply Last reply Reply Quote 1
                        • CJtheTigerC
                          CJtheTiger @ferdinand
                          last edited by

                          Hi @ferdinand,

                          you're simply a wizard. I'll try this code out when I get home. From looking at your code DetectHandle is going to be the key to success for me. It already gets the qualifier which I think is what gets me there. Why didn't I find this function earlier?

                          Regarding SceneHookData

                          The thing I don't like about this approach is that this would only work for my specific plugin. For the SceneHook to draw the handles of my ObjectData it would need to know how they should be drawn. Other plugins might also want to know about Ctrl, but we can't expect the SceneHook to know how to draw them. Except (and this is my .NET brain speaking) the ObjectData injects its drawing routine into the SceneHook so we can break this dependency. Unless of course your suggested approach already covers that in some way I'm not aware of.

                          Offtopic: .NET

                          Yeah .NET is a completely different approach. That's why working with C4D is so much fun to me, it's a whole new world to stumble around in. What even is this for (auto i: {ID_OHANDLENULL_TANGENT_LEFT, ID_OHANDLENULL_TANGENT_RIGHT}) loop? I mean I can figure out what it does but I've never seen syntax like this. Finding all of this out is just a jolly good time. In my main job I develop scalable enterprise application interfaces based on .NET, Azure and REST but at some point it's getting stale. So doing this here in my own time is just awesome.

                          Thank you so much and have a nice day!

                          Best regards,
                          Daniel

                          1 Reply Last reply Reply Quote 0
                          • M
                            mdk7b2
                            last edited by

                            this might help ✌

                            def IsDownCTRL(self):
                            
                            	# the user hold down CTRL key
                            	BC = c4d.BaseContainer()
                            	if c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.BFM_INPUT_CHANNEL, BC):
                            		return BC[c4d.BFM_INPUT_QUALIFIER] == c4d.QUALIFIER_CTRL
                            
                            	return False
                            
                            def IsDownSHIFT(self):
                            
                            	# the user hold down SHIFT key
                            	BC = c4d.BaseContainer()
                            	if c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.BFM_INPUT_CHANNEL, BC):
                            		return BC[c4d.BFM_INPUT_QUALIFIER] == c4d.QUALIFIER_SHIFT
                            
                            	return False
                            
                            def IsDownALT(self):
                            
                            	# the user hold down ALT key
                            	BC = c4d.BaseContainer()
                            	if c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.BFM_INPUT_CHANNEL, BC):
                            		return BC[c4d.BFM_INPUT] == c4d.QUALIFIER_ALT
                            
                            	return False
                            
                            1 Reply Last reply Reply Quote 1
                            • M
                              mdk7b2
                              last edited by

                              def IsDownALT(self):

                              	# the user hold down ALT key
                              	BC = c4d.BaseContainer()
                              	if c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.BFM_INPUT_CHANNEL, BC):
                              		return BC[c4d.BFM_INPUT_QUALIFIER] == c4d.QUALIFIER_ALT
                              
                              	return False
                              
                              1 Reply Last reply Reply Quote 0
                              • CJtheTigerC
                                CJtheTiger
                                last edited by ferdinand

                                This post is deleted!
                                1 Reply Last reply Reply Quote 0
                                • CJtheTigerC
                                  CJtheTiger
                                  last edited by ferdinand

                                  I'd like to follow this post up with a closing response.

                                  @ferdinand once again delivered the key to success: The DetectHandle function.

                                  virtual Int32 DetectHandle(BaseObject* op, BaseDraw* bd, Int32 x, Int32 y, QUALIFIER qualifier);
                                  

                                  Since it gets passed the QUALIFIER I simply store it in a private class variable and access it in Draw and MoveHandle. As @ferdinand said this comes with the drawback that releasing a key while moving the handle will not update this variable in time for Draw and MoveHandle. But this is fine for me. Just choose which key to use before you click you silly user. >:)

                                  That being said it does feel a bit dirty. So I'll leave this open for just a few more days in case someone has a better idea. Otherwise I'll simply go this route.

                                  Thanks again @ferdinand! Your help is as invaluable as ever.

                                  ferdinandF 1 Reply Last reply Reply Quote 0
                                  • ferdinandF
                                    ferdinand @CJtheTiger
                                    last edited by ferdinand

                                    Hey @CJtheTiger ,

                                    it is great to hear that you found your solution and always appreciated when users share their outcome as it will make threads more "complete" and helpful for future readers.

                                    As a minor detail: As I see that you were struggling with the video player, the trick is to put down local-player as the display label of a URL. So, when you have uploaded an mp4, avi, webm or mov file and you have then the following text in the editor:

                                    [1688150981413-e5f8e80f-803a-4eb0-8226-37791f9f1d45-cinema_4d_7muveow2my.mp4](https://developers.maxon.net/forumhttps://developers.maxon.net/forumhttps://developers.maxon.net/forum/assets/uploads/files/1688150981413-e5f8e80f-803a-4eb0-8226-37791f9f1d45-cinema_4d_7muveow2my.mp4)
                                    

                                    You must then edit it so that the displayed text of that link becomes local-player:

                                    [local-player](https://developers.maxon.net/forumhttps://developers.maxon.net/forumhttps://developers.maxon.net/forum/assets/uploads/files/1688150981413-e5f8e80f-803a-4eb0-8226-37791f9f1d45-cinema_4d_7muveow2my.mp4)
                                    

                                    It will then render like this:
                                    local-player

                                    I will hopefully have time in the coming months to have a bit more user friendly solution and also lift the file size limit over eight megabytes. But that will require me touching the server. This is just a quick and dirty solution for everyone who is tired of creating animated GIFs in the meantime 🙂

                                    Cheers,
                                    Ferdinand

                                    MAXON SDK Specialist
                                    developers.maxon.net

                                    J 1 Reply Last reply Reply Quote 1
                                    • ferdinandF ferdinand referenced this topic on
                                    • J
                                      jana @ferdinand
                                      last edited by

                                      Hello @CJtheTiger ,

                                      without further questions or postings, we will consider this topic as solved by Friday, the 11th of august 2023 and flag it accordingly.

                                      Thank you for your understanding,
                                      Maxon SDK Group

                                      1 Reply Last reply Reply Quote 0
                                      • First post
                                        Last post