Ignition 8.1 PowerTable header background color

Is it possible to use a Vision Client Startup Script to change all instances of Power Table headers background to a different color?

On 8.1 it seems that the header uses the same color as the table instead of staying white like it did previously

I tried this, but it didn't work to change the header color:

from java.awt import Color
from javax.swing import UIManager

UIManager.put('TableHeader.background', Color.white)

Is there perhaps another magic key i can use that drives the background color of the power table header?

I know we can utilize the configureHeaderStyle extension function but we have 1800 screens and I really don't want to have to go through all of them to change this manually

def configureHeaderStyle(self, colIndex, colName):
	return { 'background': 'white' }

Thanks!

You're saying you have 1800 separate power tables, and you're wanting to globally change the default color?

We have 1800 vision windows.

I do not want to manually go through ALL of them to determine which have power tables that need their header background colors updated.

I know it's possible to use UIManager to set the look and feel of objects in the client, just don't know if it's possible to do for the power table header background color. I can use the same code and change TableHeader.background to TableHeader.foreground and have that apply to all power tables. Just wasn't sure if it was possible for the background color of the header :crossed_fingers:

You will probably have to go about this differently. If I'm not mistaken, the header of a power table is created via a JViewport, and in ignition 8.1, the UIManager for a JViewport is a SynthViewportUI.

Verification script:

from javax.swing import JViewport
from javax.swing import UIManager
print UIManager.getUI(JViewport())

Output:
javax.swing.plaf.synth.SynthViewportUI

I don't believe anybody has tried this yet, and I would be quite interested to see if you could get it to work:

Ok, so messing around with these settings.

None of them seem to really have any affect in the client.

If i run this in the designer, then interact with the table it makes the background of the table white, but not the background of the header:

from java.awt import Color
from javax.swing import UIManager
UIManager.put('Synthetica.viewport.dynamicBackgroundColorEnabled', False)
UIManager.put('Viewport.background', Color.white)

image

Poking around the structure of the table, it seems the header uses a different type. But I'm not sure what to do with that information

table = event.source.parent.getComponent('Power Table')
print table.getComponent(3).getComponent(0).getUI()

prints out: com.jidesoft.plaf.synthetica.SyntheticaAutoFilterTableHeaderUI@2530beec

I can't find any info on that and I'm not sure what UIManager key would even control the background color for it, if any :frowning:

1 Like

Interesting. I haven't encountered that before in my own various power table diggings. I'm looking forward to exploring this.

This could be a useful resource for this type of activity:
List of all UIResource Elements for Predefined Look and Feel Classes

Well... based on this forum post I don't think this is possible how I want to do it :frowning:

Such a bummer, but I guess I need to suck it up and go into each window / table and do it manually

As a middle ground, you could make a script inside the designer that opens every window, looks inside it for any power tables, and manually adds this extension function implementation; see ExtensibleComponent and ExtensionFunction. Whether scripting that together is going to be faster than doing it manually is perhaps a subject of debate.

2 Likes

We do already have code that goes through each window and pulls out all of the pertinent configuration for each screen and stores it in a database, so it might be trivial to extend upon that, I guess I would just be weary about mucking something up... but i guess that's what backups are for :sweat_smile:

Thank you for the idea! That should speed things up tremendously

1 Like

@PGriffith I was hoping you would chime in at some point. It seems like you tend to have the best ideas when it comes to this sort of thing.

So... is it actually possible to enable extension functions programmatically?

Or do I need to call the ExtensionFunction constructor to rebuild it?

Something along the lines of this:

extensionFunctions = subComponent.getExtensionFunctions()
configureHeaderStyle = extensionFunctions['configureHeaderStyle'] 
			
if not configureHeaderStyle.enabled:
	newExtensionFunction = ExtensionFunction(True, configureHeaderStyle.getScript() )
	extensionFunctions = extensionFunctions.put('configureHeaderStyle', newExtensionFunction )

You would have to build a new Map<String, ExtensionFunction> and then reapply that onto the component.

So, from your example, probably more like this:

extensionFunctions = dict(subComponent.getExtensionFunctions())

if "configureHeaderStyle" in extensionFunctions:
	configureHeaderStyle = extensionFunctions['configureHeaderStyle'] 
	if not configureHeaderStyle.enabled:
	newExtensionFunction = ExtensionFunction(True, configureHeaderStyle.script)
	extensionFunctions['configureHeaderStyle'] = newExtensionFunction

	subComponent.setExtensionFunctions(extensionFunctions)

Possible complications:

  • I don't think it's guaranteed that you get a mutable map from getExtensionFunctions (hence the dict call).
  • I think configureHeaderStyle might be null in certain circumstances?
  • The 'script' to supply to the ExtensionFunction header must include the def, the parameters, and the docstring to work properly going forward.

