Power Table - Dropdown Configuration

I started going down a route of adding a PopupMenuListener in initialize() and had something close to working... Then hit the wrong button in the IDE and lost it all. So you'll have to go with Justin's approach :slight_smile:

I was contemplating something like this as well -- reworking the script for the listener's popup will become visible function. I suspect that's where the pesky scrollable viewport max width is being written from.

1 Like

What do you mean by this?

I hate to say this, but I was clicking around on the dropdown contemplating ways to depict selection [see post 24] when I observed a pretty significant bug in the onMouseClick extension function script from post 6, and I went down the rabbit hole again to fix it.

The problem was that if the user clicked on the same cell three times, or if the user clicked on something outside of the table, and then, clicked on the same cell, the scrollpane would revert back to its smaller size. This is because the cell maintains focus, and consequently, the extension function doesn't fire during the subsequent clicks.

To fix this, I ended up reworking the script for a popup menu listener, and putting the listener directly on the JComboBox itself from the configureEditor extension function. I do believe that this script is a finished product as far as controlling the dropdown width goes. I put a pretty good load on the table, and clicked it relentlessly, and so far, I haven't been able to get this to mess up.

Here is the result:

Here is the updated script:
#def configureEditor(self, colIndex, colName):
	from javax.swing import JComboBox, JScrollPane
	from java.awt import Dimension
	from java.awt.font import FontRenderContext, TextLayout
	from java.awt import Font
	from javax.swing.event import PopupMenuListener
	class DropdownListener(PopupMenuListener):
		def popupMenuWillBecomeVisible(self, event):
			def setSizes(dropdown):
				dropdown.setMaximumSize(Dimension(2*dropdownWidth, 2*dropdown.getPreferredSize().height))
				dropdown.setMinimumSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
				dropdown.setPreferredSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
			comboBox = event.source
			dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
			dropdownFont = dropdown.font
			items = [
				comboBox.renderer.getListCellRendererComponent(dropdown.getList(), comboBox.model.getElementAt(row), -1, False, False).getText()
				for row in xrange(comboBox.model.size)
			]
			widestItem = max(items, key=len)
			dropdownWidth = int(TextLayout('&'*len(widestItem), dropdownFont, FontRenderContext(None, False, False)).bounds.width)
			setSizes(dropdown)
			defaultScrollpane = next((component for component in dropdown.getComponents() if isinstance(component, JScrollPane)), None)
			viewport = defaultScrollpane.viewport
			dropdown.remove(defaultScrollpane)
			replacementScrollPane = JScrollPane()
			replacementScrollPane.setViewport(viewport)
			dropdown.add(replacementScrollPane)
			setSizes(replacementScrollPane)
		def popupMenuWillBecomeInvisible(self, event):
			pass
		def popupMenuCanceled(self, event):
			pass
	def setListener():
		if self.data.rowCount > 0 and hasattr(self.table.getCellEditor(0, colIndex), 'component'):
			editorComponent = self.table.getCellEditor(0, colIndex).component
			if isinstance(editorComponent, JComboBox):
				for listener in editorComponent.popupMenuListeners:
					editorComponent.removePopupMenuListener(listener)
				editorComponent.addPopupMenuListener(DropdownListener())
	system.util.invokeLater(setListener)

Note: To use this, it should be placed at the top of the configureEditor extension function before any return statements. The original onMouseClick script should be deleted and not used.

In case it is useful for clarification or understanding, here is the test window I used to develop this:
testWindow.zip (29.5 KB)

6 Likes

This works wonderfully, thank you! I so appreciate you putting in all the time and effort to build this out.

A quick note regarding some of the odd behavior I was seeing:

I used system.dataset.clearDataset to retain headers and delete the table data before saving the window. Starting the window in a client or designer testing in this 'clearDataset' state would cause the dropdown box to fail to render properly. Using your example of setting the table data to = None appears to entirely resolve this behavior.

To be clear, I take it there was no good way to pass the current cell value to the dropdown?

Thank you, again. I would have never got here on my own.

Of course you can. What are you wanting the dropdown to do with it?

It seems like clicking a cell in the "string column" of your example table will open the dropdown window with the first value selected. If I click on a second cell in that same column, it changes the value of the cell to the first value of the dropdown without ever opening the dropdown. I need to follow up with a second click in order to prompt the dropdown to open, and the value of the cell has already been changed.

Changing the "edit Click Count" to 2 is a workaround with the existing code.

I just wanted it to open and display the current value of the cell, rather than the first value in the list, that way the user can still see the current cell value after opening the dropdown.

