Preamble: the default frame positioning with Swing JFrames has always bothered me. Eventually I wrote some code to calculate my available desktop space, not counting my abnormally-large right-side-anchored taskbar, and use that information to size and position my JFrames. Then I got sick of copy/pasting the same frame size calculations into every Main.java of every pet project I make. So, I spent the better part of today writing a one-class library to centralize the functionality. I'm just doing this for practice and my own twisted sense of hard-working laziness, but it is complete and functioning code.
What I would like feedback on: the usual best practices for Java and Swing, naming of the class/methods/parameters of the public interface, and if it's not too much trouble I would also like feedback on the Javadoc I've written, since I've never written this level of documentation for professional projects.
I published a PDF of the javadoc and uploaded it so it's not necessary to read through the markup for that. It will be available here for one week.
The first thing I expect to be criticized is the name—I'm unhappy with TlcSizer, which was based on the initialism for top-level container, but FrameSizer was too specific and I'm not sure what would be both clear and concise.
TlcSizer.java:
package tlcsizer;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
/**
* {@code TlcSizer} is a utility class for Swing user interfaces to determine
* top-level component bounds based on screen size and available screen space.
* <p>This class is designed so that you can acquire an instance prior to
* initializing your interface, which minimizes the impact of changing the
* {@link LookAndFeel}. By aquiring a sizer instance and using only
* instance methods, you can avoid the implementation details of
* look and feel changes.
* <p>The static methods are made available for finer control over this
* process, though it is usually unnecessary to use them.
* <p><b>Note:</b> this class does not implement support for multi-screen
* environments.
*/
public final class TlcSizer {
private final Rectangle mwb;
/**
* Creates a new FrameSizer. The current {@link LookAndFeel} will be
* altered to account for objects in the native windowing system such as
* task bars and menu bars in calculating the available screen space. After
* performing this calculation, the look and feel will be reset to
* the previous look and feel.
* <p>The {@code FrameSizer} class is designed so that you can acquire an
* instance prior to initializing your interface, which minimizes the impact
* of changing the look and feel. This constructor is best used
* before client code sets the look and feel.
* <p><b>Warning:</b> this constructor alters the current
* look and feel. This will cause strange behaviour in your
* application if your interface has already been initialized. If this is
* the case, you must call {@link
* javax.swing.SwingUtilities#updateComponentTreeUI(java.awt.Component)}
* once for each top-level container. It is recommended that you
* re-{@link Window#pack() pack} your top-level container after this
* operation.
* @throws tlcsizer.TlcSizer.TlcSizerException if there was a
* problem setting the look and feel
*/
public TlcSizer() throws TlcSizerException {
this.mwb = getMaxBounds(true);
}
/**
* Creates a new {@code FrameSizer}, setting the look and feel as
* specified. This constructor needs to alter the current
* look and feel to account for objects in the native windowing
* system such as task bars and menu bars in calculating the available
* screen space. After performing this calculation, the specified
* look and feel will be set.
* <p>The {@code FrameSizer} class is designed so that you can acquire an
* instance prior to initializing your interface, which minimizes the impact
* of changing the look and feel. This constructor is best used
* as a replacement for setting the look and feel in client code.
* <p><b>Warning:</b> this constructor alters the current
* look and feel. This will cause strange behaviour in your
* application if your interface has already been initialized. If this is
* the case, you must call {@link
* javax.swing.SwingUtilities#updateComponentTreeUI(java.awt.Component)}
* once for each top-level container. It is recommended that you
* re-{@link Window#pack() pack} your top-level container after this
* operation.
* @param lookAndFeel the name of the {@link LookAndFeel} to set
* @throws tlcsizer.TlcSizer.TlcSizerException if there was a
* problem setting the look and feel
*/
public TlcSizer(String lookAndFeel) throws TlcSizerException {
this.mwb = getMaxBounds(false);
try {
UIManager.setLookAndFeel(lookAndFeel);
} catch (ClassNotFoundException | InstantiationException |
IllegalAccessException | UnsupportedLookAndFeelException ex) {
throw new TlcSizerException(ex);
}
}
/**
* Creates a FrameSizer which relies upon a given maximum window bounds,
* rather than calculating it internally. This eliminates the usual need for
* a FrameSizer to change the current look and feel to the
* system's native look and feel to calculate the bounds itself.
* @param maximumWindowBounds the externally-provided maximum window bounds
*/
public TlcSizer(Rectangle maximumWindowBounds) {
this.mwb = maximumWindowBounds;
}
/**
* Calculates the bounds for a top-level container of a given size such that
* the top-level container is centred <i>on the screen</i> as best as
* possible. The size and position are constrained to fit within available
* space, which excludes objects in the native windowing system such as
* task bars and menu bars.
* @param preferredWindowSize The preferred size of the top-level container.
* Often, this is best obtained by {@link java.awt.Window#pack() packing}
* it and then calling {@link java.awt.Window#getPreferredSize()}.
* @return the bounds calculated for the given top-level container size
* @see #getSpaceCentredBounds(java.awt.Dimension)
* @see #getScreenCentredBounds(java.awt.Dimension, java.awt.Rectangle)
* @see GraphicsEnvironment#getMaximumWindowBounds()
*/
public Rectangle getScreenCentredBounds(Dimension preferredWindowSize) {
return getCentredBounds(preferredWindowSize, mwb,
ReferenceRegion.SCREEN);
}
/**
* Convenience method for setting the bounds of a window after its
* preferred, centred bounds have been determined. The window will be
* packed, which comes with the side-effect of setting it displayable.
* @param window the window whose bounds are to be set
* @return {@code true} if the window's bounds were changed as a result of
* this operation
* @see #getScreenCentredBounds(java.awt.Dimension)
* @see Window#pack()
*/
public boolean setScreenCentredBounds(Window window) {
return setBounds(window, mwb, ReferenceRegion.SCREEN);
}
/**
* Calculates the bounds for a top-level container of a given size such that
* the top-level container is centred <i>on the screen</i> as best as
* possible. The size and position are constrained to fit within available
* space, which excludes objects in the native windowing system such as
* task bars and menu bars.
* @param preferredWindowSize The preferred size of the top-level container.
* Often, this is best obtained by {@link java.awt.Window#pack() packing}
* it and then calling {@link java.awt.Window#getPreferredSize()}.
* @param maximumWindowBounds The maximum window bounds for the screen,
* usually obtained from {@link #getMaxBounds(boolean)}.
* @return the bounds calculated for the given top-level container size
* @see #getSpaceCentredBounds(java.awt.Dimension, java.awt.Rectangle)
* @see #getScreenCentredBounds(java.awt.Dimension)
* @see GraphicsEnvironment#getMaximumWindowBounds()
*/
public static Rectangle getScreenCentredBounds(
Dimension preferredWindowSize, Rectangle maximumWindowBounds) {
return getCentredBounds(preferredWindowSize, maximumWindowBounds,
ReferenceRegion.SCREEN);
}
/**
* Convenience method for setting the bounds of a window after its
* preferred, centred bounds have been determined. The window will be
* packed, which comes with the side-effect of setting it displayable.
* @param window the window whose bounds are to be set
* @param maximumWindowBounds The maximum window bounds for the screen,
* usually obtained from {@link #getMaxBounds(boolean)}.
* @return {@code true} if the window's bounds were changed as a result of
* this operation
* @see #getScreenCentredBounds(java.awt.Dimension)
* @see Window#pack()
*/
public static boolean setScreenCentredBounds(Window window,
Rectangle maximumWindowBounds) {
return setBounds(window, maximumWindowBounds, ReferenceRegion.SCREEN);
}
/**
* Calculates the bounds for a top-level container of a given size such that
* the top-level container is centred <i>within the available space</i> as
* best as possible. These bounds account for objects in the native
* windowing system such as task bars and menu bars.
* @param preferredWindowSize The preferred size of the top-level container.
* Often, this is best obtained by {@link java.awt.Window#pack() packing}
* it and then calling {@link java.awt.Window#getPreferredSize()}.
* @return the bounds calculated for the given top-level container size
* @see #getScreenCentredBounds(java.awt.Dimension)
* @see #getSpaceCentredBounds(java.awt.Dimension, java.awt.Rectangle)
* @see GraphicsEnvironment#getMaximumWindowBounds()
*/
public Rectangle getSpaceCentredBounds(Dimension preferredWindowSize) {
return getCentredBounds(preferredWindowSize, mwb,
ReferenceRegion.AVAILABLE_SPACE);
}
/**
* Convenience method for setting the bounds of a window after its
* preferred, centred bounds have been determined. The window will be
* packed, which comes with the side-effect of setting it displayable.
* @param window the window whose bounds are to be set
* @return {@code true} if the window's bounds were changed as a result of
* this operation
* @see #getSpaceCentredBounds(java.awt.Dimension)
* @see Window#pack()
*/
public boolean setSpaceCentredBounds(Window window) {
return setBounds(window, mwb, ReferenceRegion.AVAILABLE_SPACE);
}
/**
* Calculates the bounds for a top-level container of a given size such that
* the top-level container is centred <i>within the available space</i> as
* best as possible. These bounds account for objects in the native
* windowing system such as task bars and menu bars.
* @param preferredWindowSize The preferred size of the top-level container.
* Often, this is best obtained by {@link java.awt.Window#pack() packing}
* it and then calling {@link java.awt.Window#getPreferredSize()}.
* @param maximumWindowBounds The maximum window bounds for the screen,
* usually obtained from {@link #getMaxBounds(boolean)}.
* @return the bounds calculated for the given top-level container size
* @see #getScreenCentredBounds(java.awt.Dimension, java.awt.Rectangle)
* @see #getSpaceCentredBounds(java.awt.Dimension)
*/
public static Rectangle getSpaceCentredBounds(
Dimension preferredWindowSize, Rectangle maximumWindowBounds) {
return getCentredBounds(preferredWindowSize, maximumWindowBounds,
ReferenceRegion.AVAILABLE_SPACE);
}
/**
* Convenience method for setting the bounds of a window after its
* preferred, centred bounds have been determined. The window will be
* packed, which comes with the side-effect of setting it displayable.
* @param window the window whose bounds are to be set
* @param maximumWindowBounds The maximum window bounds for the screen,
* usually obtained from {@link #getMaxBounds(boolean)}.
* @return {@code true} if the window's bounds were changed as a result of
* this operation
* @see #getSpaceCentredBounds(java.awt.Dimension)
* @see Window#pack()
*/
public static boolean setSpaceCentredBounds(Window window,
Rectangle maximumWindowBounds) {
return setBounds(window, maximumWindowBounds,
ReferenceRegion.AVAILABLE_SPACE);
}
private static boolean setBounds(Window window, Rectangle mwb,
ReferenceRegion region) {
window.pack();
final Rectangle newBounds = getCentredBounds(window.getPreferredSize(),
mwb, region);
final boolean wasTlcModified = newBounds.equals(window.getBounds());
if (wasTlcModified) {
window.setBounds(newBounds);
}
return wasTlcModified;
}
private static Rectangle getCentredBounds(Dimension preferredSize,
Rectangle mwb, ReferenceRegion region) {
final Rectangle newBounds;
final Dimension dFrame = new Dimension(
Math.min(mwb.width, preferredSize.width),
Math.min(mwb.height, preferredSize.height));
final Point pFrame = new Point();
final int x;
final int y;
switch (region) {
case SCREEN:
final Dimension screen =
java.awt.Toolkit.getDefaultToolkit().getScreenSize();
// calculate ideal position
x = (screen.width - dFrame.width) / 2;
y = (screen.height - dFrame.height) / 2;
// constrain position to avoid clipping OS desktop decorations
pFrame.x = constrain(x, mwb.x, mwb.x + mwb.width);
pFrame.y = constrain(y, mwb.y, mwb.y + mwb.height);
break;
case AVAILABLE_SPACE:
x = mwb.x + (mwb.width - dFrame.width) / 2;
y = mwb.y + (mwb.height - dFrame.height) / 2;
// unnecessary to constrain since we used mwb for ideal position
assert x == constrain(x, mwb.x, mwb.x + mwb.width);
assert y == constrain(y, mwb.y, mwb.y + mwb.height);
break;
default:
throw new RuntimeException("switch enum hit default case");
}
return new Rectangle(pFrame, dFrame);
}
private static int constrain(int value, int min, int max) {
if (max < min) throw new ArithmeticException();
value = Math.min(value, max);
value = Math.max(value, min);
return value;
}
/**
* This method obtains the maximum window bounds from the local
* {@link GraphicsEnvironment} via
* {@link GraphicsEnvironment#getMaximumWindowBounds()
* getMaximumWindowBounds()}. This method changes the
* look and feel to the native system look and feel,
* which is necessary for an accurate result.
* @param changeLnfBackAfterwards whether to change the
* look and feel back to the previously set
* look and feel after the maximum bounds have been obtained.
* @return the maximum bounds
* @throws tlcsizer.TlcSizer.TlcSizerException if there was a
* problem setting the look and feel
*/
public static Rectangle getMaxBounds(boolean changeLnfBackAfterwards)
throws TlcSizerException {
LookAndFeel originalLnf = UIManager.getLookAndFeel();
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException |
IllegalAccessException | UnsupportedLookAndFeelException ex) {
throw new TlcSizerException(ex);
}
// Needs to be done with the platform-dependent look and feel active
Rectangle mwb = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getMaximumWindowBounds();
if (changeLnfBackAfterwards) {
try {
UIManager.setLookAndFeel(originalLnf);
} catch (UnsupportedLookAndFeelException ex) {
throw new TlcSizerException(ex);
}
}
return mwb;
}
private static enum ReferenceRegion { SCREEN, AVAILABLE_SPACE }
/**
* Thrown when a {@link TlcSizer} encounters an exception when attempting
* to set the look and feel. The cause is set to the original
* exception encountered, which may be any of those thrown by
* {@link UIManager#setLookAndFeel(java.lang.String)}.
* @see Exception#getCause()
*/
public final static class TlcSizerException extends Exception {
private TlcSizerException(Throwable cause) { super(cause); }
}
}
publicmethods, it will be nicer for developers/maintainers if you do the same for yourprivateones too. :) \$\endgroup\$