VISION - Create a Dark Mode Popup Calendar

I've been trying to create a Dark Mode Popup Calendar, and i ran into this problem right here, i cannot change the colors of these specific parts of the popup calendar, how can i do that? is that even possible? i've been searching everywhere and i did not find anything about it :sob:

image

The cool thing about Vision is that anything's possible. It's just a question of whether or not the maintenance costs are worth it.

These two handy utility functions are the product of my years of digging stuff out of Vision components:

# Returns the first nested component of a given class
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getComponentOfClass(container, className):
	for component in container.components:
		if component.__class__.__name__ == className:
			return component
		else:
			foundComponent = getComponentOfClass(component, className)
			if foundComponent:
				return foundComponent

# Returns all nested components of a given class
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getAllComponentsOfClass(container, className):
	foundComponents = []
	for component in container.components:
		if component.__class__.__name__ == className:
			foundComponents.append(component)
		else:
			foundComponents.extend(getAllComponentsOfClass(component, className))
	return foundComponents

The dropdown arrow is a JButton, so using my utility script, getting it from the popup calendar for direct manipulation can be done in this way:

popupCalender = event.source    
dropdownButton = getComponentOfClass(popupCalender, 'JButton')
dropdownButton.background = system.gui.color('grey')

Result:
image

Modifying the down arrow is trickier because it's an icon, but icons of this nature are easy to make. Creating and applying a single character icon looks like this:

from javax.swing import Icon
class WhiteDownArrow(Icon):
	# https://docs.oracle.com/javase/8/docs/api/javax/swing/Icon.html
	# Arguments = (Component c, Graphics g, int x, int y)
	def paintIcon(self, c, g, x, y):
		g.setColor(system.gui.color('white'))
		g.drawString(unicode('▾'), 6, 13) # X and Y coordinates set using trial and error
	
	def getIconWidth(self):
		return 14
	
	def getIconHeight(self):
		return 14

dropdownButton = getComponentOfClass(popupCalender, 'JButton')
dropdownButton.background = system.gui.color('grey')
dropdownButton.icon = WhiteDownArrow()

Result:
image

Getting the internal JButtons and JSpinner from the calendar itself is problematic because first you have to get the calendar. Looking through my notes on this component, it looks like the only way I've found to get that is through reflection. However, once the popup calendar has been obtained, the buttons and JSpinner can be retrieved using my utility scripts in this way:

popupButtons = getAllComponentsOfClass(popup, 'JButton')
spinner = getComponentOfClass(popup, 'JSpinner')

However, when I try to change the color on the JButtons, something in that component overwrites the change when the popup is subsequently launched. An easy way to get around this is to simply add a property change listener to the buttons that will maintain the color

Packaging this all together in a propertyChange script for the dropdown, it ends up looking like this:

# Assumes there is a boolean custom property on the popup calendar called 'isDarkMode',
# ...that is used to toggle the dark mode theme on an off
if event.propertyName == 'isDarkMode' and event.newValue:
	from javax.swing import Icon
	from java.beans import PropertyChangeListener
	
	# Maintains the color of JButtons embedded in a hidden popup menu
	class DarkModeColorListener(PropertyChangeListener):
		def propertyChange(self, event):
			if event.propertyName == 'background' and event.newValue != black:
				event.source.background = black
			elif event.propertyName == 'foreground' and event.newValue != white:
				event.source.foreground = white
	
	# Creates a white replacement icon for the popup calendar dropdown button
	class WhiteDownArrow(Icon):
		# https://docs.oracle.com/javase/8/docs/api/javax/swing/Icon.html
		# Arguments = (Component c, Graphics g, int x, int y)
		def paintIcon(self, c, g, x, y):
			g.setColor(white)
			g.drawString(unicode('▾'), 6, 13) # X and Y coordinates set using trial and error
		
		def getIconWidth(self):
			return 14
		
		def getIconHeight(self):
			return 14
	
	# Returns the first nested component of a given class
	# container = The object containing nested components to be recursively searched
	# className = the __name__ of the class given as a string
	def getComponentOfClass(container, className):
		for component in container.components:
			if component.__class__.__name__ == className:
				return component
			else:
				foundComponent = getComponentOfClass(component, className)
				if foundComponent:
					return foundComponent
	
	# Returns all nested components of a given class
	# container = The object containing nested components to be recursively searched
	# className = the __name__ of the class given as a string
	def getAllComponentsOfClass(container, className):
		foundComponents = []
		for component in container.components:
			if component.__class__.__name__ == className:
				foundComponents.append(component)
			else:
				foundComponents.extend(getAllComponentsOfClass(component, className))
		return foundComponents
	
	# Assign the colors to variables for use everywhere in the script
	black = system.gui.color('black')
	white = system.gui.color('white')
	
	# Get the dropdown JButton, change its background color and give it a white icon
	dropdownButton = getComponentOfClass(event.source, 'JButton')
	dropdownButton.background = system.gui.color('grey')
	dropdownButton.icon = WhiteDownArrow()
	
	# Get the popup calendar from the dropdown using reflection
	popupField = event.source.getClass().getDeclaredField('popup')
	popupField.setAccessible(True)
	popup = popupField.get(event.source)
	
	# Get all of the jbuttons from the popup, change its colors, and add a listener to maintain them
	popupButtons = getAllComponentsOfClass(popup, 'JButton')
	for button in popupButtons:
		if 'DarkModeColorListener' not in [listener.__class__.__name__ for listener in button.propertyChangeListeners]:
			button.addPropertyChangeListener(DarkModeColorListener())
		button.background = black
		button.foreground = white
	
	# Get the spinner component from the popup and directly manipulate its colors
	spinner = getComponentOfClass(popup, 'JSpinner')
	spinner.background = black
	button.foreground = white

