Table Multi-Select

How can I tell if the user is selecting multiple rows? I have a dynamic property, type string, and want to store the selected row indices, but can’t tell when they are doing the selection. Actually, I can see when they select something by monitoring the selectedColumn property, but I can’t use this when they deselect a row.

So, how can I bind this dynamic property to the getSelectedRows, or trigger an event when the selection changes?

Try something like this:

from fpmi.gui import messageBox table = event.source.rootContainer.getComponent("Table") for row in table.getSelectedRows(): messageBox(table.data.getValueAt(row, 0))
You’ll of course will need to tailor it to your needs.

:blush: Oops, I didn’t read your last line before responding…

I’ll get back to you shortly with a better answer.

Try binding to the property change event and test for event.propertyName == ‘selectedRow’ or event.propertyName == ‘selectedColumn’ and then iterate over the rows with getSelectedRows().

Thanks Mickey. I did some more testing, and it turns out I am getting the columnSelected event when I deselect a row too, so I can somehow make that event work. And, when using multi-select, the selectedRow is actually the lowest row in the selectedRow array. That makes sense.

But upon further testing, I think there may be a bug. If I add the following code to the table:

if event.propertyName == 'selectedRow':
	print 'selectedRow = ', event.source.selectedRow

and then select a row, I get four events triggered. I can handle that I guess, because I bind the selectedRow to a dynamic property elsewhere anyway, so I only see one change even if the event fires four times. As long as I’m not doing anything too intensive in this event (I’m just getting the array and passing it to a dynamic property, so the property should only see one transition)

edit: I must have been doing something wrong before because now it works with event.propertyName == ‘selectedRow’. I think what confused me was that the value for selectedRow doesn’t change (neither in the designer or output console), but there must be something happening behind the scenes. Other than the four events, it looks good now.

Your observations are consistent with mine. As far as I can tell, the propertyChange event gets fired multiple times whenever the table’s selection is changed. I typically see it fire with event.propertyName == ‘selectedRow’ twice and event.propertyName == ‘selectedColumn’ twice. I’m not sure why it generates double the events but we just need to be aware that it does and handle it appropriately.

In my observations, the actual value of event.source.selectedRow and event.source.selectedColumn remain the same if multi-row selection is enabled and a user clicks to select additional rows. What changes though is the array returned by table.getSelectedRows(). To see how the array changes, try the following code:

[code]if event.propertyName == ‘selectedRow’:

global eventCount
global prevSelectedRows

try:
	if eventCount == 0:
		pass
except:
	eventCount = 0

try:
	if prevSelectedRows == None:
		pass
except:
	prevSelectedRows = []

print 'eventCount: %d' % eventCount

if eventCount == 0:
	print 'selectedRow: %d' % event.source.selectedRow
	print 'prevSelectedRows: %s' % prevSelectedRows

	curSelectedRows = event.source.selectedRows
	print 'curSelectedRows: %s' % curSelectedRows

	newSelectedRows = [row for row in curSelectedRows if row not in prevSelectedRows]
	print 'newSelectedRows: %s' % newSelectedRows

	newDeselectedRows = [row for row in prevSelectedRows if row not in curSelectedRows]
	print 'newDeselectedRows: %s' % newDeselectedRows

	prevSelectedRows = curSelectedRows
	eventCount = 1
else:
	eventCount = 0

[/code]
I’m not exactly sure what you need to do but if you need to determine which row(s) is/are selected or deselected, then you’ll need to compare the previous rows selected with the current rows selected.

If the amount of processing you need to do during an event grows, you can use a global variable, as I did above, to limit your processing to a single call.

The try/except clauses initialize eventCount and prevSelectedRows the first time the event is fired.

I just needed to populate a string with a list of the selected rows. Once I found out that the event is fired even though the selectedRow property doesn’t change, it worked fine. I trigger all of my events off the SelRows string property, so even though this event fires four times, I only see one property change on the string, which is what is doing all of the work elsewhere in my code.

if event.propertyName == 'selectedRow':
	rows = table.getSelectedRows()
	event.source.parent.getComponent('Layers').SelRows = ''
	for r in rows:
		event.source.parent.getComponent('Layers').SelRows+= str(r)+','