I see. You have exceptionally long dropdown lists, and you are wanting to manipulate the scroll index, so that when the dropdown opens, the currently selected value is in the visible rows no matter how far down it is in the list.

1 Like

This is certainly doable, and seems like a fun challenge, but I won't have a day off to mess with this until this weekend. That said, you have enough information here that I imagine that you can get this done on your own before then. The above script is already getting the scroll pane from the dropdown as well as the list renderer, so it's not that big of a jump to get the vertical scroll bar and set its value to a list position. There are numerous examples of scroll position manipulation in the forum. As far as setting the item selected, that should be doable too.

Here are a few related examples from the forum to get you started:
Example of scroll pane and scroll bar manipulation
Another example of scroll position maniptulation
An example of dropdown rendering manipulation

When you get this working, report back. We would love to see what you come up for this.

4 Likes

Thank you for pointing me in the right direction; I owe you several beers. I'll give it a shot and report back. :slight_smile:

1 Like

Truthfully, taking shots in the dark here; I feel like I'm very clearly out of my depth on this one. Between trying to follow other examples on the forums and snippets from stack, this is the closest I've come to a working example.

  • Cannot find a means to pass the selected value to the listener (currently hard coded for testing)
  • Throws an error the first time any column is selected
    • Exception in thread "AWT-EventQueue-0" java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location

I feel like I'm way off of the right way to go about it, but honestly I'm kind of lost at this point. I will circle back later this week and see if I can make any progress.

