package org.rr.commons.swing; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Desktop; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Point; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.LookAndFeel; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.plaf.metal.MetalTheme; import org.rr.commons.collection.CompoundList; import org.rr.commons.utils.ListUtils; import org.rr.commons.utils.MathUtils; import org.rr.commons.utils.ReflectionFailureException; import org.rr.commons.utils.ReflectionUtils; import org.rr.commons.utils.StringUtil; import org.rr.commons.utils.UtilConstants; public class SwingUtils { // A maximally-contrasting palette of colors designed by Kenneth Kelly, but without white, black, or gray. private static final List<Color> KENNETH_KELLY_COLORS = Collections.unmodifiableList(new ArrayList<Color>() {{ add(new Color(0xEAD846)); add(new Color(0x6F0989)); add(new Color(0xD9712A)); add(new Color(0x97C9E4)); add(new Color(0xB82035)); add(new Color(0xC3C385)); add(new Color(0x62AC49)); add(new Color(0xCE81AD)); add(new Color(0x476CB3)); add(new Color(0xDB8963)); add(new Color(0x491093)); add(new Color(0xDFAA36)); add(new Color(0x8F0189)); add(new Color(0xE7F45E)); add(new Color(0x7C1B15)); add(new Color(0x94B741)); add(new Color(0x6C3715)); add(new Color(0xD03227)); add(new Color(0x2B3916)); }}); private static JLabel defaultLabel = new JLabel(); private static Color selectionForegroundColor; private static Color selectionBackgroundColor; private static Color foregroundColor; private static Color backgroundColor; private static Color stripeBackgroundColor; /** * Fetch all components from a specified type out of a Window. <BR> * * @param className * Type of the component that should fetched from the given * Window. If the given {@link Window} is <code>null</code>, * all opened <code>{@link Window}</code> instanced will be searched for * a component matching to the given class name. * @param window * The Window which contains the components that should be * fetched. * @return All <code>{@link Component}s</code> of the specified class type * contained by the given <code>{@link Window}</code>. */ public static Component[] getAllComponents(Class<? extends Component> className, Window window) { final ArrayList<Component> comps = new ArrayList<>(); comps.addAll(getAllComponentsRecursive(window, new Class[] {className}, null)); return comps.toArray(new Component[comps.size()]); } /** * Fetch all components from a specified type out of a <code>{@link Container}</code>. <BR> * * @param className * Type of the component that should fetched from the given <code>{@link Container}</code>. * @param container * The <code>{@link Container}</code> which contains the components that should be * fetched. If the given {@link Container} is <code>null</code>, all * opened forms will be searched for a component matching to the * given class name. * @return All <code>{@link Component}s</code> of the specified class type * contained by the given <code>{@link Container}</code>. */ public static Component[] getAllComponents(Class<? extends Component> className, Container container) { Component[] result; if (container == null) { result = getAllComponents(className, (Window)null); } else { result = getAllComponentsRecursive(container, new Class[] {className}, null).toArray(new Component[0]); } return result; } /** * Fills up the given ArrayList with all Components, matching to the given * class file, found downwards the given Container. The Container should be * a JRootPane for example. * * @param container * Container that should be searched for The Component * @param recursive * An empty ArrayList which will be filled up or null if a new * ArrayList should be returned. * @return An array of the type specified with the className will be * returned. If className is <code>null</code> an array over * {@link Component} will be returned. */ private static ArrayList<Component> getAllComponentsRecursive(Container container, Class<? extends Component> classNames[], ArrayList<Component> recursive) { //is there no class name defined, just setup the default value. if (classNames == null || classNames.length == 0) { classNames = new Class[] {Component.class}; } // No ArrayList, just create a new one. if (recursive == null) { // recursive = new Object[0]; recursive = new ArrayList<>(100); } // No Container, nothing to do. if (container == null) { return recursive; } // Search for the component which is an instance of the Class specified // with the className parameter for (int i = 0; i < container.getComponentCount(); i++) { try { Component comp = container.getComponent(i); if (comp instanceof Container) { getAllComponentsRecursive((Container) comp, classNames, recursive); } for (int j = 0; j < classNames.length; j++) { if (classNames[j]==null || classNames[j].isInstance(comp)) { recursive.add(comp); break; } } } catch (Exception e) { // container.getComponent(i); can fail if it was removed. } } return recursive; } /** * Get the background color for the current UI. * @return The desired background color. */ public static Color getSelectionBackgroundColor() { if(selectionBackgroundColor != null) { return selectionBackgroundColor; } selectionBackgroundColor = UIManager.getColor("Table.selectionBackground"); if(selectionBackgroundColor == null) { selectionBackgroundColor = UIManager.getColor("Table[Enabled+Selected].textBackground"); if(selectionBackgroundColor == null) { selectionBackgroundColor = new JList().getSelectionBackground(); } //sometimes the UIManager color won't work selectionBackgroundColor = new Color(selectionBackgroundColor.getRed(), selectionBackgroundColor.getGreen(), selectionBackgroundColor.getBlue()); } //sometimes the UIManager color won't work return selectionBackgroundColor; } /** * Get the foreground color for the current UI. * @return The desired foreground color. */ public static Color getSelectionForegroundColor() { if(selectionForegroundColor != null) { return selectionForegroundColor; } //for windows selectionForegroundColor = (Color) Toolkit.getDefaultToolkit().getDesktopProperty("win.item.highlightTextColor"); if(selectionForegroundColor == null) { selectionForegroundColor = UIManager.getColor("Table.selectionForeground"); if(selectionForegroundColor == null) { selectionForegroundColor = UIManager.getColor("Table[Enabled+Selected].textForeground"); if(selectionForegroundColor == null) { selectionForegroundColor = new JList().getSelectionForeground(); } } //sometimes the UIManager color won't work selectionForegroundColor = new Color(selectionForegroundColor.getRed(), selectionForegroundColor.getGreen(), selectionForegroundColor.getBlue()); } return selectionForegroundColor; } /** * Creates a new color instance which is brighter. * @param color The base color to be made brighter. * @param brighter The rgb value by which the given base color should be made brighter. * @return The brighter color. */ public static Color getBrighterColor(Color color, int brighter) { Color c = new Color(MathUtils.range(color.getRed() + brighter, 0, 255) , MathUtils.range(color.getGreen() + brighter, 0, 255), MathUtils.range(color.getBlue() + brighter, 0, 255)); return c; } public static Color getForegroundColor() { if(foregroundColor != null) { return foregroundColor; } foregroundColor = UIManager.getColor("Table.foreground"); if(foregroundColor == null) { foregroundColor = new JList().getForeground(); } //sometimes the UIManager color won't work foregroundColor = new Color(foregroundColor.getRed(), foregroundColor.getGreen(), foregroundColor.getBlue()); //sometimes the UIManager color won't work return foregroundColor; } public static Color getBackgroundColor() { if(backgroundColor != null) { return backgroundColor; } backgroundColor = UIManager.getColor("Table.background"); if(backgroundColor == null) { backgroundColor = new JList().getBackground(); } //sometimes the UIManager color won't work backgroundColor = new Color(backgroundColor.getRed(), backgroundColor.getGreen(), backgroundColor.getBlue()); return backgroundColor; } public static Color getStripeBackgroundColor() { if(stripeBackgroundColor != null) { return stripeBackgroundColor; } stripeBackgroundColor = getBackgroundColor(); //sometimes the UIManager color won't work int addDarkness = stripeBackgroundColor.getRed() > 128 ? 10 : -1; stripeBackgroundColor = new Color(stripeBackgroundColor.getRed() - addDarkness, stripeBackgroundColor.getGreen() - addDarkness, stripeBackgroundColor.getBlue() - addDarkness); return stripeBackgroundColor; } /** * Center the given <code>windows</code> in relative to the given <code>invoker</code>. * @param invoker Reference window for the window to center. * @param window The window instance to center. */ public static void centerOnWindow(Window invoker, Window window) { Point invokerLocationOnScreen = invoker.getLocationOnScreen(); Dimension invokerSize = invoker.getSize(); Dimension windowSize = window.getSize(); window.setLocation(invokerLocationOnScreen.x + (invokerSize.width / 2) - (windowSize.width / 2), invokerLocationOnScreen.y + ((invokerSize.height / 2) - (windowSize.height / 2) / 2)); } public static void centerOnScreen(Window window) { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension size = window.getSize(); window.setLocation((screenSize.width - size.width) / 2, (screenSize.height - size.height) / 2); } /** * Sets the given action so it's invoked if the user hits the escape key. * @param dialog The dialog to attach the escape key. * @param abortAction The action that is invoked if the escape key is pressed. */ public static void setEscapeWindowAction(JDialog dialog, ActionListener abortAction) { if(abortAction != null) { ((JComponent)dialog.getContentPane()).registerKeyboardAction(abortAction, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); } } /** * Get the mnemonic from the given text. The mnemonic is marked by a leading & character. * @param text The mnemonic search text. * @return The mnemonic or -1 if no mnemonic could be found. */ public static int getMnemonicKeyCode(String text) { for(int i = 0 ; i < text.length(); i++) { char c = text.charAt(i); if(c == '&') { try { return (int) text.charAt(i + 1); } catch(IndexOutOfBoundsException e) { return -1; } } } return -1; } /** * Removes the mnemonic marker from the given string and returns the result. * @param text The text where the marker should be removed from. * @return The text where the first mnemonic char is removed. */ public static String removeMnemonicMarker(String text) { return StringUtil.replace(text, "&", "", 0, 1, UtilConstants.COMPARE_BINARY); } /** * Get the surrounding component of the given type. * @param comp The component child of the component to search * @param type The parent component class. * @return The desired component or <code>null</code> if no component could be found. */ public static Component getSurroundingComponent(Component comp, Class<? extends Component> type) { Component fScrollPane = null; while(comp != null) { comp = comp.getParent(); if(comp != null && ReflectionUtils.isAssignable(type, comp.getClass())) { fScrollPane = comp; break; } } return fScrollPane; } /** * Get the {@link JScrollPane} that surrounds the given component. * @return The desired {@link JScrollPane} or <code>null</code> if no {@link JScrollPane} could be found. */ public static JScrollPane getSurroundingScrollPane(Component comp) { JScrollPane fScrollPane = null; while(comp != null) { comp = comp.getParent(); if(comp instanceof JScrollPane) { fScrollPane = (JScrollPane) comp; break; } } return fScrollPane; } public static Dimension getTextDimension(final String text, final Font font) { if(defaultLabel == null) { defaultLabel = new JLabel(); } // get metrics from the graphics FontMetrics fontMetrics = defaultLabel.getFontMetrics(font); // get the height of a line of text in this // font and render context int hgt = fontMetrics.getHeight(); // get the advance of my text in this font // and render context int adv = fontMetrics.stringWidth(text); // calculate the size of a box to hold the text Dimension size = new Dimension(adv, hgt); return size; } /** * Open the given url in the default system web browser. * @param url The url to browse- * @throws MalformedURLException * @throws IOException * @throws URISyntaxException */ public static void openURL(String url) throws MalformedURLException, IOException, URISyntaxException { Desktop.getDesktop().browse(new URL(url).toURI()); } /** * Sets the given look and feel. * * @param lafClassName The look and feel class name. Should also be a class name and a theme name which * are separated by a ; char. * @throws ReflectionFailureException * @throws UnsupportedLookAndFeelException * @throws ClassNotFoundException * @throws InstantiationException * @throws IllegalAccessException */ public static void setLookAndFeel(String lafClassName) throws ReflectionFailureException, UnsupportedLookAndFeelException, ClassNotFoundException, InstantiationException, IllegalAccessException { if (lafClassName.indexOf(';') != -1) { List<String> split = ListUtils.split(lafClassName, ";"); String className = split.get(0); String themeClassName = split.get(1); LookAndFeel lafInstance = (LookAndFeel) ReflectionUtils.getObjectInstance(className, null); if (ReflectionUtils.containsMethod(className, "setCurrentTheme", new Class<?>[] { MetalTheme.class }, ReflectionUtils.VISIBILITY_VISIBLE_ACCESSIBLE_ONLY)) { MetalTheme theme = (MetalTheme) ReflectionUtils.getObjectInstance(themeClassName, null); ReflectionUtils.invokeMethod(lafInstance, "setCurrentTheme", theme); } UIManager.setLookAndFeel(lafInstance); } else { UIManager.setLookAndFeel(lafClassName); } } public static void setColor(Component comp, boolean isSelected) { if(isSelected) { comp.setBackground(SwingUtils.getSelectionBackgroundColor()); comp.setForeground(SwingUtils.getSelectionForegroundColor()); } else { comp.setBackground(SwingUtils.getBackgroundColor()); comp.setForeground(SwingUtils.getForegroundColor()); } } /** * Generates a list of colors. The result is always identical. * * @param amount The number of colors to calculate. * @return A list of colors. Never returns <code>null</code>. * @see https://stackoverflow.com/questions/3403826/how-to-dynamically-compute-a-list-of-colors */ private static List<Color> calculateUniqueColors(int amount) { final int lowerLimit = 0x10; final int upperLimit = 0xE0; final int colorStep = (int) ((upperLimit - lowerLimit) / Math.pow(amount, 1f / 3)); final List<Color> colors = new ArrayList<>(amount); for (int R = lowerLimit; R < upperLimit; R += colorStep) for (int G = lowerLimit; G < upperLimit; G += colorStep) for (int B = lowerLimit; B < upperLimit; B += colorStep) { if (colors.size() >= amount) { // The calculated step is not very precise, so this safeguard is appropriate return colors; } else { int color = (R << 16) + (G << 8) + (B); colors.add(new Color(color)); } } return colors; } /** * Generates a list of colors. The result is always identical. * * @param amount The number of colors to calculate. * @return A list of colors. Never returns <code>null</code>. */ public static List<Color> getUniqueColors(int amount) { if(amount <= KENNETH_KELLY_COLORS.size()) { return KENNETH_KELLY_COLORS; } return new CompoundList<>(KENNETH_KELLY_COLORS, calculateUniqueColors(amount - KENNETH_KELLY_COLORS.size())); } }