A customer wants the cursor to change from an arrow to a hand for all clickable objects. This is easy to do in Ignition. However, if a template is placed over this object, the click action will be executed, but the cursor will no longer change.
Is it possible to provide this template with some kind of transparency attribute for mouse actions so that the cursor keeps changing? Or maybe in another way?
Just for clarity: we're talking about Vision?
Yes, Vision
I figured that it wouldn't too difficult to script this, but I've encountered an interesting result. the mouse event handlers on the actual template don't fire. It's as if the listeners aren't there. The mouse event handlers with the template work fine though. Is this part of the problem?
My initial understanding of your question was that a nonclickable component, that just happened to be a template, was overlapping another component in some way, and you were wanting the mouse cursor to change to hand indicating that a clickable component was underneath. Is this correct, or have I misunderstood the question?
Messing around with this, I developed this script that can detect overlap and respond with a mouse cursor change:
Prototype Script
from java.awt import Cursor, MouseInfo
from javax.swing import SwingUtilities
from com.inductiveautomation.factorypmi.application.components import PMIButton
from com.inductiveautomation.factorypmi.application.components import BasicContainer
def getCoordinates(component):
x1 = component.x
y1 = component.y
x2 = component.x + component.width
y2 = component.y + component.height
return [x1, y1, x2, y2]
def isOverlapped(selfCoordinates, componentCoordinates):
left = selfCoordinates[2] < componentCoordinates[0]
right = selfCoordinates[0] > componentCoordinates[2]
above = selfCoordinates[1] > componentCoordinates[3]
below = selfCoordinates[3] < componentCoordinates[1]
return not (left or right or above or below)
def getOverlapCoordinates(selfCoordinates, componentCoordinates):
x1 = max(selfCoordinates[0], componentCoordinates[0])
y1 = max(selfCoordinates[1], componentCoordinates[1])
x2 = min(selfCoordinates[2], componentCoordinates[2])
y2 = min(selfCoordinates[3], componentCoordinates[3])
return [x1, y1, x2, y2]
selfCoordinates = getCoordinates(event.source)
container = SwingUtilities.getAncestorOfClass(BasicContainer, event.source)
for component in container.getComponents():
if component != event.source and isinstance(component, PMIButton):
componentCoordinates = getCoordinates(component)
if isOverlapped(selfCoordinates, componentCoordinates):
overLapCoordinates = getOverlapCoordinates(selfCoordinates, componentCoordinates)
mousePosition = SwingUtilities. convertPoint(event.source, event.x, event.y, container)
if mousePosition.x >= overLapCoordinates[0] and mousePosition.x <= overLapCoordinates[2] and mousePosition.y >= overLapCoordinates[1] and mousePosition.y <= overLapCoordinates[3]:
event.source.setCursorCode(Cursor.HAND_CURSOR)
else:
event.source.setCursorCode(Cursor.DEFAULT_CURSOR)
However, it only works with standard components, and I've had no luck getting this to work from a template. I've tried the mouseMoved event handler; I've added my own custom listener, and I've attempted to get the components by calling them from the root container instead of using ancestorOfClass.
What kinda worked was putting a transparent label over the template instance and firing the script from the mouseMoved event handler of label, but the label blocked access to some of the template functions, so additional scripting would have been needed.
Perhaps somebody else will have a better idea.
Thank you very much for your research
Unlike web technologies, where events "bubble up" to containing elements, events in java Swing are consumed on the innermost component that has a listener defined. If a template definition has an event defined on its root, a template instance will never get that event.
Also note that events are batched into a shared listener, as shown in the scripting UI for a component. Any event defined in a batch will cause that batch's listener to exist, blocking all of those events in parent components.
Consider defining custom methods on your template instance that your template definition attempts to delegate to, after its own handling. Or perhaps, construct a new mouse event (with recomputed outer coordinates) to fire on the parent.
The following script works inside a template:
from java.awt import Cursor, MouseInfo
from java.awt.event import MouseEvent
from javax.swing import SwingUtilities
from com.inductiveautomation.factorypmi.application.components import PMIButton, BasicContainer
from com.inductiveautomation.factorypmi.application.components.template import TemplateHolder
def getCoordinates(component):
x1 = component.x
y1 = component.y
x2 = component.x + component.width
y2 = component.y + component.height
return [x1, y1, x2, y2]
def isOverlapped(selfCoordinates, componentCoordinates):
outOfBoundsLeft = (selfCoordinates[2] <= componentCoordinates[0])
outOfBoundsRight = (selfCoordinates[0] >= componentCoordinates[2])
outOfBoundsTop = (selfCoordinates[3] <= componentCoordinates[1])
outOfBoundsBottom = (selfCoordinates[1] >= componentCoordinates[3])
if outOfBoundsLeft or outOfBoundsRight or outOfBoundsTop or outOfBoundsBottom:
return False
else:
return True
selfCoordinates = getCoordinates(event.source)
templateHolder = SwingUtilities.getAncestorOfClass(TemplateHolder, event.source)
container = SwingUtilities.getAncestorOfClass(BasicContainer, templateHolder)
for component in container.getComponents():
if component != event.source and isinstance(component, PMIButton):
componentCoordinates = getCoordinates(component)
if isOverlapped(selfCoordinates, componentCoordinates):
mousePosition = SwingUtilities. convertPoint(event.source, event.x, event.y, container)
if mousePosition.x >= componentCoordinates[0] and mousePosition.x <= componentCoordinates[2] and mousePosition.y >= componentCoordinates[1] and mousePosition.y <= componentCoordinates[3]:
event.source.setCursorCode(Cursor.HAND_CURSOR)
component.getModel().setRollover(True)
if event.ID == MouseEvent.MOUSE_CLICKED:
component.doClick()
else:
event.source.setCursorCode(Cursor.DEFAULT_CURSOR)
component.getModel().setRollover(False)
It's the same as the above script except that it gets the template holder first, and then gets the parent container relative to the holder. I also eliminated the getOverlapCoordinates function, since it really wasn't necessary. Due to the fact that events are specific to the component they are firing from, the component coordinates are effectively the same thing.
I put this script in a custom method that I called evaluateCursorAction
:
This works as expected, but it has to be called independently from the mouseClicked and mouseMoved event handlers on each component within the template that the cursor function is supposed to pass through.
It seemed like it would be a good idea to not use the individual components event handlers, and instead add a custom listener to individual component classes at runtime. In this way, if components within the template get added or replaced, there will be less risk of regression and less technical overhead.
Perhaps such a script would look like this:
Automated Listener Application Script
from java.awt.event import MouseListener, MouseMotionListener
from com.inductiveautomation.factorypmi.application.components.template import VisionTemplate
def getTemplateInstance(component):
if isinstance(component.parent, VisionTemplate):
return component.parent
else:
templateInstance = getTemplateInstance(component.parent)
if templateInstance is not None:
return templateInstance
class mouseClickListener(MouseListener):
def mouseClicked(self, event):
getTemplateInstance(event.source).evaluateCursorAction(event)
def mouseEntered(self, event):
pass
def mouseExited(self, event):
pass
def mousePressed(self, event):
pass
def mouseReleased(self, event):
pass
class mouseMovementListener(MouseMotionListener):
def mouseMoved(self, event):
getTemplateInstance(event.source).evaluateCursorAction(event)
def mouseDragged(self, event):
pass
def setListeners(self):
for component in self.getComponents():
if component is not None:
component.addMouseListener(mouseClickListener())
component.addMouseMotionListener(mouseMovementListener())
component.setFocusable(True)
setListeners(component)
if event.propertyName == 'componentRunning':
setListeners(event.source)
I haven't messed with it enough to get it working, but nevertheless, there's the idea if anybody wants to follow it up.