Script thus far
from javax.swing import JComboBox, JScrollPane
from java.awt import Dimension
from java.awt.font import FontRenderContext, TextLayout
from java.awt import Font
from javax.swing.event import PopupMenuListener
class DropdownListener(PopupMenuListener):
	def popupMenuWillBecomeVisible(self, event):
		def setSizes(dropdown):
			dropdown.setMaximumSize(Dimension(2*dropdownWidth, 2*dropdown.getPreferredSize().height))
			dropdown.setMinimumSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
			dropdown.setPreferredSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
		comboBox = event.source
		"""
		1 line added
		set the currently selected value in box
		Need a means to make this dynamic
		"""			
		comboBox.getModel().setSelectedItem("0 - no operation")
		dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
		dropdownFont = dropdown.font
		"""
		3 lines added
		1 line edited
		Manipulate list by setting index before adding to combox box
		Need a means to make this dynamic
		"""
		dropdownList = dropdown.getList()
		dropdownList.setSelectedIndex(100)
		dropdownList.ensureIndexIsVisible(dropdownList.getSelectedIndex())
		"""
		replaced dropdown.getList with dropdownList
		"""
		items = [
			comboBox.renderer.getListCellRendererComponent(dropdownList, comboBox.model.getElementAt(row), -1, False, False).getText()
			for row in xrange(comboBox.model.size)
		widestItem = max(items, key=len)
		dropdownWidth = int(TextLayout('&'*len(widestItem), dropdownFont, FontRenderContext(None, False, False)).bounds.width)
		setSizes(dropdown)
		defaultScrollpane = next((component for component in dropdown.getComponents() if isinstance(component, JScrollPane)), None)
		viewport = defaultScrollpane.viewport
		dropdown.remove(defaultScrollpane)
		replacementScrollPane = JScrollPane()
		replacementScrollPane.setViewport(viewport)
		dropdown.add(replacementScrollPane)
		setSizes(replacementScrollPane)
	def popupMenuWillBecomeInvisible(self, event):
		pass
	def popupMenuCanceled(self, event):
		pass
def setListener():
	if self.data.rowCount > 0 and hasattr(self.table.getCellEditor(0, colIndex), 'component'):
		editorComponent = self.table.getCellEditor(0, colIndex).component
		if isinstance(editorComponent, JComboBox):
			for listener in editorComponent.popupMenuListeners:
				editorComponent.removePopupMenuListener(listener)
			editorComponent.addPopupMenuListener(DropdownListener())
system.util.invokeLater(setListener)

#create dropdown list
#create options list
dropdownList = []
for row in system.dataset.toPyDataSet(self.recipeSteps):
	#remove line breaks
	description = row[1].replace('\r', '')
	description = description.replace('\n', '')
	#create tuple
	dropdownTuple = (int(row[0]), description)
	#append to list
	dropdownList.append(dropdownTuple)
	
colList = ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6', 'Step 7', 'Step 8', 'Step 9', 'Step 10']
if colName in colList:
	return {'options': dropdownList}

You should be able to just get it using the selected row and column

I imagine that the error is occurring because the event is "will become", so the selected value hasn't been determined when the event initializes. I would probably not attack this from the main body of the listener. Instead, I would set up a separate function for this, and to get around the fact that the dropdown hasn't loaded yet, I would use invoke later.

Example:

#[...]
	class DropdownListener(PopupMenuListener):
		def popupMenuWillBecomeVisible(self, event):
			def setSizes(dropdown):
				dropdown.setMaximumSize(Dimension(2*dropdownWidth, 2*dropdown.getPreferredSize().height))
				dropdown.setMinimumSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
				dropdown.setPreferredSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
			#======ADD THIS: =======
			def setScrollPanePosition():
				table = event.source.parent
				selectedValue = table.getValueAt(table.selectedRow, table.selectedColumn)
				#do work with the selected value here
			system.util.invokeLater(setScrollPanePosition)
			#=====================
			comboBox = event.source
#[...]

I don't believe this isn't going to work with a (value, label) context because "0 - no operation" is going to be the label part of that, and setSelecteItem is going to try and set the value. You will probably need to iterate through and find the value that corresponds to that label, and while you are doing that, it would be a good time to find the index the item lives at for purposes of calculating scroll position.

I also imagine that you will not want to set this value directly, but rather, I imagine that you will want to get the renderer and use it to indicate selected.

Example:

def setScrollPanePosition():
	comboBox = event.source
	dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
	table = comboBox.parent
	renderer = comboBox.renderer
	defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
	selectedLabel = table.getValueAt(table.selectedRow, table.selectedColumn)
	scrollPosition = 0
	for index in range(comboBox.itemCount):
		label = renderer.getListCellRendererComponent(dropdown.getList(), comboBox.getItemAt(index), index, False, False).getText()
		if selectedLabel == label:
			scrollPosition = (defaultCellHeight * index)
			break
1 Like

You're far too generous with your time.

table = event.source.parent

I was using the property reference tool and getting "self", which clearly is not the table in this instance (doh!). I managed to get the viewport / scroll worked out between your existing code and Nick's scroll scripting, and it works great.

Regarding the selection indication, I've yet to get it working using the renderer. So far I have retrieved the renderer, updated the background, and attempted to update the renderer, but to no avail. I'm certain I'm doing something wrong (see below).

I know very, very little about how it works. However, current behavior of the dropdown is it updates the cell value to index 0 on popup open, which means if the operator clicks away without making a selection, they have accidentally changed the value of the cell. Using the comboBox.getModel().setSelectedItem(label) method has (thus far) kept my cell value consistent when opening a popup, which is very desirable for me. I tried implementing this method in the setScrollPanePosition and the popupMenuWillBecomeBisible functions, but every time I do it breaks the scrolling script above or throws errors..

Finally, I have to reiterate my appreciation; I can write down what I think I want to happen all day, but am years of working with java from being able to implement this on my own. Thank you.

Updated Script

def setScrollPanePosition():
	comboBox = event.source
	dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
	table = comboBox.parent
	renderer = comboBox.renderer
	defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
	#strip decimal and convert to string to compare to label
	selectedLabel = str(int(table.getValueAt(table.selectedRow, table.selectedColumn)))
	scrollPosition = 0
	for index in range(comboBox.itemCount):
		label = renderer.getListCellRendererComponent(dropdown.getList(), comboBox.getItemAt(index), index, False, False).getText()
		if selectedLabel == label.split(" - ")[0]:
			scrollPosition = (defaultCellHeight * (index))
			break
				
	#modify viewport to display current value
	scrollpane = next((component for component in dropdown.getComponents() if isinstance(component, JScrollPane)), None)
	viewport = defaultScrollpane.viewport
	rect = viewport.getVisibleRect()
	rect.y = scrollPosition
	viewport.scrollRectToVisible(rect)
				
	#show current value as selected
#	component = renderer.getListCellRendererComponent(dropdown.getList(), label, index, True, True)
#	component.background = event.source.parent.selectionBackground
#	renderer = component
#	comboBox.setRenderer(renderer)

	##alternate##
#	#comboBox.getModel().setSelectedItem(label)

system.util.invokeLater(setScrollPanePosition)

This is probably not correct, but it's also my fault. In my example:

I had assumed that you were displaying the label in the table because that's my inclination, but it's clear from your code that you are actually displaying the value.

As a consequence of my false assumption, I have you translating the selectedValue to a string, translating the value of the dropdown to a label, and finally, parsing the label into a string version of the value for purposes of comparison.

In your usage case, the correct approach is probably more like:

#it is possible that based on your statement that there is a decimal that the selectedValue will need to be cast to a float instead of an int
#selectedValue = float(table.getValueAt(table.selectedRow, table.selectedColumn))
selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
scrollPosition = 0
for index in range(comboBox.itemCount):
	if selectedValue == comboBox.getItemAt(index):
		scrollPosition = (defaultCellHeight * index)
		break

Furthermore, if your value follows a zero indexed consecutive pattern [0, 1, 2, 3], you could skip the iterations all together:

defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
scrollPosition = selectedValue * defaultCellHeight

#==========================================
#==========================================

While I have no doubt that this works, it also seems more complex than it needs to be. I would add Point to your java.awt import:

from java.awt import Font, Point

Then you can set your viewport position directly:

scrollPane.viewport.viewPosition = Point(0, scrollPosition)

Also, we have already assigned the scrollpane, comboBox, and dropdown to variables earlier in the script, so you don't have to do it again. I would rewrite that part of the script to look like this:

			def setScrollPanePosition():
				defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
				selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
				scrollPosition = selectedValue * defaultCellHeight
				replacementScrollPane.viewport.viewPosition = Point(0, scrollPosition)
			system.util.invokeLater(setScrollPanePosition)
			comboBox = event.source
			table = event.source.parent
			renderer = comboBox.renderer
			dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)

