By using hints from various other posts, I had built utility functions for defining and using a custom editor that can refresh the drop down list using methods defined when setting up the custom editor.
Now I’m enhancing it to allow for multiple selection.
I’m having few problems presently with my implementation:
- Setting the font to the table cell font
- Getting the Popup to STAY visible after selecting one of the choices
- Clearing the edit mode on the Cell if a different component on the screen is selected
The current source is as follows:
# Convenience functions for Ignition development, typically loaded
# as "shared.MWES.Editors.{Cell Editor}"
#
# Copyright 2008-2018 Midwest Engineered Systems, Inc. <sales@mwes.com>
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
#
#
# Custom Cell Editors for use in power tables
#
import system
import re
from java.awt.event import ActionEvent, ActionListener
from javax.swing import JComponent, JComboBox, JCheckBox, JLabel, JList
from javax.swing import DefaultCellEditor, AbstractCellEditor
from javax.swing import DefaultComboBoxModel, ComboBoxModel, ListModel, ListSelectionModel
from javax.swing import ListCellRenderer
from javax.swing.table import TableCellEditor
libName = "MWES"
srcName = "Editors"
class SelectableKeyValueElement:
def __init__(self, data, selected=False):
self.elemKey = ''
self.elemValue = ''
self.elemSelected = selected
if data != None:
if isinstance(data, tuple) or isinstance(data, list):
if len(data) > 0:
self.elemKey = data[0]
if len(data) > 1:
self.elemValue = data[1]
else:
self.elemValue = self.elemKey
else:
self.elemKey = str(data)
self.elemValue = self.elemKey
def key(self, newKey = None):
if newKey == None:
return self.getKey()
else:
return self.setKey(newKey)
def getKey(self):
return self.elemKey
def setKey(self, key):
self.elemKey = key
def value(self, newValue = None):
if newValue == None:
return self.getValue()
else:
return self.setValue(newValue)
def getValue(self):
return self.elemValue
def setValue(self, value):
self.value = elemValue
def isSelected(self):
return self.elemSelected
def setSelected(self, selected):
self.elemSelected = selected
def getElement(self):
return (self.elemKey, self.elemValue, self.elemSelected)
def __str__(self):
return "%s: %s (%s)" % (str(self.elemKey), str(self.elemValue), str(self.elemSelected))
class ComboSelectableKeyValueModel(ComboBoxModel):
def __init__(self, *args, **kwargs ):
self.libName = libName + '.' + srcName
self.srcName = 'ComboSelectableKeyValueModel'
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, method='__init__' )
logger.info("Starting")
idx = 0
sz = len(args)
myData = shared.MWES.Args.getParam( sz, idx, "Data", None, True, args, kwargs )
idx+=1
currValue = shared.MWES.Args.getParam( sz, idx, "CurrentValue", '', False, args, kwargs )
idx+=1
keyCol = shared.MWES.Args.getParam( sz, idx, "KeyColumn", 'key', False, args, kwargs )
idx+=1
valueCol = shared.MWES.Args.getParam( sz, idx, "ValueColumn", 'value', False, args, kwargs )
idx+=1
self.multiSelect = shared.MWES.Args.getParam( sz, idx, "MultiSelect", False, False, args, kwargs )
idx+=1
self.keyMap = { }
self.keyList = []
self.valueMap = { }
self.selectedKeys = [ ]
self.data = myData
self.currValue = currValue
self.keyCol = keyCol
self.valueCol = valueCol
self.refreshItemList()
def isMultiSelect(self):
return self.multiSelect
def setMultiSelect(self, multiSelect):
self.multiSelect = multiSelect
def refreshItemList (self, row=-1):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info("Starting. Data %s, CurrValue %s" % (str(self.data), str(self.currValue)))
myData = self.data
currValue = self.currValue
keyCol = self.keyCol
valueCol = self.valueCol
if callable(myData):
myData = myData(row)
if callable(currValue):
currValue = currValue(row)
self.keyMap = { }
self.keyList = []
self.valueMap = { }
self.selectedKeys = [ ]
if myData != None:
currValues = []
if currValue != None and len(currValue) > 0:
currValues = currValue.split(',')
if isinstance(myData, list):
for item in myData:
self.addElement (SelectableKeyValueElement ( item ), currValues)
elif str(myData)[:9] == 'Dataset [':
logger.info("Processing Dataset")
for row in range(myData.getRowCount()):
try:
key = myData.getValueAt(row, keyCol)
value = myData.getValueAt(row, valueCol)
self.addElement (SelectableKeyValueElement ( (key, value) ), currValues)
except:
pass
def alphaNumOrder(key):
elems = []
keys = re.split(r'(\d+)', key)
for elem in keys:
if elem.isnumeric():
if len(elem) < 6:
s = len(elem)
while s < 6:
elem = '0' + elem
s = s + 1
elems.append(elem)
return ''.join(elems)
self.keyList = sorted(self.keyMap.keys(), key=alphaNumOrder)
logger.info("Finished. ValueMap: %s, KeyMap: %s, KeyList: %s" % (str(self.valueMap), str(self.keyMap), str(self.keyList)))
def addElement(self, kvElem, currValues=[]):
# logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
# logger.info ("Starting, Item = %s, CurrValues = %s" % (str(kvElem), str(currValues)))
key = kvElem.key()
value = kvElem.value()
if key in currValues or value in currValues:
if (self.multiSelect) or (len(self.selectedKeys) == 0):
kvElem.setSelected(True)
self.selectedKeys.append(key)
self.valueMap[value] = kvElem
self.keyMap[key] = kvElem
def getSize(self):
return len(self.keyMap.keys())
def getElementAt(self, index):
# logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
# logger.info ("Starting, Index = %s" % (str(index)))
if index >= 0 and index < len(self.keyList):
return self.keyMap[self.keyList[index]]
else:
return ''
def setSelectedItem(self, anItem, force=None):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting, Item = %s, Force=%s" % (str(anItem), str(force)))
key, kvElem = self.getElementAndKey(anItem)
logger.info ("Starting, Key %s, Item = %s" % (str(key), str(kvElem)))
if kvElem != None:
selected = not kvElem.isSelected()
if force != None and isinstance(force, bool):
selected = force
elif not self.multiSelect:
selected = True
logger.info ("Setting Selected to %s for Item = %s" % (str(selected), str(kvElem)))
kvElem.setSelected ( selected )
if key in self.selectedKeys:
if not kvElem.isSelected():
self.selectedKeys.remove(key)
elif kvElem.isSelected():
if not self.multiSelect and len(self.selectedKeys) > 0:
self.clearAllSelectedKeys()
self.selectedKeys.append(key)
logger.info ("Finish, Key %s, Item = %s, SelectedKeys=%s" % (str(key), str(kvElem), str(self.selectedKeys)))
def clearAllSelectedKeys(self):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting")
for key in self.selectedKeys:
self.keyMap[key].setSelected(False)
self.selectedKeys = []
def getSelectedKeys(self, asList=True):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting, As List = %s" % (str(asList)))
ret = ''
if asList:
ret = self.selectedKeys
else:
ret=''
for key in self.selectedKeys:
if len(ret) > 0:
ret = ret + ','
ret = ret + key
logger.info ("Finish, Key(s) = %s" % (str(ret)))
return ret
def setSelectedKeys(self, indices):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting, Indices = %s" % (str(indices)))
idxList = indices
if not isinstance(idxList, list):
idxList = str(indices).split(',')
for idx in idxList:
self.setSelectedItem(idx, True)
def getSelectedValues(self):
# logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
# logger.info ("Starting")
ret = ''
for key in self.selectedKeys:
if len(ret) > 0:
ret = ret + ', '
ret = ret + self.keyMap[key].value()
# logger.info ("Finish: Ret %s" % (str(ret)))
return ret
def getElementAndKey(self, anItem):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting, Item = %s" % (str(anItem)))
if isinstance(anItem, SelectableKeyValueElement):
key = anItem.key()
else:
key = str(anItem)
kvElem = None
try:
kvElem = self.keyMap[key]
except:
try:
kvElem = self.valueMap[key]
key = kvElem.key()
except:
logger.info("Unable to find '%s' in maps" % (str(key)))
logger.info ("Finished, Key = %s, Elem = %s" % (str(key), str(kvElem)))
return ( key, kvElem )
def isSelected(self, anItem):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info ("Starting, Item = %s" % (str(anItem)))
key, kvElem = self.getElementAndKey(anItem)
if kvElem != None:
return kvElem.isSelected()
else:
return False
class ComboSelectableKeyValueCellRenderer ( JComponent, ListCellRenderer ):
def __init__(self):
self.libName = libName + '.' + srcName
self.srcName = 'ComboKeyValueCellRenderer'
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, method='__init__' )
logger.info("Starting")
def getListCellRendererComponent(self, list, value, index, isSelected, cellHasFocus):
model = list.getModel()
retValue = None
stringValue = ''
if index != -1:
checked = False
multiSelect = False
if isinstance(model, ComboSelectableKeyValueModel):
multiSelect = model.isMultiSelect()
if value != None:
if isinstance(value, SelectableKeyValueElement):
stringValue = value.value()
checked = value.isSelected()
else:
stringValue = str(value)
if checked:
self.setBackground(list.getSelectionBackground())
self.setForeground(list.getSelectionForeground())
else:
self.setBackground(list.getBackground())
self.setForeground(list.getForeground())
if multiSelect:
retValue = JCheckBox(stringValue, checked)
else:
retValue = JLabel(stringValue)
else:
if isinstance(model, ComboSelectableKeyValueModel):
stringValue = model.getSelectedValues()
else:
stringValue = model.getSelectedItem()
if stringValue != None and len(stringValue) > 0:
stringValue = '<HTML>'+stringValue
retValue = JLabel(stringValue)
return retValue
class ComboSelectableKeyValue(JComboBox):
def __init__(self):
self.libName = libName + '.' + srcName
self.srcName = 'ComboSelectableKeyValue'
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, method='__init__' )
logger.info("Starting")
def setPopupVisible ( self, v ):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info("Flag %s, IsShowing? %s" % (str(v), str(self.isShowing())))
# if v == 2:
# JComboBox.setPopupVisible(self, False)
# elif v == 1:
# JComboBox.setPopupVisible(self, True)
# elif v == 0:
# JComboBox.setPopupVisible(self, True)
class SelectableKeyValueCellEditor(DefaultCellEditor):
comboBox = ComboSelectableKeyValue()
def __init__(self, component):
self.libName = libName + '.' + srcName
self.srcName = 'SelectableKeyValueCellEditor'
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, method='__init__' )
logger.info("Starting")
DefaultCellEditor.__init__(self, component)
@staticmethod
def getCellEditorValue():
result = SelectableKeyValueCellEditor.comboBox.getModel().getSelectedKeys(False)
return result
def getTableCellEditorComponent(self, table, value, isSelected, row, column):
logger = shared.MWES.Logging.getLog(lib=self.libName, src=self.srcName, autoMethod=True )
logger.info("Starting, Value %s, IsSelected %s, Row %d, Column %d" % (str(value), str(isSelected), row, column))
comp = DefaultCellEditor.getTableCellEditorComponent(self, table, value, isSelected, row, column)
font = table.getFont()
renderer = comp.getRenderer()
if renderer != None:
logger.info("Setting Renderer Font %s" % (str(font)))
renderer.setFont(font)
comp.setRenderer(renderer)
comp.setFont(font)
if isinstance(comp, ComboSelectableKeyValue):
comp.getModel().refreshItemList(row)
comp.setModel(comp.getModel())
SelectableKeyValueCellEditor.comboBox = comp
return comp
def DynamicDropdownCellEditor(buildOptions, getCurrentValue, keyCol=None, valueCol=None):
"""
This will create a Custom dropdown editor that will retrieve the values for the dropdown list dynamically when the cell is selected.
Arguments:
buildOptions: A function that will return a list of tuples.
The format of the returned list should be:
[ ( Key, Description ), ( Key, Description ) ]
OR with the keyCol and valueCol fields defined, you can also return a Dataset and the extraction of the key,value pairs
will be done by the ComboSelectableKeyValueModel class.
Note that the returned list should NOT contain duplicate descriptions
since internally the methods key off of the DESCRIPTION field NOT the KEY field!!
getCurrentValue: A function that will return the value that should be highlighted when the dropdown appears.
This should return the KEY value not the description value.
Results:
A Custom Editor Class Instance to be returned as the 'editor' map entry from the configureEditor power table extension function
Example:
#
# TypeList is a dataset that gets converted to a list of tuples
#
def configureEditor(self, colIndex, colName):
rootContainer = self.parent.parent
if colName == 'type':
# You can either build a list of tuples
def buildTypeOptionsTupleList(row=None):
options = []
descData = rootContainer.TypeList
for typeRow in range(descData.rowCount):
type = descData.getValueAt(typeRow, "type")
desc = descData.getValueAt(typeRow, "description")
if desc != None and len(desc) > 0:
options.append( ( str(type), str(desc) ) )
return options
# Or just return the Dataset directly, if you passed the keyCol and valueCol fields
def buildTypeOptions(row=None):
return rootContainer.TypeList
def getCurrentTypeValue(row=None):
currType = ''
if row == None and rootContainer.RobotRow >= 0:
row = rootContainer.RobotRow
if row != None and row >= 0:
currType = rootContainer.RobotList.getValueAt(row, 'type')
if currType != None and len(currType) > 0:
currType = str(currType)
else:
currType = None
return currType
customCellEditor = shared.MWES.Editors.DynamicDropdownCellEditor(buildTypeOptions, getCurrentTypeValue)
return {'editor': customCellEditor}
"""
comboBoxModel = ComboSelectableKeyValueModel(buildOptions, getCurrentValue, keyCol, valueCol, False)
comboBox = ComboSelectableKeyValue()
comboBox.setModel(comboBoxModel)
comboBox.setRenderer(ComboSelectableKeyValueCellRenderer())
return SelectableKeyValueCellEditor(comboBox)
def DynamicMultiSelectCheckBoxCellEditor(buildOptions, getCurrentValue, keyCol=None, valueCol=None):
"""
This will create a Custom dropdown editor that will retrieve the values for the dropdown list dynamically when the cell is selected.
Arguments:
buildOptions: A function that will return a list of tuples.
The format of the returned list should be:
[ ( Key, Description ), ( Key, Description ) ]
OR with the keyCol and valueCol fields defined, you can also return a Dataset and the extraction of the key,value pairs
will be done by the ComboSelectableKeyValueModel class.
Note that the returned list should NOT contain duplicate descriptions
since internally the methods key off of the DESCRIPTION field NOT the KEY field!!
getCurrentValue: A function that will return the value that should be highlighted when the dropdown appears.
This should return the KEY value not the description value.
Results:
A Custom Editor Class Instance to be returned as the 'editor' map entry from the configureEditor power table extension function
Example:
#
# TypeList is a dataset that gets converted to a list of tuples
#
def configureEditor(self, colIndex, colName):
rootContainer = self.parent.parent
if colName == 'type':
# You can either build a list of tuples
def buildTypeOptionsTupleList(row=None):
options = []
descData = rootContainer.TypeList
for typeRow in range(descData.rowCount):
type = descData.getValueAt(typeRow, "type")
desc = descData.getValueAt(typeRow, "description")
if desc != None and len(desc) > 0:
options.append( ( str(type), str(desc) ) )
return options
# Or just return the Dataset directly, if you passed the keyCol and valueCol fields
def buildTypeOptions(row=None):
return rootContainer.TypeList
def getCurrentTypeValue(row=None):
currType = ''
if row == None and rootContainer.RobotRow >= 0:
row = rootContainer.RobotRow
if row != None and row >= 0:
currType = rootContainer.RobotList.getValueAt(row, 'type')
if currType != None and len(currType) > 0:
currType = str(currType)
else:
currType = None
return currType
customCellEditor = shared.MWES.Editors.DynamicDropdownCellEditor(buildTypeOptions, getCurrentTypeValue)
return {'editor': customCellEditor}
"""
comboBoxModel = ComboSelectableKeyValueModel(buildOptions, getCurrentValue, keyCol, valueCol, True)
comboBox = ComboSelectableKeyValue()
comboBox.setModel(comboBoxModel)
comboBox.setRenderer(ComboSelectableKeyValueCellRenderer())
return SelectableKeyValueCellEditor(comboBox)