Tag Expression Blocker for Vision
An initialization script that inhibits the introduction of any new GUI blocking tag expressions
With this patch in place, the ok button on the expression binding editor will become disabled and display a warning icon with a helpful tool tip message if an inexperienced developer attempts to introduce a tag expression anywhere other than an expression tag:
The Library Script:
from com.inductiveautomation.factorypmi.designer.property.configurators import DynamicOptsDialog
from java.awt import AWTEvent, BorderLayout, MouseInfo, Toolkit
from java.awt.event import ActionListener, AWTEventListener, HierarchyListener, WindowAdapter
from com.inductiveautomation.ignition.client.images import ImageLoader
from javax.swing import ImageIcon, JDialog, JLabel, SwingUtilities
from javax.swing.event import DocumentListener, ListSelectionListener
WARNING_ICON = ImageLoader.getInstance().loadImage("Builtin/icons/16/warning.png")
# Returns the first nested component of a given class
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getComponentOfClass(container, className):
for component in container.components:
if component.__class__.__name__ == className:
return component
foundComponent = getComponentOfClass(component, className)
if foundComponent:
return foundComponent
# Yields all nested components of a given class.
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getAllComponentsOfClass(container, className):
for component in container.components:
if component.__class__.__name__ == className:
yield component
for found in getAllComponentsOfClass(component, className):
yield found
def getActionFilter(originalListener, configuratorList):
class ActionFilter(ActionListener):
def __init__(self, originalAction, configuratorList):
self.originalAction = originalAction
self.configuratorList = configuratorList
def actionPerformed(self, event):
parentWindow = SwingUtilities.getAncestorOfClass(DynamicOptsDialog, event.source)
expressionPainter = getComponentOfClass(parentWindow, 'CodeEditorPainter')
if not expressionPainter or getJListSelectedText(self.configuratorList) != 'Expression' or isBindingTextValid(expressionPainter.codeEditor.text):
self.originalAction.actionPerformed(event)
else:
setOKButtonText(event.source, False)
setWarningPopup(event)
return ActionFilter(originalListener, configuratorList)
def getJListSelectedText(jlist):
selectedIndex = jlist.selectedIndex
if selectedIndex < 0 or selectedIndex >= jlist.model.size:
return None
value = jlist.model.getElementAt(selectedIndex)
renderer = jlist.cellRenderer
component = renderer.getListCellRendererComponent(jlist, value, selectedIndex, False, False)
if isinstance(component, JLabel):
return component.text
return unicode(value)
def getSelectionListener(okButton, expressionEditor):
class ConfiguratorSelectionListener(ListSelectionListener):
def __init__(self, okButton, expressionEditor):
self.okButton = okButton
self.expressionEditor = expressionEditor
def valueChanged(self, event):
if event.valueIsAdjusting:
return
selectedConfigurator = getJListSelectedText(event.source)
if selectedConfigurator == 'Expression':
setOKButtonText(self.okButton, isBindingTextValid(self.expressionEditor.text))
else:
setOKButtonText(self.okButton, True)
return ConfiguratorSelectionListener(okButton, expressionEditor)
# Call this to block the introduction of any new gui locking tag expressions
# ...will not affect preexisting tag expressions other than subsequent binding edits will be blocked until the original binding is corrected
def setBindingEditorPatch():
# Detects when the binding editor window is opened and applies the patch
class BindingWindowMonitor(AWTEventListener):
def eventDispatched(self, event):
if event.__class__.__name__ != 'WindowEvent':
return
if event.ID != event.WINDOW_OPENED:
return
window = event.window
if not isinstance(window, DynamicOptsDialog):
return
okButton = next((button for button in getAllComponentsOfClass(window, 'JButton') if button.text == 'OK'), None)
if not okButton:
return
configuratorList = getComponentOfClass(window, 'SyntheticaSafeGroupList')
if not configuratorList:
return
for listener in okButton.actionListeners:
if listener.__class__.__name__ == 'DynamicOptsDialog$1':
okButton.removeActionListener(listener)
okButton.addActionListener(getActionFilter(listener, configuratorList))
elif listener.__class__.__name__ == 'ActionFilter':
okButton.removeActionListener(listener)
okButton.addActionListener(getActionFilter(listener.originalAction, configuratorList))
# During testing, this was ALWAYS found
expressionConfigurator = getComponentOfClass(window, 'ExpressionConfigurator')
if not expressionConfigurator:
return
# During testing this was NOT FOUND more times that it was found
expressionPainter = getComponentOfClass(expressionConfigurator, 'CodeEditorPainter')
if expressionPainter:
setExpressionEditorListeners(expressionPainter, okButton, configuratorList)
return
# When the expression painter is not found, add a listener to detect when it is added,
# ...so a key listener can be put in place to detect any tag expression attempts
for listener in expressionConfigurator.hierarchyListeners: # Don't allow outdated instances of this listener to persist
if listener.__class__.__name__ == 'PainterListener':
expressionConfigurator.removeHierarchyListener(listener)
expressionConfigurator.addHierarchyListener(PainterListener(okButton, configuratorList))
# Unfortunately the expression editor is not always present when the binding editor is opened,
# ...so this listener will detect the painter when it gets added, and apply the required key listener
class PainterListener(HierarchyListener):
def __init__(self, okButton, configuratorList):
self.okButton = okButton
self.configuratorList = configuratorList
def hierarchyChanged(self, event):
expressionPainter = getComponentOfClass(event.source, 'CodeEditorPainter')
if expressionPainter:
# Stop listening once expression painter has been located
event.source.removeHierarchyListener(self)
setExpressionEditorListeners(expressionPainter, self.okButton, self.configuratorList)
# Add a listener to the designer to detect the initial opening of the binding editor
toolkit = Toolkit.getDefaultToolkit()
for proxy in toolkit.AWTEventListeners:
if proxy.listener.__class__.__name__ == 'BindingWindowMonitor':
toolkit.removeAWTEventListener(proxy.listener) # Remove any outdated listener instances
toolkit.addAWTEventListener(BindingWindowMonitor(), AWTEvent.WINDOW_EVENT_MASK)
def isBindingTextValid(text):
# Normalize the text and eliminate line breaks, odd capitalization, and whitespace before evaluation,
# ...because those weird things are allowed and could be present in the expression
comparisonText = ''.join(text.lower().split())
return not 'tag(' in comparisonText
def setOKButtonText(okButton, isValid):
if isValid:
okButton.toolTipText = None
okButton.icon = None
else:
okButton.toolTipText = unicode('<html>Tag Expressions are not permitted in UI bindings.<br>' +
'• Use indirect tag bindings instead.<br>' +
'• If an expression is required, bind the tag values to individual custom properties,<br>' +
' and reference those custom properties in the expression binding.</html>')
okButton.icon = ImageIcon(WARNING_ICON)
# This listener blocks a user's from being able to commit any gui binding edit that contains 'tag('
def setExpressionEditorListeners(expressionPainter, okButton, configuratorList):
class TagExpressionDocumentListener(DocumentListener):
def __init__(self, expressionEditor, okButton):
self.expressionEditor = expressionEditor
self.okButton = okButton
def validate(self):
setOKButtonText(self.okButton, isBindingTextValid(self.expressionEditor.text))
def insertUpdate(self, event):
self.validate()
def removeUpdate(self, event):
self.validate()
def changedUpdate(self, event):
self.validate()
expressionEditor = expressionPainter.codeEditor
document = expressionEditor.document
for listener in document.documentListeners:
if listener.__class__.__name__ == 'TagExpressionDocumentListener':
document.removeDocumentListener(listener)
document.addDocumentListener(TagExpressionDocumentListener(expressionEditor, okButton))
for listener in configuratorList.listSelectionListeners:
if listener.__class__.__name__ == 'ConfiguratorSelectionListener':
configuratorList.removeListSelectionListener(listener)
configuratorList.addListSelectionListener(getSelectionListener(okButton, expressionEditor))
def setWarningPopup(event):
class CloseOnDeactivate(WindowAdapter):
def windowDeactivated(self, event):
event.getWindow().dispose()
headerTitle = 'Tag Expression NOT Permitted'
popupText = unicode('<html>Tag Expressions are not permitted in UI bindings.<br>' +
'• Use indirect tag bindings instead.<br>' +
'• If an expression is required, bind the tag values to individual custom properties,<br>' +
' and reference those custom properties in the expression binding.</html>')
parent = SwingUtilities.getWindowAncestor(event.source)
# set the header for the popup
dialog = JDialog(parent, headerTitle, False)
# Add the focus handler to the dialog
dialog.addWindowListener(CloseOnDeactivate())
# Add the custom icon to the header bar of the dialog
dialog.setIconImage(WARNING_ICON)
# Add the internal message to a label, and configure the popup
label = JLabel(popupText, JLabel.LEFT)
# Set the layout and size of the dialog
dialog.setLayout(BorderLayout())
dialog.add(label, BorderLayout.NORTH)
dialog.setSize(480, 120)
# Define an offset margin to be used for positioning calculations.
offsetMargine = 100
# Get the current location of the mouse pointer.
location = MouseInfo.getPointerInfo().location
# Calculate the default X and Y coordinates for the dialog, offset by the margin.
defaultX = location.x - offsetMargine
defaultY = location.y - offsetMargine
# Set the location of the dialog to the calculated X and Y coordinates.
dialog.setLocation(defaultX, defaultY)
# Show the popup
dialog.setVisible(True)
This approach is backward compatible because it prevents the creation of new tag expression bindings without affecting the existing ones. It also does not restrict the use of tag expressions within expression tags.
To automatically add this patch when the designer is first opened, use a dateTime Vision client tag that has its value bound to the expression now(0) and simply call the setBindingEditorPatch() library script on value change.
Edits:
• Simplified tool tip text
• Made detection more robust ~ switching from a key adapter to a document listener and capturing the ok button action event
• Added lightweight dialog popup that occurs if an unpermitted ok button click occurs
• Improved visualizations by keeping the okay button enabled and by reevaluating the warning label when on binding configurator change