Dynamic component ordering

Does anyone know of a good way to make a dynamically ordered container? I am working on a maintenance/manual override view with a table listing out all relevant I/O connected to the PLC. Along the right side is a container (Currently a Flex Column) that is parent to Coordinate Container that is the manual override control.

The toggle switch controls the selected output, and the check box assigns the container to the selected output.

The Override container block has been designed to allow for multiple instances to be added. I would like to always have one unassigned block visible. This isn't hard, however if for example there are four outputs being controlled, and five blocks visible. If the first block is deselected("Lock selection" is unchecked) all the remining blocks should be moved up and all new selections should be placed at the bottom of the coulomb. I think this can be done with coordinate containers, but I'm not sure if there is a better way.

A flex repeater where you manually juggle the instances array?

If that isn't an option for some reason, a View Canvas...where you manually juggle the instances array?

1 Like

I'm fairly new to Ignition, how do I change an instance position in a flex repeater?

Flex repeaters take a list of dictionaries for their instances, and the order of the items in the list is the displayed order. Move/change the item's position in the list and the change will be reflected in the flex repeater.

Something like this:

def swapArrayPositions(sourceArray, pos1, pos2):
	""" """
	sourceArray[pos1], sourceArray[pos2] = sourceArray[pos2], sourceArray[pos1]
	return sourceArray

should let you swap positions of two items in the list.

2 Likes

Is it posable to dynamically create instances, or do they need to be defined? I'm thinking of having one instance defined, and add another when an output is selected.

Yes

This is completely possible. I have a case where once the input is completed in one instance, I add a new blank instance to allow more entry. You pretty much just need to add or remove items from the instance list via script, and the flex repeater will update accordingly.

Just have a value change script on the value from the selection that either adds or removes the extra instance from the list depending on the current value.

You could go even simpler and have both instances already defined, and bind the visibility of the second to the selection value.

Hertz

Tip: Hz or hertz but never "hz"!
SI units named after a person have their symbols capitalised and are lowercase when spelt out.

Tip: Hz or hertz but never "hz"!

I know, its a placeholder for motor drive speed settings. It will be RPM when I figure out what they are.

I finally found time to work on this project again. The dynamic ordering is mostly working, it is using a message handler to update the FlexRepeter Instance structure.

def onMessageReceived(self, payload):
	"""
	This method will be called when a message with the matching type code
	arrives at this component.

	Arguments:
		self: A reference to this component
		payload: The data object sent along with the message
	"""
	bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}
	inst = []
	length = 0
	try: 
		inst = list(self.props.instances)
		length = len(inst)-1
		if inst is None or length < 0:
			exception
#		system.perspective.print("used existing structure "+str(length))
	except:
#		system.perspective.print("created new structure")
		inst = []
		inst.append(bInst)
		length = len(inst)
	
	loop = True
	if length < 0 or inst == None:
		loop = False
	index = 0
	if loop:
#		for y in range(0,1):
			for x in inst:
				# remove unused eliments
				if not x['locked'] and length > 0:
					if index != length:
						inst.pop(index)
						system.perspective.print("removed item at index: "+str(index))
				# check if last eliment is locked
				if index == length and x['locked']:
					inst.append(bInst)
					system.perspective.print("added Item")

				length = len(inst)-1	
				index += 1

	self.custom.index = length

	# update main display component
	self.props.instances = inst
	self.refreshBinding('props.instances')

Is there a better way to send the message then a "onMouseMove" event on the root container? I tried using a Gateway Timer Script, but that didn't work. It seams like a waste of server resources to run this script every time the mouse curser moves. I'm also not sure how an "onMouseMove" event works with a touch screen.

All Perspective scripts run in the gateway server. If triggered by UI interactions like MouseMove, then yes, quite the waste. Consider including buttons in each row that trigger the desired motions upon mouse click. Much more reasonable traffic and scripting overhead.

3 Likes

Does that work like the "onClick" or "onMouseDown" events? I tried both of these with not much succus. Ideally I would like it to send the message each time a check box changes state.

Send a message using the onActionPerformed script of the checkbox.

Your loop can be reduced to

def onMessageReceived(self, payload):
	"""
	This method will be called when a message with the matching type code
	arrives at this component.

	Arguments:
		self: A reference to this component
		payload: The data object sent along with the message
	"""
	bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}
	inst = []
	length = 0
	try:
		inst = list(self.props.instances)
		length = len(inst)-1
		if inst is None or length < 0:
			exception
	#		system.perspective.print("used existing structure "+str(length))
	except:
		#		system.perspective.print("created new structure")
		inst = []
		inst.append(bInst)
		length = len(inst)

	# Only update if there are items present and our instance exists
	if length > 0 and inst is not None:
		newInstanceList = [x for idx, x in enumerate(inst) if x['Locked'] and idx > 0]
		if newInstanceList[-1]['Locked']:
			newInstanceList.append(bInst)
	
	else:
		newInstanceList = inst
	
	self.custom.index = len(newInstanceList)

	# update main display component
	self.props.instances = newInstanceList
	self.refreshBinding('props.instances')

I'm not sure what exception is supposed to be...
Is it supposed to raise an exception so the except clause is executed ? That seems like a very convoluted way of doing things.
Otherwise, what is exception, and what do you expect will trigger the except ?

Also, If that's the case, then the following condition will always be true, since if the list is empty, you're putting things in it. Or am I completely misunderstanding what's going on ?