#==========================================
#==========================================
Back to the topic of comboBox.model.selectedItem = selectedValue
If you could find a way to make this work from the popupMenuWillBecomeVisible event that isn't buggy, that would save a lot of time because the scroll bar positioning would just happen, but I don't know how to do it.
The problem that really needs fixed is that the onCellEdited event is writing the wrong value back if the user doesn't select a value after opening the popup. This is because popup isn't losing its last selected value the next time it is rendered. I feel like this needs to be handled with the popupMenuWillBecomeInvisible event.

Change that from pass to:

def popupMenuWillBecomeInvisible(self, event):
	event.source.setSelectedItem(None)

Then, in your onCellEdited event handler:

#def onCellEdited(self, [...], newValue):
	if newValue != None:
		self.data = system.dataset.setValue(self.data, rowIndex, colIndex, newValue)

In this way the value of the comboBox will start as None, and if the value hasn't changed, it won't be written back.

Putting it all together, the script should look like this:

Full Script
def configureEditor(self, colIndex, colName):
	from javax.swing import JComboBox, JScrollPane
	from java.awt import Dimension
	from java.awt.font import FontRenderContext, TextLayout
	from java.awt import Font, Point
	from javax.swing.event import PopupMenuListener
	class DropdownListener(PopupMenuListener):
		def popupMenuWillBecomeVisible(self, event):
			def setSizes(dropdown):
				dropdown.setMaximumSize(Dimension(2*dropdownWidth, 2*dropdown.getPreferredSize().height))
				dropdown.setMinimumSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
				dropdown.setPreferredSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
			def setScrollPanePosition():
				defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
				selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
				scrollPosition = selectedValue * defaultCellHeight
				replacementScrollPane.viewport.viewPosition = Point(0, scrollPosition)
			system.util.invokeLater(setScrollPanePosition)
			comboBox = event.source
			table = event.source.parent
			renderer = comboBox.renderer
			dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
			dropdownFont = dropdown.font
			items = [
				comboBox.renderer.getListCellRendererComponent(dropdown.getList(), comboBox.model.getElementAt(row), -1, False, False).getText()
				for row in xrange(comboBox.model.size)
			]
			widestItem = max(items, key=len)
			dropdownWidth = int(TextLayout('&'*len(widestItem), dropdownFont, FontRenderContext(None, False, False)).bounds.width)
			setSizes(dropdown)
			defaultScrollpane = next((component for component in dropdown.getComponents() if isinstance(component, JScrollPane)), None)
			viewport = defaultScrollpane.viewport
			dropdown.remove(defaultScrollpane)
			replacementScrollPane = JScrollPane()
			replacementScrollPane.setViewport(viewport)
			dropdown.add(replacementScrollPane)
			setSizes(replacementScrollPane)
		def popupMenuWillBecomeInvisible(self, event):
			event.source.setSelectedItem(None)
		def popupMenuCanceled(self, event):
			pass
	def setListener():
		if self.data.rowCount > 0 and hasattr(self.table.getCellEditor(0, colIndex), 'component'):
			editorComponent = self.table.getCellEditor(0, colIndex).component
			if isinstance(editorComponent, JComboBox):
				for listener in editorComponent.popupMenuListeners:
					editorComponent.removePopupMenuListener(listener)
				editorComponent.addPopupMenuListener(DropdownListener())
	system.util.invokeLater(setListener)
