I’m working on a small plugin where I’m trying to inject a custom button into the Layers Panel toolbar (right next to the “New Layer” / “Delete” icons).
I’m traversing the Eto control tree, finding the specific PixelLayout that holds those buttons, and just adding mine to the end.
The weird part:
It works perfectly the first time I load Rhino or show the panel. The layout reports valid bounds, and my button sits exactly where it should.
But… if I close the Layers panel and open it again (switching between tabs has no problem), things break.
Even though the panel is visible on screen, the parent PixelLayout suddenly reports a Size of {-1, -1} or 0,0.
Because the container thinks it has no size, my button gets added but ends up stuck at 0,0 (overlapping the other icons) or just totally invisible, even though it’s technically in the Children list.
I’m triggering this on Panels.Show and even waiting for RhinoApp.Idle, but it seems like on re-creation, the internal layout state is just… empty?
This is happening on both Mac and Windows.
Is there anything I’m missing here @curtisw? Is there a trick to force the layout to “wake up” and measure itself correctly after a re-open?
I believe Panels Unload and Load again when closed in Rhino8. So if you keep a handle on the PixelLayout, that PixelLayout was removed from the control and is trying to get GC’d. You’ll need to respond to Unload/Load and re-init everything you’re doing.
I will state of course that injecting controls into Rhino ones isn’t something we can promise will work the same way in every version of Rhino, the layout may change in 8.31 for example and you’d need to re-do things.
Thank you for the clarification about the Unload/Load lifecycle. I’ve implemented the changes you suggested and have some interesting findings to share.
What I changed:
Subscribed to Panels.Show event to detect when the Layers Panel is reopened
Reset my injection state and re-inject on the next RhinoApp.Idle
Side note: I initially tried using WeakReference<Control> to detect if the button was destroyed, but it didn’t work - the old button still reported IsDisposed: False even after the panel was recreated (GC hadn’t collected it yet). So explicitly calling Reset() on Panels.Show is the reliable approach.
Current issue - PixelLayout reports Size: {-1,-1}
After fixing the re-injection logic, I’m hitting the core problem. Here are my diagnostic logs when reopening the Layers Panel:
Reer Agent: Panel shown event - PanelId: 3610bf83-047d-4f7f-93fd-163ea305b493
Reer Agent: Layers Panel detected, resetting and scheduling re-injection...
Reer Agent: Idle triggered, attempting re-injection...
Reer Agent: No previous button reference, proceeding with injection...
Reer Agent: panelObj is Control: True
Reer Agent: Traversing control tree...
Reer Agent: Found PixelLayout at depth 3, Size: -1,-1
Reer Agent: PixelLayout has 12 children
Reer Agent: Found 12 toolbar buttons
So:
Panels.Show event fires correctly
Panels.GetPanel(PanelIds.Layers) returns a valid Control
I find the PixelLayout at depth 3 with 12 children (the 11 native buttons plus my custom one)
But PixelLayout.Size reports {-1, -1}
This causes my positioning calculation to fail. Although the button exists and the position is correct, it doesn’t appear.
Questions:
Is waiting for one Idle cycle not enough? Should I wait for multiple cycles or use a timer?
Is there a specific event that indicates the PixelLayout has valid dimensions?
What’s the reason for the RhinoApp.Idle? Rhino’s Idle is a wait loop which isn’t when you want to run UI logic. Why not perform things in the event? Once Shown the layers panel is ready to go.
You’re absolutely right that Idle shouldn’t be necessary — and I’ve confirmed that it doesn’t actually help either.
I originally added the Idle delay because when I first tried injecting directly in the Show event, the button wasn’t appearing. My assumption was that maybe the panel’s content gets rendered after the panel container becomes visible, so I thought waiting for Idle would give the layout system time to measure everything.
So you’re correct — the Idle pattern was unnecessary complexity. The real issue isn’t timing, it’s that the PixelLayout.Size property simply doesn’t update correctly after the Unload/Load cycle you mentioned.
The layers panel uses Rhino.UI.Controls.ControlGridLayout as the host for those buttons. The ControlGridLayout uses a PixelLayout that it draws the controls onto. The -1,-1 is the control size before its completed its layout. This is very normal.
Thinking out loud here… Assuming you can locate the control grid layout you might be able to just add your image button to its item collection and all of the layout routines will happen automatically from there, then when resizing the panel narrow and forcing multi-row it will still place your button in the correct location.
@Trav , your approach was the first thing I tried… I successfully located the ControlGridLayout in the Layers Panel and can add my custom button to its Items collection. The item is added correctly (the count goes from 11 to 12), and it renders perfectly, working when resizing as you mentioned.
The problem here arises when I close the Layer panel and reopen it again. Even disposing of the original button and repeating the logic once the panel is visible, all the logs I placed assert that my button is here, but, for some reason, it does not appear.
The problem might be related to the class I’m using. As the Rhino.UI.Controls.ImageToolTipButton is not public, I’m adding a common Eto.Forms.Button. I’m not sure if the problem is here.
Is it possible to create an ImageToolTipButton from plugin code? Is there a public API or factory method I should use? Or is there a different approach to adding custom buttons to the Layers Panel toolbar that will render correctly?
The problem is the panel isn’t being reused but sent off to be garbage collected or placed on a heap to clean up, when its turned off. So your old buttons hanging around in limbo and a new panel is created which your button is no longer a part of. Some of this behavior actually looks a bit like a bug in terms of longevity of that old panel. You can see this from getting all panel instances of the layers panel. You’ll see them stack up.
Also, “technically” this isn’t something we would encourage at all without some sort of public API for it which we aren’t working on. So you’re basically “hacking” the UI to bend it to your will.
But the overall issue here is there’s more than 1 layer panel happening. I’m fishing around in the panel manager code to see if we can’t get those old panels disposed a bit faster.. it feels like a bug.
Edit, I cleaned up the lingering panels yesterday. They should get destroyed when the tab is closed now. RH-92375 but new panels will still get created often so you’d still have to watch for their creation.
Oh and ImageTooltipButton is derived from ImageButton. It looks like the only difference is a popup up tool tip that shows left / right click behaviors. We can likely get it exposed but you probably aren’t missing much without it.