Get pressed keys in ObjectData plugin
-
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 theDraw
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).
-
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 onGetInputState
, 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, butGeIsMainThreadAndNoDrawThread
will be not.´Please refer to Support Procedures: Confidential Data in case you cannot share your code publicly.
Cheers,
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 amaxon::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.
-
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
returnsfalse
and your input value polling therefore never happens, orisPressed
remainsfalse
although it shouldn't?BFM_INPUT_VALUE
is also of typeInt32
, and notbool
, so you are calling the wrongBaseContainer
method here. The documentation is a bit dodgy on the type ofBFM_INPUT_VALUE
because it indicates bothInt32
andbool
, but I would try my luck withBaseContainer::GetInt32
instead because that is the type that is literally mentioned.Cheers,
FerdinandResult:
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)
-
@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:
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
todefaultw
in the last line) which makes me think that this might have something to do with me being in theDraw
of anObjectData
plugin? I wasn't able to get the dialog working in C++ (Open
returnsfalse
; haven't done a custom dialog in C++ before though so I'd have to read into that first).Thanks for your relentless support!
-
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 withmaxon::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
andGeIsMainThreadAndNoDrawThread
in your::Draw
. I would have expected the first one to betrue
, and the second one to befalse
. But apparentlyGeIsMainThread
is nottrue
. 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:
- Define a field
Bool _ctrl_pressed = false;
on yourObjectData
hook. - In its
NodeData::Message
, without checking for any message ID.- Check if
node
is selected, i.e., callBaseList2D::GetBit(BIT_ACTIVE)
on it.- If not, set
_ctrl_pressed
tofalse
and bail.
- If not, set
- Otherwise:
- Check for being on the main thread.
- Poll the ctrl key set
_ctrl_pressed
to the result.
- Check if
- 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,
FerdinandPS: 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.
- Define a field
-
Hello @CJtheTiger,
Please excuse the long waiting time. I had a look at your problem.
- I was misinformed about the nature of
GeIsMainThreadAndNoDrawThread()
andObjectData::Draw
, it is normal that::Draw
is executed off main thread from time to time. - What you are trying to do here is not intended by the developers.
- 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.
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 whennode
is dirty. I tried some tricks with flagging myself as dirty whenCTRL
is pressed, but Cinema 4D does not let itself to be tricked that easily and omitsC4DAtom::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 changeID_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 typeT
in a fixed interval of the currentCTRL
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.
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 logickey 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 - I was misinformed about the nature of
-
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: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:
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.
When hovering over a handle I want to highlight it along with the neighboring handle if there is one:
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.
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 theSceneHookData
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
-
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 -
Hey @CJtheTiger ,
Please excuse the long waiting time.
- 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. - 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.
- 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 aSceneHookData
collects objects of typeT
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 yourObjectData
hook, you simply set up shop in another hook where you are not (SceneHookData
) and do your drawing from there. - 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,
FerdinandFile: 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.
- As indicated earlier by me, doing what you want to do, is somewhat possible when overwriting the custom handle handling methods of
-
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 -
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
-
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
-
This post is deleted! -
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 inDraw
andMoveHandle
. As @ferdinand said this comes with the drawback that releasing a key while moving the handle will not update this variable in time forDraw
andMoveHandle
. But this is fine for me. Just choose which key to use before you click you silly user. >:) -
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-playerI 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 -
-
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