How to transform a component inside of a vision template

I have a button in a template, and I want to resize it to a perfect square at runtime regardless of how the template has been sized.

To develop this, I have a Multi-State button inside a test template, and a test button that is in the same container as the template:

image

My desired result is this: button @ (x = 100, y = 100, width = 100, height = 100)
image

The obvious approach is this:

# Retrieve the button from the template
button = event.source.parent.getComponent('testTemplate').getComponent(0).getComponent(0)

# Transform the button in the normal and natural way using system.gui.transform
system.gui.transform(button, newX = 100, newY = 100, newWidth = 100, newHeight = 100)

...but this produces a radically incorrect result where the y and height dimensions seem to be exponentially larger than they should be, and the height dimension walks about 4 pixels every time the transform is applied.

It should also be noted that switching this to a relative coordinate space has no perceivable effect on the transform

The x dimension is the ONLY exception in that it always transforms in the expected way.

# Behaves as expected, but is obviously short three dimensions
button = system.gui.transform(button, newX = 100)

Trying to set the dimensions directly on the component actually works for the height and the width, but a second later, the component snaps back to its original size:

from java.awt import Dimension
button = event.source.parent.getComponent('testTemplate').getComponent(0).getComponent(0)
button.setMinimumSize(Dimension(100, 100))
button.setMaximumSize(Dimension(100, 100))
button.setPreferredSize(Dimension(100, 100))
button.setSize(Dimension(100, 100))

Believing this is a layout issue, I looked the layout up in the documentation, and it says this about the constraints:

Vision Layout constraints object, used with FPMILayout to hold the "layout constraints" (LC) of a component. This is a bit of a mess because of backwards compatibility, here's the explanation
[...]
Conveniently, the functions FPMILayout.getPreferredBounds(javax.swing.JComponent) and FPMILayout.getBounds(javax.swing.JComponent) make neat work of this confusion. these methods (and their setters) should be the only way that bounds get accessed.

Trying to directly adjust the size with FPMILayout actually works for BOTH size and location, but a second later, the component snaps back to its original position as if nothing had been changed:

from com.inductiveautomation.factorypmi.application.components.util import FPMILayout
from java.awt.geom import Rectangle2D
button = event.source.parent.getComponent('testTemplate').getComponent(0).getComponent(0)
layout = FPMILayout.getOffsettingParent(button).layout
layout.setBounds(button, Rectangle2D.Double(100, 100, 100, 100))

Setting the preferredBounds has a permanent effect, but just like the transform, the result is way off. However, I discovered that if I created a ratio between the widths and heights of the preferredBounds to the actual bounds, and then used that to transform the component, I got the size and position I wanted:

from com.inductiveautomation.factorypmi.application.components.util import FPMILayout

#Retrieve the button
button = event.source.parent.getComponent('testTemplate').getComponent(0).getComponent(0)

# Get the FPMILayout
layout = FPMILayout.getOffsettingParent(button).layout

# Make sure there is no possiblility of dividing by zero
widthDivisor, heightDivisor = max(1, layout.getBounds(button).width), max(1, layout.getBounds(button).height)

# Define ratios to get sizes that are relative to the original template
widthRatio = float(layout.getPreferredBounds(button).width) / widthDivisor
heightRatio = float(layout.getPreferredBounds(button).height) / heightDivisor

# Define the desired dimensions
desiredX = 100
desiredY = 100
desiredWidth = 100
desiredHeight = 100

# Calculate the dimensions that will be needed to get the transform to work using ratios
newX = desiredX
newY = desiredY * heightRatio
newWidth = desiredWidth * widthRatio
newHeight = desiredHeight * heightRatio

# Execute the transform
system.gui.transform(button, newX, newY, newWidth, newHeight)

However, I assume the button text is still rendering at its original coordinates because the word 'Off' is no longer visible:
Edit: It's possible that I messed something up when I was probing the component. After restarting the designer, the label renders as expected.

