Component outline on different z-order than component

I'm drawing an outline around my component when the mouse hovers over it. Sometimes the component needs to be partially covered by another component, but I still want to show the full outline when the mouse hovers over it. The video below should make it clear what I'm trying to achieve. Does anyone have any advice?

That's not really possible with Swing's transparency model, unfortunately.

1 Like

Not the answer I was hoping for, but was fearing to get. I'll try to figure something out.

Position a copy of the grey-pink blocks on top of the existing one but make it completely transparent except for the hover outline. Remove the hover effect from the original one.

I'm certain that this can be achieved in some way. Explain your hierarchy; what type of components are involved? Is the outline a separate component? If so, in Vision, the z-order is set in the designer using these controls:
View ==> Toolbars ==> Shape Editing
image

I want to package it all into one component, the graphic itself as well as the outline. Having 2 components makes it too easy to not line them up properly, especially when there are multiple of these components on the HMI.

This is how it will eventually be used. The component is a slide gate, which is often used under conveyors to allow the conveyor to deliver grain to multiple different destinations. The pink color simply shows what the current route is.

However it can also be used in other locations, where the whole slide gate is seen. Like this.

image

I could give the slide gate a property to not show the top of it, something like this.
image

I could then attach it to the bottom of a conveyor and make it's z-order higher than that of the conveyor, and then the outline would show up correctly. It's just that this way the user really has to pay attention to z-order, as it's not obvious which component is in front until Preview Mode is turned on and the mouse is hovered over it.

Clever.

Do you mind sharing how you accomplished the outline on hover? We have a client coming from Wonderware that has that on all their plants and I have a feeling they're going to want that. I was curious if you had a simple way you were doing it.

That's actually what we're doing as well. Moving from Wonderware to Ignition, and we're trying to keep that outline effect.

Here is how I achieve this. I created the following method in a class called Utilities

/**
 * Expand or shrink an area in all directions by a defined offset.
 *
 * @param A         Area
 * @param offset    Offset to expand/shrink.
 * @param join      Method to use for line joins.
 * @return          New area that is expanded or shrunk by the specified amount.
 */
public static Area areaOutline(Area A, float offset, int join) {
    if (offset < 0.1 && offset > -0.1)
        return A;
    
    Area newArea = new Area(A);
    Stroke stroke = new BasicStroke(Math.abs(2f * offset),
                                    BasicStroke.CAP_SQUARE, 
                                    join);
    Area strokeShape = new Area(stroke.createStrokedShape(newA));

    if (offset > 0.0)
        newArea.add(strokeShape);
    else
        newArea.subtract(strokeShape);
    
    return newArea;
}

which is then implemented like this under paintComponent:

if (mouseEntered) {
    Area outline = Utilities.areaOutline(componentArea,3,BasicStroke.JOIN_ROUND);
    
    g.setStroke(new BasicStroke(2f));
    g.setColor(Color.BLACK);
    g.draw(outline);
    
    g.setStroke(new BasicStroke(1.5f));
    g.setColor(Color.WHITE);
    g.draw(outline);            
}
2 Likes

Now I'm confused. This looks to be all java, so did you have to build out a java JAR file that you included this in or is there a way to do it through the project library scripts?

I'm creating our own module with a bunch of components, and yes that's all written in Java.

Ok, that's beyond my level. I'm not a Java programmer at all and was hoping there was something easy to do in Vision to replicate it on all of our clickable templates.

I don't think there is a switch anywhere that turns on automatic outlines for clickable objects. You would have to draw an outline around your template yourself, then toggle its Visible property via mouse events.
image

Yeah, that was going to be my alternative if they wanted similar functionality.

The paintable canvas can be used as an overlay to provide this effect without any regard to z-order or component positioning. Simply overlay the entire container with a paintable canvas, and use the canvas's mouse moved event to detect when the cursor intersects a component, so a top level outline can be painted around it:

Example:
Add the following custom properties to the canvas:
image

Add the following custom methods to the canvas:

passMouseEvent
#def passMouseEvent(self, event):
	"""
	Passes mouse event to the underlying component
	"""
	from java.awt import Rectangle
	from java.awt.event import MouseEvent
	for component in event.source.parent.components:
		if component != event.source: 
			
			# Components that use relative coordinates must be handled differently
			if not hasattr(component, 'relX') and component.bounds.contains(event.point):
				componentEvent = MouseEvent(
					component,
					event.getID(),
					event.when,
					event.modifiers,
					event.x - component.x,
					event.y - component.y,
					event.getXOnScreen(),
					event.getYOnScreen(),
					event.clickCount,
					event.isPopupTrigger(),
					event.button)
				component.dispatchEvent(componentEvent)
				return
			elif hasattr(component, 'relX') and Rectangle(int(component.relX), int(component.relY), int(component.relWidth), int(component.relHeight)).contains(event.point):
				componentEvent = MouseEvent(
					component,
					event.getID(),
					event.when,
					event.modifiers,
					event.x - int(component.relX),
					event.y - int(component.relY),
					event.getXOnScreen(),
					event.getYOnScreen(),
					event.clickCount,
					event.isPopupTrigger(),
					event.button)
				component.dispatchEvent(componentEvent)
				return