Then, you're filtering your list, keeping only those where locked is True, and right after that you're checking if the last element in the list is locked. This will either be True if the list is not empty, or raise an exception because you're accessing a property of an object that doesn't exist.

I'd rewrite it, but frankly I'm quite lost: I have no clue what the whole thing should do.

Note: I based this comment on Ryan's code, which he says should be equivalent to your own code. The original code contains other things that you really shouldn't do, like popping things from the list you're looping through - that's a recipe for disaster.

If you can explain in more details what you're trying to accomplish, I'll give it a shot.

1 Like

I'm probably missing something simple, but sending the message from an actionPreformed event on the checkbox doesn't run the script. I tried a button in the same container as the FlexRepeter, and that didn't work ether.

I think exception throws an exception that breaks out of the try except statement. As far as I know it's unique to Ignition.

I am trying to build a dynamic list of control tiles for a maintenance interface. If the "lock selection" checkbox is checked, I want a new tile to be created below. If the checkbox on any tile is unchecked, I want that tile to be removed.

Some of the code I posted may not be very well written as this project is well beyond anything I've done in Ignition.

Did you use system.perspective.sendMessage? Did you have it send at the page level? Did you have the handler set to listen at the page level?

I send messages from templates to top level pages all the time. Its likely that your messages/message handlers were mis-configured.

Consider changing your code approach entirely. Send a message from the instance that was toggled including its index in the payload. Pop only that index from the list and return the new list, or in the case of adding an instance, check that the instance is last in the list and append a new item to the list.

That's the method that I use. The examples below are from one of my interfaces where I can select an instance and delete it or add an instance.

Add Item Method
def addMultiEntryInstance(sourceItems, DBHandler):
	""" """

	# Attribute check on passed object to prevent calling a method that does not exist
	if not hasattr(DBHandler, "new"):
		logger.warn("Provided handler has no method 'new'")
		return sourceItems

	sourceItems = shared.util.sanitizeIgnitionObject(sourceItems)
	blankItem = DBHandler.new()
	blankItem.update({"Instance": len(sourceItems)})

	sourceItems.append(blankItem)

	return sourceItems
Remove Item Method
def removeMultiEntryInstance(sourceItems, instanceIdx):
	""" """

	# Bounds check for instance index
	if instanceIdx == -1 or instanceIdx >= len(sourceItems):
		return sourceItems

	sourceItems = shared.util.sanitizeIgnitionObject(sourceItems)
	sourceItems.pop(instanceIdx)

	if len(sourceItems) == 0:
		return []

	# Rebuild instance index numbers
	for idx, item in enumerate(sourceItems):
		item.update({"Instance": idx})

	return sourceItems

The Message Handler is set to listen at the Page level. I don't know how to check if the message is being sent at the Page level. This is the first time I have used Messages in Ignition.

def runAction(self, event):
	"""
	This event is fired when the 'action' of the component occurs.

	Arguments:
		self: A reference to the component that is invoking this function.
		event: An empty event object.
	"""
	if not self.props.selected:
		self.getSibling("ToggleSwitch_1").props.selected = False
		self.props.selected = False
	
	messageType = 'updateList'
	system.perspective.sendMessage(messageType)
	system.perspective.print('msg sent')

Your example looks a lot simpler then what I came up with.

The manual specifies the additional parameters you can use when calling sendMessage. Also make sure the name of the message you are sending matches the name of the handler exactly (spaces/capitalization).

Is this action on a button? Or is it a toggle? This is on the checkbox, right? Is toggleSwitch_1 supposed to match the state of this component?

Your action script would need to be changed to something like:

Action Script
def runAction(self, event):
	""" """
	
	# Kept this in because I don't know what ToggleSwitch_1's purpose is
	if not self.props.selected:
		self.getSibling("ToggleSwitch_1").props.selected = False
		# self.props.selected = False #this is already False, why are we setting it False again?

	payload = {
		'selected': self.props.selected,
		'instance': self.view.params.instance
	}

	system.perspective.sendMessage("updateList", payload, scope="page")

	return

Your message handler would then look something like this:

Message Handler Script
def onMessageReceived(self, payload):
	""" """

	if not payload:
		return

	selected = payload['selected']
	instanceIdx = payload['instance']
	bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}

	# If checkbox was deselected, remove the instance from the list
	if not selected and instanceIdx != len(self.custom.instances) - 1:
		self.custom.instances = removeMultiEntryInstance(self.custom.instances, instanceIdx)

	# If the instance is the last in the list, add another instance.
	elif selected and instanceIdx == len(self.custom.instances) - 1:
		instances = shared.util.sanitizeIgnitionObject(self.custom.instances)
		instances.append(bInst)
		instances[-1].update({"instance": len(instances) - 1})
		self.custom.instances = instances

	return
shared.util.sanitizeIgnitionObject
def sanitizeIgnitionObject(element):
	""" """
	if hasattr(element, '__iter__'):
		if hasattr(element, 'keys'):
			return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys())
		else:
			return list(sanitizeIgnitionObject(x) for x in element)
	return element
1 Like

that script is on the checkbox.

It is a toggle that is the control for the output that is selected from a table. It is set to False when the checkbox is unselected. Thinking about it now, it's probably not needed.

I can see arguments for keeping that logic and removing it.

If you do keep it, bidirectionally bind its selected property to a custom property on the root container or view(name it something like enableOutputSelection).

Then you can change the line of code in the action script to self.view.custom.enableOutputSelection = False and it will work regardless of which container that toggle is located in.

Very helpful if you are still designing and moving stuff around.