Popup Menu Style Changes in Vision

I am working on a template that brings up a popup menu. One of the features I am trying to implement is a light/dark mode toggle for the appearance of the popup menu.

Initially, using the system.gui.createPopupMenu() and its built-in method of providing 2 lists, I was not able to change the background/foreground on the list items.

I found some other forum posts that showed the solution to this was to create an empty popup menu -- system.gui.createPopupMenu(, ) -- and then creating an individual JMenuItem for each list item. The JMenuItem can have its background/foreground changed and then it can be added to the popup menu using the .add() method.

This solution works great for a menu with only one level, but I am trying to create a multi-level menu. I was able to implement a solution where each time an item needs to expand to another level, instead of adding a JMenuItem to the list, I can add a JMenu to the list. The JMenu allows me to add JMenuItems to it, and then add the JMenu into the parent menu.

My issue is that the JMenuItems allow me to modify their background/foreground colors, but the JMenu (which extends the JMenuItem class) does not allow me to modify its background. This means whenever I change between dark/light all the menu items change properly except wherever there is a submenu available.

Is there some other way to modify that background color of the JMenu class, or is there something with Ignition that is prohibiting me from modifying it?

4 Likes

My instinct is that this could be simplified by creating the list in the normal way, and then using a recursive function to color the internal items in the list:

# Recursively sets the background and foreground colors of all the sub components of a popup menu
def setMenuColor(menu, background, foreground):
	menu.background = background
	menu.foreground = foreground
	for component in menu.components:
		setMenuColor(component, background, foreground)

My guess is that the selected property probably needs to be True for the background property to be used. There is probably a way to hack the UI and circumvent this, but I don't usually go there.

In my own experimentation using documented example #4, I ran into another issue with the BasicMenuUI mouse listener that causes the background color to revert to white any time the mouse exits.

Example:

# Recursively sets the background and foreground colors of all the sub components of a popup menu
def setMenuColor(menu, background, foreground):
	menu.background = background
	menu.foreground = foreground
	for component in menu.components:
		setMenuColor(component, background, foreground)

		# If a submenu is found, color its popup and set it selected property to true
		if 'JySubMenu' in component.__class__.__name__:
			setMenuColor(component.popupMenu, background, foreground)
			component.selected = True


# Simple nested popup example based on documented example # 4:
def sayHello(event):
	print 'Hello'
subMenu01 = [["Click Me 1", "Click Me 2"], [sayHello, sayHello]]
subMenu02 = [["Click Me 3", "Click Me 4"], [sayHello, sayHello]]
subMenu03 = [["Click Me 5", "Click Me 6"], [sayHello, sayHello]]
subMenu04 = [["Click Me 7", "Click Me 8"], [sayHello, sayHello]]
subMenu05 = [["Click Me 9", "Click Me 10"], [sayHello, sayHello]]
menu = system.gui.createPopupMenu(['Click Me', 'subMenu01', 'subMenu02', 'subMenu03', 'subMenu04', 'subMenu05'], [sayHello, subMenu01, subMenu02, subMenu03, subMenu04, subMenu05])
setMenuColor(menu, system.gui.color('black'), system.gui.color('white'))
menu.show(event)

Result:
image

Unfortunately, because of the listener action, the appearance falls apart as the mouse moves through the submenus. I could probably remove the listener, and replace it with my own, but I imagine that would get convoluted. At this point, I believe the real question is how to set the not selected color of a JySubMenu.

1 Like

I implemented the setMenuColor method you posted here, and I am seeing the same thing that you described.

I agree, it seems we need a way to set the color while the menu item is not selected.

This would be a bit of a hacky way to do this, but I'm wondering if I could set up a second listener of some sort that checks if a menu item becomes not selected and then reset it to selected again to maintain the style.

Unless you somehow remove the original, it might result in a race condition. (I don’t know exactly how the swing implementation works under the hood).