Just a suggestion but this is a little more concise and pythonesque:

if event.propertyName == 'selectedRow': event.source.parent.getComponent('Layers').SelRows = ','.join(['%s' % row for row in event.source.selectedRows])

Another question- is there a way to enforce a max number of selections? I would like them to be able to select a max of four items. I tried “addSelectionInterval” like I do with lists, but that didn’t work.

Not directly on the table. You could listen for selection changes and modify the selection if it goes over 4, but it will be greater than 4 for a split second. I would implement this restriction in whatever logic comes after the table selection. That is, whenever the selection is more than 4, show some warning and disable whatever it is that this table selection is driving.

I’m clamping the selection for now, so it wouldn’t be a problem to have one extra selected for a split second. How would I modify the selection in multi-selection mode?

Ok, I don’t blame you for not figuring this one out. We’ll have to add a setSelectedRows(int[]) function to the table component. In the meantime, you can achieve this through some crafty Java-through-Jython scripting. This snippet would go in your table’s propertyChange event.

[code]def checkRows(table=event.source):
import fpmi
rows = table.getSelectedRows()
if len(rows)>4:
def resetSelection(table=table.viewport.view, rows=rows[:4]):
table.setRowSelectionInterval(rows[0],rows[0])
for r in rows[1:]:
table.addRowSelectionInterval(r,r)
fpmi.system.invokeLater(resetSelection)

if event.propertyName in (‘selectedRow’, ‘selectedColumn’):
fpmi.system.invokeLater(checkRows)[/code]

The idea is that every time the selection changes, you check to see if there are more than 4 rows selected. If so, you reset the selection to the first 4 rows only.

Actually, I was looking at the java swing jtable component methods last night, and I got setRowSelectionInterval to work fine, but I get a fault on addRowSelectionInterval. This is what I’m doing, which is similar to what you posted:

		table.setRowSelectionInterval(rows[0],rows[0])
		table.addRowSelectionInterval(rows[1],rows[1])
		table.addRowSelectionInterval(rows[2],rows[2])

I inserted print statements to make sure the values are all ints, and they look good. In the table, I have RowSelectionAllowed set to true, and ColumnSelectionAllowed set to false. Is the InvokeLater required in this case?

I get this error:

Traceback (innermost last):
  File "<event:propertyChange>", line 15, in ?
AttributeError: addRowSelectionInterval

	at org.python.core.Py.AttributeError(Py.java)
	at org.python.core.PyInstance.invoke(PyInstance.java)
	at org.python.pycode._pyx43.f$0(<event:propertyChange>:15)
	at org.python.pycode._pyx43.call_function(<event:propertyChange>)
	at org.python.core.PyTableCode.call(PyTableCode.java)
	at org.python.core.PyCode.call(PyCode.java)
	at org.python.core.Py.runCode(Py.java)
	at com.inductiveautomation.factorypmi.application.script.ScriptManager.runCode(ScriptManager.java:245)
	at com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter.runActions(ActionAdapter.java:145)
	at com.inductiveautomation.factorypmi.application.binding.action.ActionAdapter.invoke(ActionAdapter.java:287)
	at com.inductiveautomation.factorypmi.application.binding.action.RelayInvocationHandler.invoke(RelayInvocationHandler.java:57)
	at $Proxy0.propertyChange(Unknown Source)
	at java.beans.PropertyChangeSupport.firePropertyChange(Unknown Source)
	at java.beans.PropertyChangeSupport.firePropertyChange(Unknown Source)
	at java.awt.Component.firePropertyChange(Unknown Source)
	at com.inductiveautomation.factorypmi.application.components.PMITable.access$100(PMITable.java:107)
	at com.inductiveautomation.factorypmi.application.components.PMITable$SelectionRelayListener.valueChanged(PMITable.java:1896)
	at javax.swing.DefaultListSelectionModel.fireValueChanged(Unknown Source)
	<...snip...>

No, you’re not actually dealing with a JTable in your code. Notice the table=table.viewport.view line in my script. The table component itself is actually a JScrollPane at the top level.

Ahh, got it. Thanks for the help.