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
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:
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:
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:
For reference, these were the Appearance parameters used during this test:
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.
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