Designer quality of life "mods"

I recognize this would not be a simple change, but perhaps the answer would be not to search the OPC server itself, but to search a cache of the information contained within the opc server. Then when a tag is selected, it can retrieve the actual OPC tag.

You could also limit the search to only be enabled after an OPC device connection has been selected.

Fuzzy search for OPC isn't possible, you can only fuzzy search if you had the entire address space in memory to search in. OPC browsing is live and dynamic.

Any search would require first doing a recursive browse of the tree you wish to search in. Basically a non-starter.

This doesn't really belong here... but at least while the session.custom props disappearing act bug is still around, I've also added a tool to backup the session.custom props (note: not the session.props) up from the file on the gateway (10 backups total), detect if the file has lost its props and flash a toolbar icon if so, and allow you to select the backup to restore and restore it. It's a bit hard to share at the moment as it's using functions from a few other libraries part of a larger collection... but here it is in action!

Ooooh very fancy. Definitely could have used this a few times!

Sad that this even needed to be developed as a bandaid, but kudos to you for it!

One thing I was reminded of today when exporting some resources to send to the exchange was that it would be nice if the resources would be in structured folders instead of a long list of items under each module. It would make selecting a lot of resources in a folder easier as I could just select the folder rather than clicking each individual resource. Plus it would clean up the list (this may be fixed in 8.3 but we haven't moved to 8.3 yet pending some CirrusLink ABB drivers.)

Hey @nminchin . With all of the integrations to the designer you have figured out, is there a way you can create custom keyboard shortcuts? One QOL I have wanted for a long time is the ability to assign keyboard shortcuts to things like the alignment buttons at the top of the toolbar (center, align top, align bottom, etc). Do you think that is even possible and if you have done something like that could you point me in the right direction? Thanks!

A qol feature I would like is a collapse/expand (all/region/current) for every tree-like interface in the Designer. ie. Perspective Views, Styles, Named Queries etc.

My main use case is for Perspective Views, it would save me lots of time instead of drilling down with double-clicks.

The project scripts and script console offers the same concept with fold/unfold.

If this was ever implemented, I would kindly ask for the same feature with Perspective properties (I'm sure you could all agree on this one?).

very nice. Just got bit by the missing session.custom stuff 2x last week.. If it was just props it wouldn't be to bad.. but i've got bindings on most..

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

I love blocking use of tag() in UI bindings...

Your tooltip isn't quite right. tag() does make a subscription to the target like an indirect binding, and is not making a conventional blocking read. The problem is that expressions have to deliver a result every time, and tag() stalls for up to 1000ms trying to get that first subscription result when the expression runs the first time. In a UI, especially a Vision UI, lots of tag() expressions yields pathological expression startup stalls.

tag() also pays a slight penalty on every value change to re-execute whatever tag-path generating expression is used. Indirect tag bindings decouple the indirection from the value delivery.

Expression tags don't really suffer from the tag() startup stall because they are in the tag subsystem.

Perhaps:

They improve performance by decoupling tag target determination from value delivery, while avoiding pathological expression startup UI stalls.

Thanks for the feedback. I've updated the script accordingly

You can just hold ctrl or shift, I always forget, and click on the thing that's netaed and it'll select it

Amazing. I typically navigate through the Project Browser but this warrants a workflow adjustment. Thanks @nminchin

This one is already a ticket, for what it's worth.

One papercut I have already done, though this is only going to be in 2027.2+:

Automatically jumps your project browser selection including expanding folders to whatever view/script/query/etc you have open in the current workspace.

Would be nice to have an updated database query browser that allows a different database connection per tab and doesn't clear out the contents of the tab when switching connections.
There should already be an existing ticket from a decade ago.

Another QOL thing I'd like to see is the multi instance wizard not automatically closing after you create tags. There's many times I need to create related tags of 2 different UDTs that will use the same patterns as the first set of tags but maybe with a different suffix or prefix. I have to re-open the wizard and re-enter everything again instead of just picking the next UDT and making my small name change.

When you have some views opened in the designer, and the ressources tree is collapsed and you need to search for your view to be abe to select some components of it.
It will be awesome if we could right click on view tab to re-open the ressouce tree on the selected view.

Especially relevant when the tree collapses undesirably, such as when overriding (or discarding overrides on) views from a parent project.

On a related note...

I'd love the ability to open ALL parent resources (especially views) in read-only mode, without overiding first, much like script library allows.

I haven't used this basically ever since I discovered early on that it wasn't efficient for UDT instances with nested UDT instances. Normally I set the nested UDT instance parameters to inherit or partially inherit param values from the main parent, but if you didn't supply every nested udt instance param values, they would just get written to blanks... This was back in v7.9.5 so I don't know if this has changed since, but maybe this is a QOL improvement as well to fix this if not

+1 ! This would be great!