Also, you didn't hear about any of this from me and don't contact support if it breaks anything :wink:

3 Likes

Using @PGriffith's direction, I have developed a script that will accomplish the goal of globally changing all power table header configurations from the designer. The script opens every vision window, then loops through getting all of the components from each window's root container. Then it checks each component list for power tables and nested containers. If there is a nested container, it repeats the process until there are no more nested containers.*

*For each power table found, if the configureHeaderStyle extension function does not exist or is not enabled, the script applies a configureHeaderStyle extension function that changes the color of the header to white. No other preexisting extension functions are altered or affected. The applied extension function string is in line three, so if a different color or additional properties are needed, this is where it will need to be changed.

Here is the script:

def processHeader(subComponent):
	from com.inductiveautomation.vision.api.client.components.model import ExtensionFunction
	script = 'def configureHeaderStyle(self, colIndex, colName):\n	return {\'background\': \'white\'}'
	try:
		extensionFunctions = subComponent.getExtensionFunctions()
		if "configureHeaderStyle" in extensionFunctions:
			configureHeaderStyle = extensionFunctions['configureHeaderStyle'] 
			if not configureHeaderStyle.enabled:
				updatedExtensionFunction = ExtensionFunction(True, script)
				extensionFunctions['configureHeaderStyle'] = updatedExtensionFunction
				subComponent.setExtensionFunctions(extensionFunctions)
		else:
			updatedExtensionFunction = ExtensionFunction(True, script)
			extensionFunctions['configureHeaderStyle'] = updatedExtensionFunction
			subComponent.setExtensionFunctions(extensionFunctions)
	except:
		from java.util import HashMap
		updatedExtensionFunction = ExtensionFunction(True, script)
		Map = HashMap()
		Map.put('configureHeaderStyle', ExtensionFunction(True, script))
		subComponent.setExtensionFunctions(Map)
def containerProcessor(components):
	for subComponent in components:
		if 'VisionAdvancedTable' in str(type(subComponent)):
			processHeader(subComponent)
		elif 'Container'in str(type(subComponent)):
			containerComponents = subComponent.getComponents()
			loopThroughComponents(containerComponents)
def loopThroughComponents(components):
	for subComponent in components:
		if 'VisionAdvancedTable' in str(type(subComponent)):
			processHeader(subComponent)
		elif 'Container'in str(type(subComponent)):
			containerComponents = subComponent.getComponents()
			containerProcessor(containerComponents) 
for name in system.gui.getWindowNames():
	system.nav.openWindow(name)
	components = system.gui.getWindow(name).getRootContainer().getComponents()
	loopThroughComponents(components)
	system.nav.closeWindow(name)

Edit: Everything in the above script now works correctly. The initial version seemed to work, but when I attempted to save the changes, the designer threw an exception due to unhashable dictionaries. This was fixed by removing all dictionary casts and methods, and by setting up exception handling that creates a hashmap for power tables that return a null value during the getExtensionFunctions method because they have never had any extension functions enabled.

Subsequent Edit: It occurred to me that opening 1800 windows in the designer would probably be a bad idea. I added a line of code to close each window upon edit completion.

5 Likes

Good stuff!

You could replace the string type checks with imports; should just need BasicContainer and VisionAdvancedTable. Because of Vision's serialized nature, these classpaths are stable and have been for more-or-less a decade.

2 Likes

Well...

I couldn't let @justinedwards.jle have all the fun!

here is my implementation:

from com.inductiveautomation.factorypmi.application.components import VisionAdvancedTable
from com.inductiveautomation.vision.api.client.components.model import ExtensionFunction
from java.util import HashMap

# Config
writeResultsToTags = True
baseTagPath = '[default]PowerTableSetHeaderColor'
tags = ['changedTablesOnTheseWindows', 'checkTheseWindowsManually']
batchSize = 400
batchNo = 0
baseWindowPath = 'Main Windows'
pathExclusions = [
	'dev_',
	'Popups'
]

# Filter our windows list (must start with baseWindowPath and all pathExclusion conditions must be met
windows = [win for win in system.gui.getWindowNames() if win[:len(baseWindowPath)] == baseWindowPath and all([exclusion.lower() not in win.lower() for exclusion in pathExclusions ])] # 2014
windowCount = len(windows)

# Grab the current batch of windows
if batchNo == 0:
	windows = windows[:batchSize]
	print 0, batchSize
else:
	start = batchSize*batchNo
	end = start+batchSize 
	end = end if end < windowCount else windowCount
	print start, end
	windows = windows[start:end]

