/* * CoreUtility.java * Copyright 2002-2003 (C) B. K. Oxley (binkley) * <binkley@alumni.rice.edu> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * * Created on Februrary 4th, 2002. */ package pcgen.gui2.tools; import java.awt.Component; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowEvent; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JRootPane; import javax.swing.JTabbedPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import pcgen.system.PCGenSettings; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.Contract; /** * Convenience methods from various sources. * * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a> */ public final class Utility { private static final KeyStroke escapeStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0); /** * An action map key for the user requesting a dialog close via the ESC key. */ private static final String dispatchWindowClosingActionMapKey = "pcgen:WINDOW_CLOSING"; //$NON-NLS-1$ private Utility() { } /** * Set up GridBag Constraints. * * @param gbc The gridbagconstraints to set up * @param gx cols from left (left-most col for multi-column cell) * @param gy rows from top (top-most row for multi-row cell) * @param gw cols wide * @param gh rows high * @param wx weight of x, I typically put in percentile, only need to specify this once for each column, other values in same column are 0.0 * @param wy weight of y, same as weight for cols, just specify a non-zero value for one cell in each row. */ public static void buildConstraints(GridBagConstraints gbc, int gx, int gy, int gw, int gh, double wx, double wy) { gbc.gridx = gx; gbc.gridy = gy; gbc.gridwidth = gw; gbc.gridheight = gh; gbc.weightx = wx; gbc.weighty = wy; } /** * Set up GridBag Constraints. * * @param gbc The gridbagconstraints to set up * @param gx cols from left (left-most col for multi-column cell) * @param gy rows from top (top-most row for multi-row cell) * @param gw cols wide * @param gh rows high * @param wx weight of x, I typically put in percentile, only need to specify this once for each column, other values in same column are 0.0 * @param wy weight of y, same as weight for cols, just specify a non-zero value for one cell in each row. * @param fill How should the component be resized if smaller than the space. * @param anchor Where should the component be placed if smaller than the space. */ public static void buildConstraints(GridBagConstraints gbc, int gx, int gy, int gw, int gh, double wx, double wy, int fill, int anchor) { buildConstraints(gbc, gx, gy, gw, gh, wx, wy); gbc.fill = fill; gbc.anchor = anchor; } /** * Set up GridBag Constraints in a relative pattern. Components must be * added in order row by row. * * @param gbc The gridbagconstraints to set up * @param gw cols wide * @param gh rows high * @param wx weight of x, I typically put in percentile, only need to specify this once for each column, other values in same column are 0.0 * @param wy weight of y, same as weight for cols, just specify a non-zero value for one cell in each row. * @param fill How should the component be resized if smaller than the space. * @param anchor Where should the component be placed if smaller than the space. */ public static void buildRelativeConstraints(GridBagConstraints gbc, int gw, int gh, double wx, double wy, int fill, int anchor) { buildConstraints(gbc, GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE, gw, gh, wx, wy); gbc.fill = fill; gbc.anchor = anchor; } /** * Set up GridBag Constraints in a relative pattern. Components must be * added in order row by row. * * @param gbc The gridbagconstraints to set up * @param gw cols wide * @param gh rows high * @param wx weight of x, I typically put in percentile, only need to specify this once for each column, other values in same column are 0.0 * @param wy weight of y, same as weight for cols, just specify a non-zero value for one cell in each row. */ public static void buildRelativeConstraints(GridBagConstraints gbc, int gw, int gh, double wx, double wy) { buildConstraints(gbc, GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE, gw, gh, wx, wy); } /** * Centers a {@code Component} to the screen. * * @param dialog JDialog dialog to center */ public static void centerComponent(Component dialog) { // since the Toolkit.getScreenSize() method is broken in the Linux implementation // of Java 5 (it returns double the screen size under xinerama), this method is // encapsulated to accomodate this with a hack. // TODO: remove the hack, once Java fixed this. // final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); final Rectangle screenSize = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration() .getBounds(); final Dimension dialogSize = dialog.getSize(); if (dialogSize.height > screenSize.height) { dialogSize.height = screenSize.height; } if (dialogSize.width > screenSize.width) { dialogSize.width = screenSize.width; } dialog.setSize(dialogSize); dialog.setLocation(screenSize.x + ((screenSize.width - dialogSize.width) / 2), screenSize.y + ((screenSize.height - dialogSize.height) / 2)); } /** * Update the size of the dialog to ensure it will fit on the screen. * * @param dialog The dialog to be resized. */ public static void resizeComponentToScreen(Component dialog) { // Get the maximum window size to account for taskbars etc Rectangle screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment() .getMaximumWindowBounds(); final Dimension dialogSize = dialog.getSize(); if (dialogSize.height > screenBounds.height) { dialogSize.height = screenBounds.height; } if (dialogSize.width > screenBounds.width) { dialogSize.width = screenBounds.width; } dialog.setSize(dialogSize); } /** * Centres the dialog over the component ensuring that the dialog will be * within the usable area of the screen (i.e. excluding native task bars, * menus bars etc). * * @param parent The component over which the dialog should be centred. * @param dialog The dialog to be positioned. */ public static void setComponentRelativeLocation(Component parent, Component dialog) { // First make sure it is not too big resizeComponentToScreen(dialog); // Get the maximum window size to account for taskbars etc Rectangle screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment() .getMaximumWindowBounds(); Point centreOfParent = new Point(parent.getWidth() / 2, parent.getHeight() / 2); SwingUtilities.convertPointToScreen(centreOfParent, parent); // Default to centre of parent Point location = new Point(centreOfParent.x - (dialog.getWidth() / 2), centreOfParent.y - (dialog.getHeight() / 2)); // Adjust so it fits on the screen if ((location.x + dialog.getWidth()) > (screenBounds.width + screenBounds.x)) { location.x -= (location.x + dialog.getWidth()) - (screenBounds.width + screenBounds.x); } if (location.x < screenBounds.x) { location.x = screenBounds.x; } if ((location.y + dialog.getHeight()) > (screenBounds.height + screenBounds.y)) { location.y -= (location.y + dialog.getHeight()) - (screenBounds.height + screenBounds.y); } if (location.y < screenBounds.y) { location.y = screenBounds.y; } dialog.setLocation(location); } /** * Centers a {@code JFrame} to the screen. * * @param frame JFrame frame to center * @param isPopup boolean is the frame a popup dialog? */ public static void centerComponent(Component frame, boolean isPopup) { // since the Toolkit.getScreenSize() method is broken in the Linux implementation // of Java 5 (it returns double the screen size under xinerama), this method is // encapsulated to accomodate this with a hack. // TODO: remove the hack, once Java fixed this. // final Dimension screenSize = getScreenSize(Toolkit.getDefaultToolkit()); final Rectangle screenSize = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds(); if (isPopup) { frame.setSize(screenSize.width / 2, screenSize.height / 2); } final Dimension frameSize = frame.getSize(); if (frameSize.height > screenSize.height) { frameSize.height = screenSize.height; } if (frameSize.width > screenSize.width) { frameSize.width = screenSize.width; } frame.setLocation(screenSize.x + ((screenSize.width - frameSize.width) / 2), screenSize.y + ((screenSize.height - frameSize.height) / 2)); } /** * Sets the default browser. * * @param parent The component to show the dialog over. */ public static void selectDefaultBrowser(Component parent) { final JFileChooser fc = new JFileChooser(); fc.setDialogTitle("Find and select your preferred html browser."); if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX) { // On MacOS X, do not traverse file bundles fc.putClientProperty("JFileChooser.appBundleIsTraversable", "never"); } if (PCGenSettings.getBrowserPath() == null) { //No action, as we have no idea what a good default would be... } else { fc.setCurrentDirectory(new File(PCGenSettings.getBrowserPath())); } final int returnVal = fc.showOpenDialog(parent); if (returnVal == JFileChooser.APPROVE_OPTION) { final File file = fc.getSelectedFile(); PCGenSettings.OPTIONS_CONTEXT.setProperty(PCGenSettings.BROWSER_PATH, file.getAbsolutePath()); } } /** * View a URL in a browser. Uses BrowserLauncher class. * * @param url URL to display in browser. * @throws IOException if file doesn't exist * @see DesktopBrowserLauncher */ public static void viewInBrowser(String url) throws IOException { viewInBrowser(new URL(url)); } /** * View a file (should be browsable) in a browser. * * @param file Path of the file to display in browser. * @throws IOException if file doesn't exist * @see DesktopBrowserLauncher */ public static void viewInBrowser(File file) throws IOException { viewInBrowser(file.toURI()); } /** * View a URL in a browser * * @param url URL to display in browser. * @throws IOException if the URL is bad or the browser can not be launched * @see DesktopBrowserLauncher */ static void viewInBrowser(URL url) throws IOException { try { viewInBrowser(url.toURI()); } catch (final URISyntaxException e) { throw new MalformedURLException(e.getMessage()); } } /** * View a URI in a browser. * * @param uri URI to display in browser. * @throws IOException if browser can not be launched * @see DesktopBrowserLauncher */ private static void viewInBrowser(URI uri) throws IOException { // Windows tends to lock up or not actually // display anything unless we've specified a // default browser, so at least make the user // aware that (s)he needs one. If they don't // pick one and it doesn't work, at least they // might know enough to try selecting one the // next time. if (!DesktopBrowserLauncher.isBrowseSupported() && SystemUtils.IS_OS_WINDOWS && (PCGenSettings.getBrowserPath() == null)) { selectDefaultBrowser(null); } DesktopBrowserLauncher.browse(uri); } /** * Add a keyboard shortcut to allow ESC to close the dialog. * * @param dialog The dialog to be updated. */ public static void installEscapeCloseOperation(final JDialog dialog) { JRootPane root = dialog.getRootPane(); root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeStroke, dispatchWindowClosingActionMapKey); Action dispatchClosing = new AbstractAction() { @Override public void actionPerformed(ActionEvent event) { dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING)); } }; root.getActionMap().put(dispatchWindowClosingActionMapKey, dispatchClosing); } /** * Adjust the crop rectangle to fit within the image it is cropping. Also * ensure the area is square. * * @param image The image being cropped * @param cropRect The rectangle defining the cropping area. This may be updated. */ public static void adjustRectToFitImage(RenderedImage image, Rectangle cropRect) { // Make sure the rectangle is not too big if (cropRect.width > image.getWidth()) { cropRect.width = image.getWidth(); } if (cropRect.height > image.getHeight()) { cropRect.height = image.getHeight(); } // Make it square int dimension = Math.min(cropRect.width, cropRect.height); cropRect.setSize(dimension, dimension); // Now adjust the origin point so the box is within the image if ((cropRect.x + cropRect.width) > image.getWidth()) { cropRect.x = image.getWidth() - cropRect.width; } if ((cropRect.y + cropRect.height) > image.getHeight()) { cropRect.y = image.getHeight() - cropRect.height; } } /** * This method is used to set the name of the application for the window manager, especially X11. * * @param title Title to use */ public static void setApplicationTitle(String title) { Toolkit xToolkit = Toolkit.getDefaultToolkit(); try { Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName"); //$NON-NLS-1$ awtAppClassNameField.setAccessible(true); awtAppClassNameField.set(xToolkit, title); } catch (NoSuchFieldException | IllegalAccessException e) { // Rather than do a OS system condition, just ignore this expected exception //Logging.log(Level.FINEST, "Can not set name of application for window manager", e); } } /** * Trim a string from the left to fit within the specified width. * * @param fm The font the text will be rendered in. * @param str The string to trimmed. * @param maxWidth The maximum width that the string is allowed to be. * @return String The trimmed string. */ public static String shortenString(FontMetrics fm, String str, int maxWidth) { for (int i = str.length(); i > 0; i -= 5) { String shortedString = "..." + str.substring(str.length() - i); int width = fm.stringWidth(shortedString); //System.out.println("testing '"+foo+"' = "+width); if (width < maxWidth) { return shortedString; } } return ""; } /** * Get the tabbed pane for a component * * @param c * @return the tabbed pane for a component */ @Contract("null -> null") public static JTabbedPane getTabbedPaneFor(Component c) { if (c == null) { return null; } if (c instanceof JTabbedPane) { return (JTabbedPane) c; } return getTabbedPaneFor(c.getParent()); } }