I think ultimately you’re fighting with the look and feel here, and since V8 uses an IA modified look and feel using the UI manager isn’t always possible.

2 Likes

I found a webpage a while ago that had a list of all the properties that can actually be changed with UI Manager for different Java components. The background and foreground were not listed for the JMenuItem. This may be a situation where Ignition has this arbitrary L&F property locked down.

I doubt its anything like that. If there is such a property, it won't be called background. This is an interesting problem, so if nobody finds a solution, I'll be looking at it again when I have time.

1 Like

A post of interest, the whole thread is pertinent, but specifically:

2 Likes

I think this post is where I found the list of properties that UIManager works with. I couldn't find it again, but I'll need to give it another review.

Edit: I should probably take the time to reiterate this:

This approach can have unintended side effects.

I was able to wrap the original listener with my own and achieve the desired effect.
Example:

# Using the mouseReleased event handler

# Create a listner that reapplies the selected property of all JySubMenu items
# ...after the BasicMenuUI listener has done all of its required work
from java.awt.event import MouseAdapter
class NonDeselectingListener(MouseAdapter):
	def __init__(self, originalListener):
		self.originalListener = originalListener
	
	def mouseEntered(self, event):
		self.originalListener.mouseEntered(event)
		self.restoreSelections(event)
	    
	def mouseExited(self, event):
		self.originalListener.mouseExited(event)
	
	def mouseReleased(self, event):
		self.originalListener.mouseReleased(event)
		
	def restoreSelections(self, event):
		for component in event.source.parent.components:
			if 'JySubMenu' in component.__class__.__name__:
				component.selected = True

# Recursively sets the background and foreground colors of all the sub components of a popup menu
def setMenuColor(menu, background, foreground):
	menu.background = background
	menu.foreground = foreground
	for component in menu.components:
		setMenuColor(component, background, foreground)

		# If a submenu is found, color its popup and set it selected property to true
		if 'JySubMenu' in component.__class__.__name__:
			setMenuColor(component.popupMenu, background, foreground)
			component.selected = True
		
		# Locate the Basic UI listener of the menu item
		# ...and wrap in a listener that listener that will restore the selected coloring
		# ...before the component is repainted
		for listener in component.mouseListeners:
			if 'BasicMenuUI' in listener.__class__.__name__ or 'BasicMenuItemUI' in listener.__class__.__name__:
				component.removeMouseListener(listener)
				component.addMouseListener(NonDeselectingListener(listener))
	
# Simple nested popup example based on documented example # 4:
def sayHello(event):
	print 'Hello'
subMenu01 = [["Click Me 1", "Click Me 2"], [sayHello, sayHello]]
subMenu02 = [["Click Me 3", "Click Me 4"], [sayHello, sayHello]]
subMenu03 = [["Click Me 5", "Click Me 6"], [sayHello, sayHello]]
subMenu04 = [["Click Me 7", "Click Me 8"], [sayHello, sayHello]]
subMenu05 = [["Click Me 9", "Click Me 10"], [sayHello, sayHello]]
menu = system.gui.createPopupMenu(['Click Me', 'subMenu01', 'subMenu02', 'subMenu03', 'subMenu04', 'subMenu05'], [sayHello, subMenu01, subMenu02, subMenu03, subMenu04, subMenu05])
setMenuColor(menu, system.gui.color('black'), system.gui.color('white'))
menu.show(event)

Result:
MenuHack

Edit: Amended example per @Joshua_Martin's discovery below, and removed restoreSelections from all but the mouse endered event [that's the only place it's needed].

Justin,

This is fantastic code! I was trying to experiment with removing/adding custom listeners earlier, but I was having a lot of trouble getting anywhere.

I had to add one thing to the listener class you created in order to get the functions of the list items to work. Otherwise they would not actually do anything and the menu would remain open after clicking an item.