scriptToAdd = '''def configureHeaderStyle(self, colIndex, colName):
	"""
	Provides a chance to configure the style of each column header. Return a
	dictionary of name-value pairs with the desired attributes. Available
	attributes include: 'background', 'border', 'font', 'foreground',
	'horizontalAlignment', 'toolTipText', 'verticalAlignment'

	Arguments:
		self: A reference to the component that is invoking this function.
		colIndex: The index of the column in the underlying dataset
		colName: The name of the column in the underlying dataset
	"""
	from javax.swing.border import MatteBorder
	from java.awt import Color
	return { 'background' : Color.white, 'border': MatteBorder(0, 0, 1, 1, Color(164, 168, 172)) }'''

changedTablesOnTheseWindows = []
checkTheseWindowsManually = []
def searchThroughWindow(windowPath, component):
	for subComponent in component.getComponents():
		if isinstance(subComponent, VisionAdvancedTable):			
			extensionFunctions = subComponent.getExtensionFunctions()
			
			# Sometimes extensionFunctions is `None` if no extension functions are set on a table
			extensionFunctions = dict(extensionFunctions) if extensionFunctions else {}
			
			# Only apply this to tables where configureHeaderStyle exists but isn't enabled
			# or tables without configureHeaderStyle setup all together
			configureHeaderStyle = extensionFunctions.get('configureHeaderStyle')
			if (configureHeaderStyle and not configureHeaderStyle.enabled) or not configureHeaderStyle:
				extensionFunctions['configureHeaderStyle'] =  ExtensionFunction(True, scriptToAdd) 
				
				# Convert our `extensionFuncitons` dict back into a HashMap
				# writing our dict back to the component will throw an error 
				# becaues the new value (a map) isn't serializable
				newExtensionFunctionsMap = HashMap()
				for key, value in extensionFunctions.iteritems():
					newExtensionFunctionsMap.put(key, value)
				subComponent.setExtensionFunctions(newExtensionFunctionsMap)
				
				changedTablesOnTheseWindows.append([windowPath, subComponent.name])
			# Keep a record of WindowPath + Table Name for manually applying these changes to 
			# Tables that already have a `configureHeaderStyle` extension function
			else:
				checkTheseWindowsManually.append([windowPath, subComponent.name])
		
		# Dig deeper into window	
		searchThroughWindow(windowPath, subComponent)

for win in windows:
	windowReference = system.nav.openWindow(win)	
	searchThroughWindow(win, windowReference)	
	system.nav.closeWindow(win)

if writeResultsToTags:
	batchFolderPath = '{}/{}'.format(baseTagPath, batchNo)
	fullTagPaths = ['{}/{}'.format(batchFolderPath, tag) for tag in tags]
	
	# Create current batch folder if it doesn't exist
	if not system.tag.exists(batchFolderPath):
		system.tag.configure(baseTagPath, {'name': batchNo, 'tagType': 'Folder'})
	
	# Create tags if they don't exist
	for idx, tag in enumerate(fullTagPaths):
		if not system.tag.exists(tag):
			system.tag.configure( batchFolderPath, { 'name': tags[idx], 'valueSource': 'memory', 'tagType': 'AtomicTag','dataType': 'DataSet' } )
	
	# Write results to tags		
	headers = ['windowPath', 'tableName']
	system.tag.writeBlocking( 
		fullTagPaths, 
		[
			system.dataset.toDataSet(headers, changedTablesOnTheseWindows), 
			system.dataset.toDataSet(headers, checkTheseWindowsManually)
		]
)

It supports window filtering, batching, and writing the results to tags.
My work laptop is horrible and is screaming while doing this so the batching was a must. Even doing batches of 400 windows at a time is still taking upwards of 30-40 minutes per batch :frowning:
image

Thank you both for all of the help. I can get by doing these in small batches. It's still much faster than having to do this manually like some barbarian

2 Likes

I've set up a virtual machine that I do my playing around on, and it isn't fast, but the sandbox Vision instanced I created to test this script only had 4 windows with various nested power tables, so the script completed almost instantly. Nevertheless, I was at some point pondering the scale of your undertaking, and I had imagined that adding some sort of batching method would be desirable. Especially after I decided to make sure the script result still worked outside of the designer, and I discovered the dictionary hashing error. I imagine that it would be terrible to have processed 1800 windows only to discover that none of the work was savable! In any case, I'm glad we were able to figure this problem out; I'm sure that this work will come in handy elsewhere in the future.

heh I'm sure glad I implemented it too because my first batch resulted in failure because of the aforementioned hashing error :sweat_smile:

Yes! I already can see using this again in the future to validate our non-templatized components (multi-state indicators :face_vomiting:) are all setup the same way and fix the outliers

4 Likes