Result:
image

For reference, these were the Appearance parameters used during this test:
image

Since I foresee this being used on multiple popup calendars throughout a project, I believe that it would make sense to store this in the project library. If this were wrapped up in a function called setPopupCalendarDark and the function were nested in a library script called `componentScripts, it would look like this:

Library Script Example
from javax.swing import Icon
from java.beans import PropertyChangeListener

# Returns the first nested component of a given class
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getComponentOfClass(container, className):
	for component in container.components:
		if component.__class__.__name__ == className:
			return component
		else:
			foundComponent = getComponentOfClass(component, className)
			if foundComponent:
				return foundComponent

# Returns all nested components of a given class
# container = The object containing nested components to be recursively searched
# className = the __name__ of the class given as a string
def getAllComponentsOfClass(container, className):
	foundComponents = []
	for component in container.components:
		if component.__class__.__name__ == className:
			foundComponents.append(component)
		else:
			foundComponents.extend(getAllComponentsOfClass(component, className))
	return foundComponents

def setPopupCalendarDark(popupCalendar):
	# Maintains the color of JButtons embedded in a hidden popup menu
	class DarkModeColorListener(PropertyChangeListener):
		def propertyChange(self, event):
			if event.propertyName == 'background' and event.newValue != black:
				event.source.background = black
			elif event.propertyName == 'foreground' and event.newValue != white:
				event.source.foreground = white
	
	# Creates a white replacement icon for the popup calendar dropdown button
	class WhiteDownArrow(Icon):
		# https://docs.oracle.com/javase/8/docs/api/javax/swing/Icon.html
		# Arguments = (Component c, Graphics g, int x, int y)
		def paintIcon(self, c, g, x, y):
			g.setColor(white)
			g.drawString(unicode('▾'), 6, 13) # X and Y coordinates set using trial and error
		
		def getIconWidth(self):
			return 14
		
		def getIconHeight(self):
			return 14
	
	# Assign the colors to variables for use everywhere in the script
	black = system.gui.color('black')
	white = system.gui.color('white')
	
	# Get the dropdown JButton, change its background color and give it a white icon
	dropdownButton = getComponentOfClass(popupCalendar, 'JButton')
	dropdownButton.background = system.gui.color('grey')
	dropdownButton.icon = WhiteDownArrow()
	
	# Get the popup calendar from the dropdown using reflection
	popupField = popupCalendar.getClass().getDeclaredField('popup')
	popupField.setAccessible(True)
	popup = popupField.get(popupCalendar)
	
	# Get all of the jbuttons from the popup, change its colors, and add a listener to maintain them
	popupButtons = getAllComponentsOfClass(popup, 'JButton')
	for button in popupButtons:
		if 'DarkModeColorListener' not in [listener.__class__.__name__ for listener in button.propertyChangeListeners]:
			button.addPropertyChangeListener(DarkModeColorListener())
		button.background = black
		button.foreground = white
	
	# Get the spinner component from the popup and directly manipulate its colors
	spinner = getComponentOfClass(popup, 'JSpinner')
	spinner.background = black
	button.foreground = white

Then, it could called by any popup calendar in this way:

# Assumes there is a boolean custom property on the popup calendar called 'isDarkMode',
# ...that is used to toggle the dark mode theme on an off
if event.propertyName == 'isDarkMode' and event.newValue:
	componentScripts.setPopupCalendarDark(event.source)

This approach also frees up the utility scripts to be called from anywhere using their library script paths.

7 Likes

Can't underscore this enough.
I almost tagged you directly, Justin, but figured you wouldn't be able to resist the challenge once you saw this thread :joy:

3 Likes