class NonDeselectingListener(MouseAdapter):
	def __init__(self, originalListener):
		self.originalListener = originalListener
	
	def mouseEntered(self, event):
		self.originalListener.mouseEntered(event)
		self.restoreSelections(event)
	    
	def mouseExited(self, event):
		self.originalListener.mouseExited(event)
		self.restoreSelections(event)
	
	def mouseReleased(self, event):
		self.originalListener.mouseReleased(event)
		self.restoreSelections(event)
		
	def restoreSelections(self, event):
		for component in event.source.parent.components:
			if 'JySubMenu' in component.__class__.__name__:
				component.selected = True

The only remaining item that I want to try to tackle with this is figuring something out for the selected highlight to be a little less obvious on the expandable menu items.

1 Like

That makes sense; I amended my example to reflect this change, and further experimentation revealed that it is the mouseEntered event that triggers the deselection, so that is the only place where the restoreSelections function is needed.

Edit: One other thing of note that we overlooked is keyboard functionality. Pressing up and down on the keys to navigate the menu items causes the same problem that the mouse entered event caused. The simplest way to override this is just to set the menu's focusable property to False:

# Disable key events [KeyboardFocusManager]
menu.focusable = False
menu.show(event)

I had another idea on this, since I imagine that having the mouseover selection highlighted would look cool. Start by defining the highlight, foreground, and background colors when initially configuring the popup menu:

# Define the color parameters initially in some way
highlightColor = system.gui.color('red')
backgroundColor = system.gui.color('black')
foregroundColor = system.gui.color('white')
#subMenu01 = # [...] etc. etc.
#menu = system.gui.createPopupMenu([...])
setMenuColor(menu, backgroundColor, foregroundColor)
menu.show(event)

Then, the colors can be used in the listener like so:

	def mouseEntered(self, event):
		self.originalListener.mouseEntered(event)
		self.restoreSelections(event)
		event.source.background = highlightColor
	    
	def mouseExited(self, event):
		self.originalListener.mouseExited(event)
		event.source.background = backgroundColor

Result:
highlightedMenus

1 Like

This highlight functionality works great. I think I will mark the solution a couple replies up since it this has definitely accomplished what I was trying to do.

I can't tell if it's the same on the clip you just posted, but there is one smaller thing I am going to try to figure out. It's not very obvious with the black menu background, but we are going to be dynamically toggling between light and dark. There seems to be a slight color adjustment on the menu items that have a submenu since they are always selected. In 'light mode' the effect is much more obvious:
image
I tried changing a few different properties in the setMenuColor method, but nothing seems to have an effect.

1 Like

Perhaps just leave it natural in light mode, and only apply the hocus pocus when dark mode is selected.

1 Like

Great suggestion. Got too deep in the weeds. Gotta use the KISS method

1 Like

Just for fun, I went ahead and worked out what the color adjustment is for the selected sub menus. In all cases, the red value is reduced to 84% of the initial value; the green value is reduced to 93% of its initial value, and the blue value isn't changed at all. This effect can't be seen in my original example because I was using pure black, so the RGB values were all 0.

To eliminate the shading, the menu item and submenu background colors have to be defined separately:

# Initial RGB values for the background color
# [MUST BE values between 0 and 255]
red = 255
green = 255
blue = 255

# Define the sub menu background color using the initial RGB values,
# ...and adjust the menuItem background color to match what the submenu background will be after selected is set to True
subMenuBGColor = system.gui.color(red, green, blue)
menuItemBGColor = system.gui.color(int(red * 0.84), int(green * 0.93), blue)

Then, distinguish the two in the mouse adapter:

	def mouseExited(self, event):
		self.originalListener.mouseExited(event)
		if 'JySubMenu' in event.source.__class__.__name__:
			event.source.background = subMenuBGColor
		else:
			event.source.background = menuItemBGColor

	# [...]

	def restoreSelections(self, event):
		for component in event.source.parent.components:
			if 'JySubMenu' in component.__class__.__name__:
				component.selected = True
			else:
				component.background = menuItemBGColor

