Java tutorial
/* * 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); }