de.ailis.xadrian.utils.SwingUtils.java Source code

Java tutorial

Introduction

Here is the source code for de.ailis.xadrian.utils.SwingUtils.java

Source

/*
 * Copyright (C) 2010-2012 Klaus Reimer <k@ailis.de>
 * See LICENSE.TXT for licensing information.
 */

package de.ailis.xadrian.utils;

import java.awt.Desktop;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.KeyStroke;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.text.JTextComponent;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.WString;

import de.ailis.xadrian.support.Config;

/**
 * Static utility methods for common Swing tasks.
 * 
 * @author Klaus Reimer (k@ailis.de)
 */
public final class SwingUtils {
    /** The logger. */
    private static final Log LOG = LogFactory.getLog(SwingUtils.class);

    /** If platform has a shell32 library. */
    private static boolean hasShell32;

    static {
        try {
            Native.register("shell32");
            hasShell32 = true;
        } catch (final Throwable e) {
            hasShell32 = false;
        }
    }

    /**
     * Private constructor to prevent instantiation
     */
    private SwingUtils() {
        // Empty
    }

    /**
     * Gives a component a popup menu
     * 
     * @param component
     *            The target component
     * @param popup
     *            The popup menu
     */
    public static void setPopupMenu(final JComponent component, final JPopupMenu popup) {
        component.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(final MouseEvent e) {
                // Ignore mouse buttons outside of the normal range. This
                // fixes problems with trackpad scrolling.
                if (e.getButton() > MouseEvent.BUTTON3)
                    return;

                if (e.isPopupTrigger()) {
                    popup.show(component, e.getX(), e.getY());
                }
            }

            @Override
            public void mouseReleased(final MouseEvent e) {
                // Ignore mouse buttons outside of the normal range. This
                // fixes problems with trackpad scrolling.
                if (e.getButton() > MouseEvent.BUTTON3)
                    return;

                if (e.isPopupTrigger()) {
                    popup.show(component, e.getX(), e.getY());
                }
            }
        });
    }

    /**
     * Called internally by installGtkPopupBugWorkaround to fix the thickness
     * of a GTK style field by setting it to a minimum value of 1.
     * 
     * @param style
     *            The GTK style object.
     * @param fieldName
     *            The field name.
     * @throws Exception
     *             When reflection fails.
     */
    private static void fixGtkThickness(Object style, String fieldName) throws Exception {
        Field field = style.getClass().getDeclaredField(fieldName);
        boolean accessible = field.isAccessible();
        field.setAccessible(true);
        field.setInt(style, Math.max(1, field.getInt(style)));
        field.setAccessible(accessible);
    }

    /**
     * Called internally by installGtkPopupBugWorkaround. Returns a specific
     * GTK style object.
     * 
     * @param styleFactory
     *            The GTK style factory.
     * @param component
     *            The target component of the style.
     * @param regionName
     *            The name of the target region of the style.
     * @return The GTK style.
     * @throws Exception
     *             When reflection fails.
     */
    private static Object getGtkStyle(Object styleFactory, JComponent component, String regionName)
            throws Exception {
        // Create the region object
        Class<?> regionClass = Class.forName("javax.swing.plaf.synth.Region");
        Field field = regionClass.getField(regionName);
        Object region = field.get(regionClass);

        // Get and return the style
        Class<?> styleFactoryClass = styleFactory.getClass();
        Method method = styleFactoryClass.getMethod("getStyle", new Class<?>[] { JComponent.class, regionClass });
        boolean accessible = method.isAccessible();
        method.setAccessible(true);
        Object style = method.invoke(styleFactory, component, region);
        method.setAccessible(accessible);
        return style;
    }

    /**
     * Swing menus are looking pretty bad on Linux when the GTK LaF is used (See
     * bug #6925412). It will most likely never be fixed anytime soon so this
     * method provides a workaround for it. It uses reflection to change the GTK
     * style objects of Swing so popup menu borders have a minimum thickness of
     * 1 and menu separators have a minimum vertical thickness of 1.
     */
    public static void installGtkPopupBugWorkaround() {
        // Get current look-and-feel implementation class
        LookAndFeel laf = UIManager.getLookAndFeel();
        Class<?> lafClass = laf.getClass();

        // Do nothing when not using the problematic LaF
        if (!lafClass.getName().equals("com.sun.java.swing.plaf.gtk.GTKLookAndFeel"))
            return;

        // We do reflection from here on. Failure is silently ignored. The
        // workaround is simply not installed when something goes wrong here
        try {
            // Access the GTK style factory
            Field field = lafClass.getDeclaredField("styleFactory");
            boolean accessible = field.isAccessible();
            field.setAccessible(true);
            Object styleFactory = field.get(laf);
            field.setAccessible(accessible);

            // Fix the horizontal and vertical thickness of popup menu style
            Object style = getGtkStyle(styleFactory, new JPopupMenu(), "POPUP_MENU");
            fixGtkThickness(style, "yThickness");
            fixGtkThickness(style, "xThickness");

            // Fix the vertical thickness of the popup menu separator style
            style = getGtkStyle(styleFactory, new JSeparator(), "POPUP_MENU_SEPARATOR");
            fixGtkThickness(style, "yThickness");
        } catch (Exception e) {
            // Silently ignored. Workaround can't be applied.
        }
    }

    /**
     * Installs a workaround for bug #4699955 in a JSpinner.
     * 
     * @param spinner
     *            The spinner to fix
     */
    public static void installSpinnerBugWorkaround(final JSpinner spinner) {
        ((DefaultEditor) spinner.getEditor()).getTextField().addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(final FocusEvent e) {
                if (e.getSource() instanceof JTextComponent) {
                    final JTextComponent text = ((JTextComponent) e.getSource());
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            text.selectAll();
                        }
                    });
                }
            }
        });
        spinner.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(final FocusEvent e) {
                if (e.getSource() instanceof JSpinner) {
                    final JTextComponent text = ((DefaultEditor) ((JSpinner) e.getSource()).getEditor())
                            .getTextField();
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            text.requestFocus();
                        }
                    });
                }
            }
        });
    }

    /**
     * Checks if the specified window (may it be a dialog or a frame) is
     * resizable.
     * 
     * @param window
     *            The window
     * @return True if window is resizable, false if not
     */
    public static boolean isResizable(final Window window) {
        if (window instanceof Dialog)
            return ((Dialog) window).isResizable();
        if (window instanceof Frame)
            return ((Frame) window).isResizable();
        return false;
    }

    /**
     * Prepares the locale. The default is the system locale.
     */
    public static void prepareLocale() {
        final String locale = Config.getInstance().getLocale();
        if (locale != null)
            Locale.setDefault(new Locale(locale));
    }

    /**
     * Prepares the theme. The theme can be overridden with the environment
     * variable XADRIAN_SYSTHEME. The default is the system look and feel.
     */
    public static void prepareTheme() {
        final String theme = Config.getInstance().getTheme();
        if (theme != null) {
            try {
                UIManager.setLookAndFeel(theme);
                installGtkPopupBugWorkaround();
                return;
            } catch (final Exception e) {
                LOG.warn("Can't set theme " + theme + ". Falling back to system look-and-feel. Reason: " + e, e);
            }
        }

        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            installGtkPopupBugWorkaround();
        } catch (final Exception e) {
            LOG.warn("Can't set system look-and-feel. " + "Falling back to default. Reason: " + e, e);
        }
    }

    /**
     * Prepares the Swing GUI.
     */
    public static void prepareGUI() {
        prepareLocale();
        prepareTheme();
    }

    /**
     * Runs the specified component in an empty test frame. This method is used
     * to test single components during development.
     * 
     * @param component
     *            The component to test
     * @throws Exception
     *             When something goes wrong
     */
    public static void testComponent(final JComponent component) throws Exception {
        final JFrame frame = new JFrame("Test");
        frame.setName("componentTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(component);
        frame.pack();
        frame.setVisible(true);
    }

    /**
     * Sets the preferred height of the specified component.
     * 
     * @param component
     *            The component
     * @param height
     *            The preferred height
     */
    public static void setPreferredHeight(final JComponent component, final int height) {
        component.setPreferredSize(new Dimension(component.getPreferredSize().width, height));
    }

    /**
     * Sets the preferred width of the specified component.
     * 
     * @param component
     *            The component
     * @param width
     *            The preferred width
     */
    public static void setPreferredWidth(final JComponent component, final int width) {
        component.setPreferredSize(new Dimension(width, component.getPreferredSize().height));
    }

    /**
     * Adds a component action.
     * 
     * @param component
     *            The compoenet to add the action to
     * @param action
     *            The action to add
     */
    public static void addComponentAction(final JComponent component, final Action action) {
        final InputMap imap = component
                .getInputMap(component.isFocusable() ? JComponent.WHEN_FOCUSED : JComponent.WHEN_IN_FOCUSED_WINDOW);
        final ActionMap amap = component.getActionMap();
        final KeyStroke ks = (KeyStroke) action.getValue(Action.ACCELERATOR_KEY);
        imap.put(ks, action.getValue(Action.NAME));
        amap.put(action.getValue(Action.NAME), action);
    }

    /**
     * Opens a URL in the browser. It first tries to do this with the Desktop
     * API. If this fails then it tries to use the FreeDesktop-API.
     * 
     * @param uri
     *            The URI to open.
     */
    public static void openBrowser(final URI uri) {
        try {
            try {
                Desktop.getDesktop().browse(uri);
            } catch (final UnsupportedOperationException e) {
                Runtime.getRuntime().exec("xdg-open '" + uri + "'");
            }
        } catch (final IOException e) {
            LOG.error("Unable to external browser: " + e, e);
        }
    }

    /**
     * Opens a URL in the browser. It first tries to do this with the Desktop
     * API. If this fails then it tries to use the FreeDesktop-API.
     * 
     * @param url
     *            The URL to open.
     */
    public static void openBrowser(final String url) {
        try {
            openBrowser(new URI(url));
        } catch (final URISyntaxException e) {
            LOG.error(e.toString(), e);
        }
    }

    /**
     * Sets the application name. There is no API for this (See
     * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6528430) so this
     * method uses reflection to do this. This may fail if the Java
     * implementation is changed but any exception here will be ignored.
     * 
     * The application name is currently only used for X11 desktops and only
     * important for some window managers like Gnome Shell.
     * 
     * @param appName
     *            The application name to set.
     */
    public static void setAppName(final String appName) {
        final Toolkit toolkit = Toolkit.getDefaultToolkit();
        final Class<?> cls = toolkit.getClass();

        try {
            // When X11 toolkit is used then set the awtAppClassName field
            if (cls.getName().equals("sun.awt.X11.XToolkit")) {
                final Field field = cls.getDeclaredField("awtAppClassName");
                field.setAccessible(true);
                field.set(toolkit, appName);
            }
        } catch (final Exception e) {
            LOG.warn("Unable to set application name: " + e, e);
        }
    }

    /**
     * Sets the app user model id. This is needed for the Windows 7 taskbar
     * so the application is correctly associated with the starter icon.
     * The same app user model id must be set in the shortcut.
     * 
     * @param appId
     *            The app user model id to set.
     */
    public static void setAppUserModelId(final String appId) {
        if (!hasShell32)
            return;
        try {
            final long errorCode = SetCurrentProcessExplicitAppUserModelID(new WString(appId)).longValue();
            if (errorCode != 0)
                LOG.error("Unable to set appUserModelID. Error code " + errorCode);
        } catch (final Throwable e) {
            LOG.error("Unable to set appUserModelID: " + e, e);
        }
    }

    /**
     * Native Windows function mapped via JNA.
     * 
     * @param appId
     *            The app user model ID to set.
     * @return Error code.
     */
    private static native NativeLong SetCurrentProcessExplicitAppUserModelID(WString appId);
}