mapOutline
def mapOutline(self, event):
	"""
	Sets the custom properties on the paintable canvas that define an outline
	...and in doing so, triggers the repaint event
	"""
	from java.awt import Rectangle
	for component in event.source.parent.components:
		# Filter out components that shouldn't be outlined
		if component.__class__.__name__ not in ['PMIPaintableCanvas', 'PMILabel'] or 'Polygon' in component.name:
			
			# Handle components that use relative coordinates differently than standard components
			if not hasattr(component, 'relX') and component.bounds.contains(event.point):
				event.source.outlineX = component.x - event.source.outlineGap
				event.source.outlineY = component.y - event.source.outlineGap
				event.source.outlineWidth = component.width + (2 * event.source.outlineGap)
				event.source.outlineHeight = component.height + (2 * event.source.outlineGap)
				return
				
			elif hasattr(component, 'relX') and Rectangle(int(component.relX), int(component.relY), int(component.relWidth), int(component.relHeight)).contains(event.point):
				event.source.outlineX = component.relX - event.source.outlineGap
				event.source.outlineY = component.relY - event.source.outlineGap
				event.source.outlineWidth = component.relWidth + (2 * event.source.outlineGap)
				event.source.outlineHeight = component.relHeight + (2 * event.source.outlineGap)
				return
	event.source.outlineX = None

image

Both custom methods will take a single parameter called event. The
passMouseEvent will be used to provide to the underlying components any mouse event that would otherwise be consumed by the overlay.

The mapOutline will be used to set the custom properties on the canvas that define the outline. The top level if statement in the mapOutline method should be used to determine which components receive the outline.

Call the passMouseEvent from all paintable canvas mouse event handlers in this way:

event.source.passMouseEvent(event)

image

call the mapOutline method from the mouseMoved event handler in this way:

# All mouse events will have passMouseEvent,
#...but only mouseMoved will have mapOutline
event.source.passMouseEvent(event)
event.source.mapOutline(event)

Finally, use the following script on the canvas's repaint event handler to paint the outlines:

if event.source.outlineX is not None:
	from java.awt import BasicStroke
	graphics = event.graphics
	graphics.setStroke(BasicStroke(5))
	graphics.setColor(system.gui.color('yellow'))
	x = event.source.outlineX
	y = event.source.outlineY
	width = event.source.outlineWidth
	height = event.source.outlineHeight
	radius = event.source.outlineGap
	graphics.drawRoundRect(x, y, width, height, radius, radius)

Result:
PaintableCanvasFloatingOutlineDemo

1 Like

It's possible that this would work if the separate outline component's existence and position were determined at run time by the focusable component it was outlining. Since the last component added will be painted on top, I imagine the focusable component itself could dynamically create an outline component and add it to the parent container using the mouse entered event, and since the focusable component would have its own coordinates to determine the outline placement, alignment would maintained automatically. Furthermore, adding and removing the outline component at run time eliminates the need to maintain a proper z-order for the secondary component.

Lastly, if for some reason the overlay component interferes with the focusable component's mouse events, see the passMouseEvent method I put together for the paintable canvas overlay example. It simply converts the overlay's mouse events to what the underlying component's mouse event would have been, and passes the converted mouse event to the underlying component's dispatchEvent method.

I'm going to play with this more I think as I don't want it to highligh every component, but only those that can be interacted with. Most of my vessels and lines which are static would probably trigger this outline also, so I may just have to do some binding on the border to get this to work as I'd like, but not sure if I'll have time to mess with it this week.

Perhaps simply checking focusability would be enough:

if component != event.source and component.focusable:

...or something like this:

if component != event.source and component.focusable and component.visible and component.enabled:

Experimenting with focusability, I was also able to make a more concise list of only the components that can receive focus from the tab key in this way:

from java.awt import KeyboardFocusManager
keyboardFocusPolicy = KeyboardFocusManager.getCurrentKeyboardFocusManager().defaultFocusTraversalPolicy
parentContainer = event.source.parent
startingComponent = keyboardFocusPolicy.getFirstComponent(parentContainer)
nextComponent = keyboardFocusPolicy.getComponentAfter(parentContainer, startingComponent)
lastComponent = keyboardFocusPolicy.getLastComponent(parentContainer)
tabFocusableComponents = [startingComponent]
for component in parentContainer.components:
	tabFocusableComponents.append(nextComponent)
	if nextComponent == lastComponent:
		break
	else:
		nextComponent = keyboardFocusPolicy.getComponentAfter(parentContainer, nextComponent)

# Possibly use this list for validating?
print tabFocusableComponents

There is probably some code golfing to be done there, and there is the hurdle of figuring out an efficient way to use it, but since it looks like it could be useful, I've decided to go ahead and post it.