...and in the setMenuColor function:

		if 'JySubMenu' in component.__class__.__name__:
			setMenuColor(component, subMenuBGColor, foregroundColor)
			setMenuColor(component.popupMenu, subMenuBGColor, foregroundColor)
			component.selected = True
		else:
			setMenuColor(component, menuItemBGColor, foregroundColor)

Here is the updated code example with the changes applied:

Complete Code Example
# Using the mouseReleased event handler

# Create a listner that reapplies the selected property of all JySubMenu items
# ...after the BasicMenuUI listener has done all of its required work
from java.awt.event import MouseAdapter
class NonDeselectingListener(MouseAdapter):
	def __init__(self, originalListener):
		self.originalListener = originalListener
	
	def mouseEntered(self, event):
		self.originalListener.mouseEntered(event)
		self.restoreSelections(event)
		event.source.background = highlightColor
	    
	def mouseExited(self, event):
		self.originalListener.mouseExited(event)
		if 'JySubMenu' in event.source.__class__.__name__:
			event.source.background = subMenuBGColor
		else:
			event.source.background = menuItemBGColor
	
	def mouseReleased(self, event):
		self.originalListener.mouseReleased(event)
		
	def restoreSelections(self, event):
		for component in event.source.parent.components:
			if 'JySubMenu' in component.__class__.__name__:
				component.selected = True
			else:
				component.background = menuItemBGColor
		

# Recursively sets the background and foreground colors of all the sub components of a popup menu
def setMenuColor(menu, background, foreground):
	menu.background = background
	menu.foreground = foreground
	for component in menu.components:
		
		# If a submenu is found, color its popup and set it selected property to true
		if 'JySubMenu' in component.__class__.__name__:
			setMenuColor(component, subMenuBGColor, foregroundColor)
			setMenuColor(component.popupMenu, subMenuBGColor, foregroundColor)
			component.selected = True
		else:
			setMenuColor(component, menuItemBGColor, foregroundColor)
		
		# Locate the Basic UI listener of the menu item
		# ...and wrap in a listener that listener that will restore the selected coloring
		# ...before the component is repainted
		for listener in component.mouseListeners:
			if 'BasicMenuUI' in listener.__class__.__name__ or 'BasicMenuItemUI' in listener.__class__.__name__:
				component.removeMouseListener(listener)
				component.addMouseListener(NonDeselectingListener(listener))
	
# Simple nested popup example based on documented example # 4:
def sayHello(event):
	print 'Hello'
highlightColor = system.gui.color('red')

# Initial RGB values for the background color
# [MUST BE values between 0 and 255]
red = 96
green = 96
blue =96
subMenuBGColor = system.gui.color(red, green, blue)

# Match color with selected color
menuItemBGColor = system.gui.color(int(red * 0.84), int(green * 0.93), blue)
foregroundColor = system.gui.color('white')
subMenu01 = [["Click Me 1", "Click Me 2"], [sayHello, sayHello]]
subMenu02 = [["Click Me 3", "Click Me 4"], [sayHello, sayHello]]
subMenu03 = [["Click Me 5", "Click Me 6"], [sayHello, sayHello]]
subMenu04 = [["Click Me 7", "Click Me 8"], [sayHello, sayHello]]
subMenu05 = [["Click Me 9", "Click Me 10"], [sayHello, sayHello]]
menu = system.gui.createPopupMenu(['Click Me', 'subMenu01', 'Click Me', 'subMenu03', 'Click Me', 'subMenu05'], [sayHello, subMenu01, sayHello, subMenu03, sayHello, subMenu05])
setMenuColor(menu, menuItemBGColor, foregroundColor)
menu.focusable = False
menu.show(event)

When using the code example above, the menu item color will always match the selected sub menu color no matter what initial RGB values are assigned to the red, green, and blue variables.

Result:
image

Edit: Added additional details and improved mouse adaptor handling