This is a tutorial I've wanted to do for some time because the glass pane is by far the most versatile Ignition tool I have ever developed.
• I've used them in informationally dense tables to highlight both the column and row underneath the mouse and to provide outlines or special indication for the selected column and row.
• I use them in set up screens to produce right click popups that illustrate conditional configurations.
• I use them to produce spotlights for component types.
• I've used them to augment charts with information that wouldn't be displayable in a conventional plot.
• I've used them to illustrate flow through pipes in real time.
• I have greatly improved the drag and drop animations between tree components, tables, and charts.
The additional functionality that a glass pane can add is only limited by the imagination. Moreover, to do these things with without a glass pane would require a host of additional shapes, components, scripting, and bindings that cumulatively could have a major performance impact to the project, but a glass pane is one component that is fully customizable, and only paints when needed.
Many times, a pure glass pane isn't needed. Meaning, not everything in this tutorial will be required for every situation. Ideally, the pane should be kept as lean as possible, and in my own practice, I often set the visibility of the pane to false when it's not in active use to make the implementation as efficient as possible, but efficiency and custom implementations are fodder for future posts. This first post will only cover creating the pure glass pane, and not any of the applications for it. My hope is that as other developers find uses for this, they will reply to this post and share what they develop to help others.
Paintable Canvas Glass Pane Tutorial
Step 1: Prep the window.
To do this right, the container that displays the content of the window should be a sibling of the glass pane.
To accomplish this, paste a regular container and a paintable canvas into the root container of the window. and then
• Rename the container "Display Container"
• Rename the paintable canvas "Glass Pane"
If done correctly, the tree structure will look like this:
...and the window should look something like this:
Resize the display container and the glass pane, so they both fill the entire window, and set the z-order of the glass pane, so it is on top of the display container. I also like to set the background of the display pane to transparent, so the background color of the window is still set by the root container.
Result:
Step 2: Prep the Root Container
Passing most mouse events to underlying components is relatively simple, but many components such as buttons perform actions on mouse entered and exited events. This will require tracking which component is under the mouse, and manually creating the events whenever the component changes. I do this by storing the component path as a string on a custom property. However, it's important to note that any custom property on a paintable canvas will trigger a repaint event any time the property changes. Therefore, to prevent unnecessary repaints, I recommend putting this custom property on the display container instead of the glass pane.
a custom property on the display container component
Step 3: Prep the glass pane
The glass pane will need to continuously reference display pane to pass and receive information to and from underlying components as well as painting over or around components when performing its actual purpose. Needless to say, there's no telling how many of these additional references will end up in the various event handlers, custom methods, and library script calls. This all translates to a big headache if some future developer or project manager decides to arbitrarily change the naming convention. For this reason, I like to go ahead and add a custom property to the glass pane that I bind directly to the display containers name. Then, this property alone can be used to call the display container, and any name changes will not incur an additional maintenance cost as a result of the scripting.
a custom property on the glass pane
bind this to the display container's name property
Since the glass pane is at the highest level z-order, it naturally gets in the way when trying to make normal edits to the window in the designer. Therefore, it's useful to put an expression binding on the glass pane's visibility property to automatically hide it when the designer is not in preview mode:
expression binding on the glass pane's "Visible" property
// # Automatically hides the component until preview mode is activated
// # ...or the client is launched
{[System]Client/System/SystemFlags} > 2
Since preview mode is automatically cancelled on save, the glass pane's expression binding ensures visibility property will always be false when the window is saved. Therefore, we can treat the visibility propertyChange
event like a componentRunning
initialization event. The main advantage to doing it this way, is that initialization can be triggered in the designer every time preview mode is activated.
Since all components are in a sibling container and since the glass pane's visibility property is false outside of run time, it shouldn't be possible at this point for anything to be pasted or placed above the glass pane, but you never know when somebody will inadvertently bring the display container itself to the front when working on the project, so as a safeguard against that type of breakage, I've developed an utility script for bringing a given component to the top level z-order at run time. This is a useful function for more than just glass panes, so I feel it should be put in the script library, so when needed, it can be called from any component in the project. For this tutorial, the library script will be named componentScripts
.
componentScripts.setTopPosition(component) in the project library
def setTopPosition(component):
'''
Arguments:
component: The component to position at the top level z-order
Returns:
None
Overview:
Since z-order in java is really paint order, and its set from top to bottom in order from last component added to first,
...this function simply removes the component and adds it back to give it top level positioning
'''
# Assign the parent to a variables,
# ...since the component can't call its parent after it has been removed from it
parent = component.parent
# Remove the component from the parent container and add it back,
# ...so it receives top level z-order and won't inadvertently be underneath anything
parent.remove(component)
parent.addComponent(component)
# Repaint the component in its new position
parent.repaint()
With all that prep work done, we can now write the initialization script for the glass pane's propertyChange event handler:
# On visible == True, initialize the glass pane
if event.propertyName == 'visible' and event.newValue:
# Force the glass pane to be at the highest layer z order in the root container
componentScripts.setTopPosition(event.source)
# Clear the current component path that is used for triggering mouse entered and exited events
event.source.parent.getComponent(event.source.displayContainerName).currentComponentPath = ''
Step 4: Handle mouse events.
With the exeptions of the mouseExited
and mouseMoved
events, all mouse events are handled by simply passing the mouse event to the deepest component with a mouse listener. The mouseExited
event handler can't use this approach because there are no underlying components under the mouse when the mouse leaves the canvas, and the mouseMoved
event handler has to keep track of the components currently under the mouse, so it can it can trigger individual mouseEntered
and mouseExited
events in the underlying components. It also has the task of floating tool tips to the surface.
To simplify this, and to make sure any future updates that are needed for the glass panes are not grotesquely time consuming, I package all this up into a library script called setGlasspaneMouseEvent
and call it from every mouse event in the canvas.
Example: calling the setGlasspaneMouseEvent function in a generic library script called componentScripts
place this on all of the glass pane's mouse event handlers
componentScripts.setGlasspaneMouseEvent(event)
Within the library script, I use several utility functions I've developed to process the underlying components. The first one uses swing utilities to get the deepest component at a given point, and then loops through the ancestor components until it finds one with a mouse listener.
script for finding the component under the mouse
def getMouseComponentAtPoint(component, container, point):
'''
Arguments:
component: the component that the given point is relative to
container: The container to search in for the deepest component
point: The point relative to the given component that will be converted to the relative point in the container
Returns:
The innermost component with a mouse listener
'''
# Get the point in the content pane relative to the glass pane
contentPoint = SwingUtilities.convertPoint(component, point, container)
# Find the closest mousable ancestor
# ...if the current component lacks mouse listeners
targetComponent = SwingUtilities.getDeepestComponentAt(container, contentPoint.x, contentPoint.y)
while targetComponent:
if targetComponent.mouseListeners:
return targetComponent
targetComponent = targetComponent.parent
Once the underlying component is found, it will need to be stored for reference and comparison during future events. I store the component as a simple comma delimited set of component indexes from the root container to the component itself. Component names could be used for this up to a point, but the names of nested Ignition components will be null
, and the names of templates that are rendered by a template repeater or canvas would be indistinguishable from each other. Consequently, I've elected to stick with indexes to avoid any parsing convolution.
script for converting a deeply nested component path into a comma delimited list of indexes
def getComponentPath(rootContainer, component):
"""
Arguments:
rootContainer: A reference to the root container of the window,
...which will be the ending point of the path building
component: The actual component that needs an encoded string path
Returns:
A string that represents the path from the root container of the window to a given component
Overview:
Creates a comma delimited string of all the hierarchical indexes from the root container to a given component.
"""
# Validate a component as not being the root container a direct child of the root container or the parent window.
# If exempt from the above conditions,
# ...define the parent and path variables that will be needed for looping through all of the parents,
# ...and obtaining their relative hierarchical indexes
if component != rootContainer and component.parent != rootContainer and not hasattr(component, 'rootContainer'):
parent = component
path = unicode(component.parent.components.index(component))
# If the component is the direct child of the root container, directly return its relative string index
elif component.parent == rootContainer:
return unicode(component.parent.components.index(parent))
# If the component is the root container or the parent window,
# ...return a blank string. [the getComponentFromPath function that decodes the path string
# ...always starts with the root container, and returns it by default if no subsequent indexes are in the string]
else:
return ''
# Loop through all parent components inserting their names into the path string until the root container is reached
while parent.parent != rootContainer:
parent = parent.parent
path = '{},{}'.format(parent.parent.components.index(parent), path)
return path
When needed, the component can be obtained using the encoded path with this function:
function for obtaining a component using a comma delimited list of indexes
def getComponentFromPath(rootContainer, path):
"""
Arguments:
path: A comma delimited string that represents the nested indexes needed to obtain the desired component
Returns:
The component represented by a given path
Overview:
Iterates through the elements of the given path string, and returns the nested component object the string represents
"""
currentComponent = rootContainer
# If the path is blank or null, simply return the root container
if not path:
return currentComponent
# The path string is comma delimited, so the elements can be broken up into a list on the comma that can be iterated through
for element in path.split(','):
index = int(element)
# As a safeguard against hierarchical change, validate the given index before attempting to retrieve the component it represents.
if 0 <= index < currentComponent.componentCount:
currentComponent = currentComponent.getComponent(index)
return currentComponent
As stated above, there are three different scenarios for passing mouse events. In all three cases, the basic task of passing mouse events is the same, and there are many other uses for passing mouse events than just glass panes, so for the generic task of dispatching a mouse event from one component to another, I developed this utility function:
utility function for converting and passing a mouse event from one component to another.
def passMouseEvent(listenerComponent, event):
if listenerComponent and hasattr(listenerComponent, 'dispatchEvent'):
listenerComponent.dispatchEvent(SwingUtilities.convertMouseEvent(event.source, event, listenerComponent))
With all that in place, the setGlasspaneMouseEvent
function is fairly straight forward. All mouse events other than mouseMoved
are simply converting and passing the paintable canvas's mouse event to an underlying component. However, the mouseMoved
event has to do a little bit more. As the mouse moves, the underlying component has to be monitored for change. If the component under the mouse changes, a mouseExited
event has to be dispatched to the old component and a mouseEntered
event has to be dispatched to the new component.
Furthermore, the toolTipText
property of the paintable canvas has to updated to reflect the toolTipText
of the underlying component, so tool tips will display properly. One thing that sometimes annoys me about tool tips it the fact that once they are triggered, they stay up and in the way even as they change from one component to the next. Consequently, I put my own spin on this by forcing a dismiss of the tool tip when the underlying component changes, and I don't reenable the display timer until a half a second has passed. I have found through trial and error that this timing feels the most natural.
library function for passing glass pane mouse events to the underlying components
from java.awt import MouseInfo # For determining mouse pointer relative locations
from java.awt.event import MouseEvent # For creating passable mouse entered and mouse exited events
from javax.swing import SwingUtilities, ToolTipManager # SwingUtilities for facilitating mouse point conversions and ToolTipManager for manipulating when the tooltip appears
def setGlasspaneMouseEvent(event):
# The rootContainer variable is used for getting the componentPath
rootContainer = system.gui.getParentWindow(event).rootContainer
displayContainer = system.gui.getParentWindow(event).rootContainer.getComponent(event.source.displayContainerName)
# Pass the given mouse event to the deepest underlying component with a mouse listener
if event.ID in [MouseEvent.MOUSE_CLICKED, MouseEvent.MOUSE_ENTERED, MouseEvent.MOUSE_PRESSED, MouseEvent.MOUSE_RELEASED, MouseEvent.MOUSE_DRAGGED]:
passMouseEvent(getMouseComponentAtPoint(event.source, displayContainer, event.point), event)
# Pass any mouse exited event to the previous underlying component with a mouse listener
elif event.ID == MouseEvent.MOUSE_EXITED:
passMouseEvent(getComponentFromPath(rootContainer, displayContainer.currentComponentPath), event)
elif event.ID == MouseEvent.MOUSE_MOVED:
# Locate the deepest component with a mouse listener using the getMouseComponentAtPoint library script
deepestComponent = getMouseComponentAtPoint(event.source, displayContainer, event.point)
# If a deepest component with a mouse listener is found that has toolTipText that's different from the current toolTipText...
if deepestComponent and hasattr(deepestComponent, 'toolTipText') and deepestComponent.toolTipText != event.source.toolTipText:
# Set the dismiss delay to 0, so the current tooltip will be dismissed immediately
ToolTipManager.sharedInstance().dismissDelay = 0
# Update the glass pane with the new toolTipText
event.source.toolTipText = deepestComponent.toolTipText
# Set the reshow delay to one second (Trial and error determined that this looks the best at run time)
ToolTipManager.sharedInstance().reshowDelay = 1000
# Delay the restore for a half a second
def restoreToolTip():
# Reset the dismiss delay
ToolTipManager.sharedInstance().dismissDelay = 5000
system.util.invokeLater(restoreToolTip, 500)
# Pass the mouse moved event to the deepest component with a mouse listener
passMouseEvent(deepestComponent, event)
# Get the component path for the previous mouse event and the current mouse event
# ...These are coded strings that represent how to get references the specific components in the content container
oldComponentPath = displayContainer.currentComponentPath
newComponentPath = getComponentPath(rootContainer, deepestComponent)
# If the new path is now different from the old path, that means the mouse has left the previous component and entered a new component,
# ...so it's time to trigger each component's relative mouse entered and exited events
if newComponentPath != oldComponentPath:
# Get the old and new components and a point that is relative to the screen
oldComponent = getComponentFromPath(rootContainer, oldComponentPath)
newComponent = getComponentFromPath(rootContainer, newComponentPath)
currentPoint = MouseInfo.getPointerInfo().location
# Verifiy that the old component exists, create a mouse exited event, and pass it to the component
if oldComponent:
mouseExited = MouseEvent(
oldComponent,
MouseEvent.MOUSE_EXITED, # Type of MouseEvent
system.date.toMillis(system.date.now()), # Timestamp
0, # Modifiers
SwingUtilities.convertPoint(event.source, currentPoint, oldComponent).x, # X coordinate
SwingUtilities.convertPoint(event.source, currentPoint, oldComponent).y, # Y coordinate
0, # Click count
False) # Popup Trigger?
passMouseEvent(oldComponent, mouseExited)
# Verifiy that the new component exists, create a mouse entered event, and pass it to the component
if newComponent:
mouseEntered = MouseEvent(
newComponent,
MouseEvent.MOUSE_ENTERED, # Type of MouseEvent
system.date.toMillis(system.date.now()), # Timestamp
0, # Modifiers
SwingUtilities.convertPoint(event.source, currentPoint, newComponent).x, # X coordinate
SwingUtilities.convertPoint(event.source, currentPoint, newComponent).y, # Y coordinate
0, # Click count
False) # Popup Trigger?
passMouseEvent(newComponent, mouseEntered)
# Update the currentComponentPath custopm property to represent the new component
displayContainer.currentComponentPath = newComponentPath
That's it! That's all that's needed to build a generic paintable canvas glass pane that is perfectly imperceptible to the user but can be used to dynamically paint anything that is needed anywhere in a window.
Here is a copy of the test window and library script that were both used to illustrate this tutorial. Note how the behavior of the underlying components is indistinguishable from the behavior of the same components in a window without an overlay.
GlasspaneLibraryScripts.zip (3.7 KB)
GlasspaneTestWindow.zip (49.7 KB)
...and have fun adding unlimited functionality to your projects using the paintable canvas as an overlay