image

At this point, I'm assuming that I've somehow wandered off course or lost track of some key concept, so before I travel any further into left field, I have to ask: does anybody know a better way to go about this?

Are you sure you enabled layout inside the template? (Separate setting.)

Aside from that, it seems to me you are reinventing an anchored layout. :man_shrugging:

I tried various relative and anchored layout combinations, but they didn't change the behavior. (Right Click --> Layout)

I also tried nesting the components inside a container within the template, but that didn't change the behavior either. What I'm doing with the ratio gets the result I want, but I was hoping for a more straightforward way to do a transform on a component that is nested within a template.

Did you try the coordinate space argument to the transform function?

Yes:

I wonder if a group would behave the same way as a template. I'll have to do some testing when I get back to my computer.

1 Like

Nope. While groups definitely exhibit the same scaling behaviors as templates when you squish them or expand them, a simple transform still works as expected on a group's individual components because the components' bounds are still relative to their location within the window editor in the designer.

That said, I've finally got my head around what I was observing. The bounds of the template are always relative to the template editor in the designer. Therefore, my erroneous expectation was for the internal components to shift relative to the window they were placed in.

Case Study 1:
Assume I design my test template at dimensions of 400 x 300:
image

When I embed it into a window, I ensure that it remains neither compressed nor stretched. Upon executing my transform, it functions as expected:

button = event.source.parent.getComponent('TestTemplate').getComponent(0).getComponent(0)
system.gui.transform(button, 100, 100, 100, 100)

image

However, if I squish the template in the window down to 200 x 100, and run the same script, the result positions the internal component to approximately where `Rectangle(100, 100, 100, 100) would have been in the template. I say approximately because I believe there are some floating point calculation errors that take place which would explain the up to 4 pixel 'wobble' I had observed in my earlier testing.
image

That said, the effect is obvious when you're squishing the template, but in my case, I was expanding the template.

Case Study 2:
Assume I designed the template at a generic size of 200 x 50:
image

..but in a window, for whatever reason, it is desirable to make the template significantly larger:
image

Attempting to transform this button to a y-coordinate position of 100, my original expectation was for it to shift relative to the window editor. However, since the shift happens relative to the template in the editor, and since the template has been magnified from 50 pixels to 200 pixels, every 50 pixels in the newY parameter of the transform will cause the object to shift 200 pixels. Consequently, relative to the window editor, the object will position itself at y = 400 pixels [outside of the visible area]. Obviously, this principle applies to all other dimensions as well, including the x dimension, which I must not have observed in my initial testing because the width dimension of my template simply didn't change much when I resized the template within my window.

Therefore, using a relative coordinate space for the transform has no perceivable effect because, as I've already stated multiple times, the parameter is relative to the window editor and not the template editor. Thus, in order to get the desired effect of a template that has been resized in a designer window, a ratio that represents how much the template has been scaled is needed.

Which brings me back to what I had developed earlier to make my template work before I understood the reason why it was needed:

# Import FPMILayout
from com.inductiveautomation.factorypmi.application.components.util import FPMILayout

#Retrieve the component
component = # Put relative path to component here

# Get the FPMILayout
# In most cases, this could probably be simplified to temlateInsance.layout 
# ...eliminating the need for the FPMILayout import
layout = FPMILayout.getOffsettingParent(component).layout

# Make sure there is no possiblility of dividing by zero
widthDivisor, heightDivisor = max(1, layout.getBounds(component).width), max(1, layout.getBounds(component).height)

# Define the scale ratios by dividing the template bounds (.preferredBounds) by the window bounds(.bounds)
widthRatio = float(layout.getPreferredBounds(component).width) / widthDivisor # for scaling x and width
heightRatio = float(layout.getPreferredBounds(component).height) / heightDivisor # for scaling y and height

Edit: added the word editor to further clarify the difference between the designer window environment and the designer template environment, and added a missing import from the preceding code example

3 Likes