4 Likes

Oh man...

Thank you for stepping through this one bit at a time; it helped my comprehension and I really appreciate it. What scripting I do is generally limited to component level and DB, so getting into the java nuts and bolts has been a great (if not overwhelming) experience.

You nailed it on all accounts. I did have to keep the loop, as the values are not sequential. I also had to add a try for rows with no value, in the event of adding a new row to the recipe.

For anyone curious, I've attached a cleaned up and reduced version of my chart.

Thank you, again, for all of your efforts and your patience with me. I seriously could have not done this without you.

Script
def configureEditor(self, colIndex, colName):
	from javax.swing import JComboBox, JScrollPane
	from java.awt import Dimension
	from java.awt.font import FontRenderContext, TextLayout
	from java.awt import Font, Point
	from javax.swing.event import PopupMenuListener
	class DropdownListener(PopupMenuListener):
		def popupMenuWillBecomeVisible(self, event):
			def setSizes(dropdown):
				dropdown.setMaximumSize(Dimension(2*dropdownWidth, 2*dropdown.getPreferredSize().height))
				dropdown.setMinimumSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
				dropdown.setPreferredSize(Dimension(dropdownWidth, dropdown.getPreferredSize().height))
			def setScrollPanePosition():
				defaultCellHeight = renderer.getListCellRendererComponent(dropdown.getList(), None, 0, False, False).getPreferredSize().height
			#if sequential indexed values, can use this instead of loop
#				selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
#				scrollPosition = selectedValue * defaultCellHeight
			#non-sequential list
				#try necessary for adding new rows with no data
				try:
					selectedValue = int(table.getValueAt(table.selectedRow, table.selectedColumn))
				except:
					return
				scrollPosition = 0
				for index in range(comboBox.itemCount):
					if selectedValue == comboBox.getItemAt(index):
						scrollPosition = (defaultCellHeight * index)
						break		
				#modify viewport to display current value
				replacementScrollPane.viewport.viewPosition = Point(0, scrollPosition)
			system.util.invokeLater(setScrollPanePosition)
			comboBox = event.source
			table = event.source.parent
			renderer = comboBox.renderer
			dropdown = comboBox.getUI().getAccessibleChild(comboBox, 0)
			dropdownFont = dropdown.font
			items = [
				comboBox.renderer.getListCellRendererComponent(dropdown.getList(), comboBox.model.getElementAt(row), -1, False, False).getText()
				for row in xrange(comboBox.model.size)
			]
			widestItem = max(items, key=len)
			dropdownWidth = int(TextLayout('&'*len(widestItem), dropdownFont, FontRenderContext(None, False, False)).bounds.width)
			setSizes(dropdown)
			defaultScrollpane = next((component for component in dropdown.getComponents() if isinstance(component, JScrollPane)), None)
			viewport = defaultScrollpane.viewport
			dropdown.remove(defaultScrollpane)
			replacementScrollPane = JScrollPane()
			replacementScrollPane.setViewport(viewport)
			dropdown.add(replacementScrollPane)
			setSizes(replacementScrollPane)
		def popupMenuWillBecomeInvisible(self, event):
			event.source.setSelectedItem(None)
		def popupMenuCanceled(self, event):
			pass
	def setListener():
		if self.data.rowCount > 0 and hasattr(self.table.getCellEditor(0, colIndex), 'component'):
			editorComponent = self.table.getCellEditor(0, colIndex).component
			if isinstance(editorComponent, JComboBox):
				for listener in editorComponent.popupMenuListeners:
					editorComponent.removePopupMenuListener(listener)
				editorComponent.addPopupMenuListener(DropdownListener())
	system.util.invokeLater(setListener)

	#create dropdown list
	#create options list
	dropdownList = []
	for row in system.dataset.toPyDataSet(self.recipeSteps):
		#remove line breaks
		description = row[1].replace('\r', '')
		description = description.replace('\n', '')
		#create tuple
		dropdownTuple = (int(row[0]), description)
		#append to list
		dropdownList.append(dropdownTuple)
		
	colList = ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5', 'Step 6', 'Step 7', 'Step 8', 'Step 9', 'Step 10']
	if colName in colList:
		return {'options': dropdownList}

Export_2023-06-15_1044.zip (21.2 KB)

1 Like