
com.samskivert.swing.util.SwingUtil Maven / Gradle / Ivy
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2011 Michael Bayne, et al.
// http://github.com/samskivert/samskivert/blob/master/COPYING
package com.samskivert.swing.util;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.Window;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.Spring;
import javax.swing.SpringLayout;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import javax.swing.text.JTextComponent;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import com.samskivert.util.SortableArrayList;
import static com.samskivert.Log.log;
/**
* Miscellaneous useful Swing-related utility functions.
*/
public class SwingUtil
{
/**
* An operation that may be applied to a component.
*/
public static interface ComponentOp
{
/** Apply an operation to the given component. */
public void apply (Component comp);
}
/**
* An interface for validating the text contained within a document.
*/
public static interface DocumentValidator
{
/** Return false if the text is not valid for any reason. */
public boolean isValid (String text);
}
/**
* An interface for transforming the text contained within a document.
*/
public static interface DocumentTransformer
{
/** Transform the specified text in some way, or simply return the text untransformed. */
public String transform (String text);
}
/**
* Center the given window within the screen boundaries.
*
* @param window the window to be centered.
*/
public static void centerWindow (Window window)
{
Rectangle bounds;
try {
bounds = GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().getDefaultConfiguration().getBounds();
} catch (Throwable t) {
Toolkit tk = window.getToolkit();
Dimension ss = tk.getScreenSize();
bounds = new Rectangle(ss);
}
int width = window.getWidth(), height = window.getHeight();
window.setBounds(bounds.x + (bounds.width-width)/2, bounds.y + (bounds.height-height)/2,
width, height);
}
/**
* Centers component b
within component a
.
*/
public static void centerComponent (Component a, Component b)
{
Dimension asize = a.getSize(), bsize = b.getSize();
b.setLocation((asize.width - bsize.width) / 2, (asize.height - bsize.height) / 2);
}
/**
* Draw a string centered within a rectangle. The string is drawn using the graphics context's
* current font and color.
*
* @param g the graphics context.
* @param str the string.
* @param x the bounding x position.
* @param y the bounding y position.
* @param width the bounding width.
* @param height the bounding height.
*/
public static void drawStringCentered (
Graphics g, String str, int x, int y, int width, int height)
{
FontMetrics fm = g.getFontMetrics(g.getFont());
int xpos = x + ((width - fm.stringWidth(str)) / 2);
int ypos = y + ((height + fm.getAscent()) / 2);
g.drawString(str, xpos, ypos);
}
/**
* Returns the most reasonable position for the specified rectangle to be placed at so as to
* maximize its containment by the specified bounding rectangle while still placing it as near
* its original coordinates as possible.
*
* @param rect the rectangle to be positioned.
* @param bounds the containing rectangle.
*/
public static Point fitRectInRect (Rectangle rect, Rectangle bounds)
{
// Guarantee that the right and bottom edges will be contained and do our best for the top
// and left edges.
return new Point(Math.min(bounds.x + bounds.width - rect.width,
Math.max(rect.x, bounds.x)),
Math.min(bounds.y + bounds.height - rect.height,
Math.max(rect.y, bounds.y)));
}
/**
* Position the specified rectangle as closely as possible to its current position, but make
* sure it is within the specified bounds and that it does not overlap any of the Shapes
* contained in the avoid list.
*
* @param r the rectangle to attempt to position.
* @param bounds the bounding box within which the rectangle must be positioned.
* @param avoidShapes a collection of Shapes that must not be overlapped. The collection will
* be destructively modified.
*
* @return true if the rectangle was successfully placed, given the constraints, or false if
* the positioning failed (the rectangle will be left at it's original location.
*/
public static boolean positionRect (
Rectangle r, Rectangle bounds, Collection extends Shape> avoidShapes)
{
Point origPos = r.getLocation();
Comparator comp = createPointComparator(origPos);
SortableArrayList possibles = new SortableArrayList();
// start things off with the passed-in point (adjusted to be inside the bounds, if needed)
possibles.add(fitRectInRect(r, bounds));
// keep track of area that doesn't generate new possibles
Area dead = new Area();
CHECKPOSSIBLES:
while (!possibles.isEmpty()) {
r.setLocation(possibles.remove(0));
// make sure the rectangle is in the view and not over a dead area
if ((!bounds.contains(r)) || dead.intersects(r)) {
continue;
}
// see if it hits any shapes we're trying to avoid
for (Iterator extends Shape> iter = avoidShapes.iterator(); iter.hasNext(); ) {
Shape shape = iter.next();
if (shape.intersects(r)) {
// remove that shape from our avoid list
iter.remove();
// but add it to our dead area
dead.add(new Area(shape));
// add 4 new possible points, each pushed in one direction
Rectangle pusher = shape.getBounds();
possibles.add(new Point(pusher.x - r.width, r.y));
possibles.add(new Point(r.x, pusher.y - r.height));
possibles.add(new Point(pusher.x + pusher.width, r.y));
possibles.add(new Point(r.x, pusher.y + pusher.height));
// re-sort the list
possibles.sort(comp);
continue CHECKPOSSIBLES;
}
}
// hey! if we got here, then it worked!
return true;
}
// we never found a match, move the rectangle back
r.setLocation(origPos);
return false;
}
/**
* Create a comparator that compares against the distance from the specified point.
*
* Note: The comparator will continue to sort by distance from the origin point, even if the
* origin point's coordinates are modified after the comparator is created.
*
* Used by positionRect().
*/
public static Comparator
createPointComparator (final P origin)
{
return new Comparator
() {
public int compare (P p1, P p2)
{
double dist1 = origin.distance(p1);
double dist2 = origin.distance(p2);
return (dist1 > dist2) ? 1 : ((dist1 < dist2) ? -1 : 0);
}
};
}
/**
* Enables (or disables) the specified component, and all of its children. A simple
* call to {@link Container#setEnabled} does not propagate the enabled state to the children of
* a component, which is senseless in our opinion, but was surely done for some arguably good
* reason.
*/
public static void setEnabled (Container comp, final boolean enabled)
{
applyToHierarchy(comp, new ComponentOp() {
public void apply (Component comp) {
comp.setEnabled(enabled);
}
});
}
/**
* Set the opacity on the specified component, and all of its children.
*/
public static void setOpaque (JComponent comp, final boolean opaque)
{
applyToHierarchy(comp, new ComponentOp() {
public void apply (Component comp) {
if (comp instanceof JComponent) {
((JComponent) comp).setOpaque(opaque);
}
}
});
}
/**
* Apply the specified ComponentOp to the supplied component and then all its descendants.
*/
public static void applyToHierarchy (Component comp, ComponentOp op)
{
applyToHierarchy(comp, Integer.MAX_VALUE, op);
}
/**
* Apply the specified ComponentOp to the supplied component and then all its descendants, up
* to the specified maximum depth.
*/
public static void applyToHierarchy (Component comp, int depth, ComponentOp op)
{
if (comp == null) {
return;
}
op.apply(comp);
if (comp instanceof Container && --depth >= 0) {
Container c = (Container) comp;
int ccount = c.getComponentCount();
for (int ii = 0; ii < ccount; ii++) {
applyToHierarchy(c.getComponent(ii), depth, op);
}
}
}
/**
* Set active Document helpers on the specified text component. Changes will not and cannot be
* made (either via user inputs or direct method manipulation) unless the validator says that
* the changes are ok.
*
* @param validator if non-null, all changes are sent to this for approval.
* @param transformer if non-null, is queried to change the text after all changes are made.
*/
public static void setDocumentHelpers (JTextComponent comp, DocumentValidator validator,
DocumentTransformer transformer)
{
setDocumentHelpers(comp.getDocument(), validator, transformer);
}
/**
* Set active Document helpers on the specified Document. Changes will not and cannot be made
* (either via user inputs or direct method manipulation) unless the validator says that the
* changes are ok.
*
* @param validator if non-null, all changes are sent to this for approval.
* @param transformer if non-null, is queried to change the text after all changes are made.
*/
public static void setDocumentHelpers (final Document doc, final DocumentValidator validator,
final DocumentTransformer transformer)
{
if (!(doc instanceof AbstractDocument)) {
throw new IllegalArgumentException("Specified document cannot be filtered!");
}
// set up the filter.
((AbstractDocument) doc).setDocumentFilter(new DocumentFilter() {
@Override public void remove (FilterBypass fb, int offset, int length)
throws BadLocationException
{
if (replaceOk(offset, length, "")) {
fb.remove(offset, length);
transform(fb);
}
}
@Override public void insertString (
FilterBypass fb, int offset, String string, AttributeSet attr)
throws BadLocationException
{
if (replaceOk(offset, 0, string)) {
fb.insertString(offset, string, attr);
transform(fb);
}
}
@Override public void replace (
FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
throws BadLocationException
{
if (replaceOk(offset, length, text)) {
fb.replace(offset, length, text, attrs);
transform(fb);
}
}
/**
* Convenience for remove/insert/replace to see if the proposed change is valid.
*/
protected boolean replaceOk (int offset, int length, String text)
throws BadLocationException
{
if (validator == null) {
return true; // everything's ok
}
try {
String current = doc.getText(0, doc.getLength());
String potential = current.substring(0, offset) +
text + current.substring(offset + length);
// validate the potential text.
return validator.isValid(potential);
} catch (IndexOutOfBoundsException ioobe) {
throw new BadLocationException("Bad Location", offset + length);
}
}
/**
* After a remove/insert/replace has taken place, we may want to transform the text in
* some way.
*/
protected void transform (FilterBypass fb)
{
if (transformer == null) {
return;
}
try {
String text = doc.getText(0, doc.getLength());
String xform = transformer.transform(text);
if (!text.equals(xform)) {
fb.replace(0, text.length(), xform, null);
}
} catch (BadLocationException ble) {
// oh well.
}
}
});
}
/**
* Activates anti-aliasing in the supplied graphics context on both text and 2D drawing
* primitives.
*
* @return an object that should be passed to {@link #restoreAntiAliasing} to restore the
* graphics context to its original settings.
*/
public static Object activateAntiAliasing (Graphics2D gfx)
{
RenderingHints ohints = gfx.getRenderingHints(), nhints = new RenderingHints(null);
nhints.add(ohints);
nhints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
nhints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
gfx.setRenderingHints(nhints);
return ohints;
}
/**
* Restores anti-aliasing in the supplied graphics context to its original setting.
*
* @param rock the results of a previous call to {@link #activateAntiAliasing} or null, in
* which case this method will NOOP. This alleviates every caller having to conditionally avoid
* calling restore if they chose not to activate earlier.
*/
public static void restoreAntiAliasing (Graphics2D gfx, Object rock)
{
if (rock != null) {
gfx.setRenderingHints((RenderingHints)rock);
}
}
/**
* Returns true if anti-aliasing is desired by default. This currently checks the value of the
* swing.aatext
property, but will someday switch to using Java Desktop Properties
* which in theory get their values from OS preferences.
*/
public static boolean getDefaultTextAntialiasing ()
{
return _defaultTextAntialiasing;
}
/**
* Adjusts the widths and heights of the cells of the supplied table to fit their contents.
*/
public static void sizeToContents (JTable table)
{
TableModel model = table.getModel();
TableColumn column = null;
Component comp = null;
int ccount = table.getColumnModel().getColumnCount(),
rcount = model.getRowCount(), cellHeight = 0;
for (int cc = 0; cc < ccount; cc++) {
int headerWidth = 0, cellWidth = 0;
column = table.getColumnModel().getColumn(cc);
try {
comp = column.getHeaderRenderer().getTableCellRendererComponent(
null, column.getHeaderValue(), false, false, 0, 0);
headerWidth = comp.getPreferredSize().width;
} catch (NullPointerException e) {
// getHeaderRenderer() this doesn't work in 1.3
}
for (int rr = 0; rr < rcount; rr++) {
Object cellValue = model.getValueAt(rr, cc);
comp = table.getDefaultRenderer(model.getColumnClass(cc)).
getTableCellRendererComponent(table, cellValue, false, false, 0, cc);
Dimension psize = comp.getPreferredSize();
cellWidth = Math.max(psize.width, cellWidth);
cellHeight = Math.max(psize.height, cellHeight);
}
column.setPreferredWidth(Math.max(headerWidth, cellWidth));
}
if (cellHeight > 0) {
table.setRowHeight(cellHeight);
}
}
/**
* Refreshes the supplied {@link JComponent} to effect a call to {@link JComponent#revalidate}
* and {@link JComponent#repaint}, which is frequently necessary in cases such as adding
* components to or removing components from a {@link JPanel} since Swing doesn't automatically
* invalidate things for proper re-rendering.
*/
public static void refresh (JComponent c)
{
c.revalidate();
c.repaint();
}
/**
* Create a custom cursor out of the specified image, putting the hotspot in the exact center
* of the created cursor.
*/
public static Cursor createImageCursor (Image img)
{
return createImageCursor(img, null);
}
/**
* Create a custom cursor out of the specified image, with the specified hotspot.
*/
public static Cursor createImageCursor (Image img, Point hotspot)
{
Toolkit tk = Toolkit.getDefaultToolkit();
// for now, just report the cursor restrictions, then blindly create
int w = img.getWidth(null);
int h = img.getHeight(null);
Dimension d = tk.getBestCursorSize(w, h);
// int colors = tk.getMaximumCursorColors();
// Log.debug("Creating custom cursor [desiredSize=" + w + "x" + h +
// ", bestSize=" + d.width + "x" + d.height +
// ", maxcolors=" + colors + "].");
// if the passed-in image is smaller, pad it with transparent pixels and use it anyway.
if (((w < d.width) && (h <= d.height)) ||
((w <= d.width) && (h < d.height))) {
Image padder = GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().getDefaultConfiguration().
createCompatibleImage(d.width, d.height, Transparency.BITMASK);
Graphics g = padder.getGraphics();
g.drawImage(img, 0, 0, null);
g.dispose();
// and reassign the image to the padded image
img = padder;
// and adjust the 'best' to cheat the hotspot checking code
d.width = w;
d.height = h;
}
// make sure the hotspot is valid
if (hotspot == null) {
hotspot = new Point(d.width / 2, d.height / 2);
} else {
hotspot.x = Math.min(d.width - 1, Math.max(0, hotspot.x));
hotspot.y = Math.min(d.height - 1, Math.max(0, hotspot.y));
}
// and create the cursor
return tk.createCustomCursor(img, hotspot, "samskivertDnDCursor");
}
/**
* Adds a one pixel border of random color to this and all panels contained in this panel's
* child hierarchy.
*/
public static void addDebugBorders (JPanel panel)
{
Color bcolor = new Color(_rando.nextInt(256), _rando.nextInt(256), _rando.nextInt(256));
panel.setBorder(BorderFactory.createLineBorder(bcolor));
for (int ii = 0; ii < panel.getComponentCount(); ii++) {
Object child = panel.getComponent(ii);
if (child instanceof JPanel) {
addDebugBorders((JPanel)child);
}
}
}
/**
* Sets the frame's icons. Unfortunately, the ability to pass multiple icons so the OS can
* choose the most size-appropriate one was added in 1.6; before that, you can only set one
* icon.
*
* This method attempts to find and use setIconImages, but if it can't, sets the frame's icon
* to the first image in the list passed in.
*/
public static void setFrameIcons (Frame frame, List extends Image> icons)
{
try {
Method m = frame.getClass().getMethod("setIconImages", List.class);
m.invoke(frame, icons);
return;
} catch (SecurityException e) {
// Fine, fine, no reflection for us
} catch (NoSuchMethodException e) {
// This is fine, we must be on a pre-1.6 JVM
} catch (Exception e) {
// Something else went awry? Log it
log.warning("Error setting frame icons", "frame", frame, "icons", icons, "e", e);
}
// We couldn't find it, couldn't reflect, or something.
// Just use whichever's at the top of the list
frame.setIconImage(icons.get(0));
}
/**
* Aligns the first rows
* cols
components of parent
in
* a grid. Each component in a column is as wide as the maximum preferred width of the
* components in that column; height is similarly determined for each row. The parent is made
* just big enough to fit them all. The components should be already added to the parent in
* row-major order.
*
* @param parent the container component; must be configured with a {@link SpringLayout} prior
* to calling this method.
* @param rows number of rows.
* @param cols number of columns.
* @param initialX x location at which to start the grid.
* @param initialY y location at which to start the grid.
* @param xPad x padding between cells.
* @param yPad y padding between cells.
*/
public static void makeCompactGrid (Container parent, int rows, int cols,
int initialX, int initialY, int xPad, int yPad)
{
SpringLayout layout = (SpringLayout)parent.getLayout();
// align all cells in each column and make them the same width
Spring x = Spring.constant(initialX);
for (int c = 0; c < cols; c++) {
Spring width = Spring.constant(0);
for (int r = 0; r < rows; r++) {
width = Spring.max(width, getConstraintsForCell(r, c, parent, cols).getWidth());
}
for (int r = 0; r < rows; r++) {
SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols);
constraints.setX(x);
constraints.setWidth(width);
}
x = Spring.sum(x, Spring.sum(width, Spring.constant(xPad)));
}
// align all cells in each row and make them the same height
Spring y = Spring.constant(initialY);
for (int r = 0; r < rows; r++) {
Spring height = Spring.constant(0);
for (int c = 0; c < cols; c++) {
height = Spring.max(height, getConstraintsForCell(r, c, parent, cols).getHeight());
}
for (int c = 0; c < cols; c++) {
SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols);
constraints.setY(y);
constraints.setHeight(height);
}
y = Spring.sum(y, Spring.sum(height, Spring.constant(yPad)));
}
// set the parent's size
SpringLayout.Constraints pcons = layout.getConstraints(parent);
pcons.setConstraint(SpringLayout.SOUTH, y);
pcons.setConstraint(SpringLayout.EAST, x);
}
/* Used by {@link #makeCompactGrid}. */
protected static SpringLayout.Constraints getConstraintsForCell (
int row, int col, Container parent, int cols)
{
SpringLayout layout = (SpringLayout)parent.getLayout();
Component c = parent.getComponent(row * cols + col);
return layout.getConstraints(c);
}
/** Used by {@link #addDebugBorders}. */
protected static final Random _rando = new Random();
/** Used by {@link #getDefaultTextAntialiasing}. */
protected static boolean _defaultTextAntialiasing;
static {
try {
_defaultTextAntialiasing = Boolean.getBoolean("swing.aatext");
} catch (Exception e) {
// security exception due to running in a sandbox, no problem
}
}
}