ORIGINAL DRAFT

This month’s Visual Components installment extends the JDesktopPane and JInternalFrame paradigm. We’ll implement a JDesktop component that, using an extended DesktopManager, allows you to optionally constrain windows to the viewing area, so that they cannot be moved or resized off the edge of your desktop. Our implementation also provides the ability to roll windows up and down. This is behavior commonly used in tool palettes, allowing the user to expose only the title bar when the palette is not in use. Finally, we’ll support three common MDI (Multiple Document Interface) operations, making it easy to cascade and tile windows vertically and horizontally on-demand.

Figure 1: JDesktop in Action.

Figure 1: JDesktop in Action.

Figure 1 shows JDesktop in action. You can download the code for this article at www.java-pro.com. When you run the JDesktopTest class, you see the same layout. The Window menu allows you to Cascade, Tile vertically or Tile horizontally, so you can see the windows rearrange themselves. You’ll notice that only the numbered windows and the Constrained windows are rearranged. This is because the cascading and tiling methods deal only with windows on the default layer, keeping windows on other layers untouched. Iconified and maximized windows are normalized before cascading or tiling.

You may have noticed that the window labeled “Constrained” touches the bottom left edges of the desktop. You can move it anywhere around the desktop but when you reach the edges, the window will never be clipped. This is also true if you try to resize it. In contrast, the numbered windows can be partially obscured if you move them off the desktop edge. The constrained feature can be applied to any window, by setting a property. To make it easy to do this, we implement a JDesktopFrame that provides accessors and constructor options to control your windows. JDesktopFrame extends JInternalFrame is exposes the same constructors for backward compatibility.

The last thing to note in Figure 1 is the “Calculator” and “Toolbox” windows. These exist on the palette layer and are excluded from any cascading or tiling activities. The toolbox window is rolled up, which means that pressing the deiconify button will roll it back out to it’s normal shape. Effectively, we are replacing the iconify and deiconify behaviors to expose the title bar when the window is iconified. The window is not actually an icon in this state and can still be moved anywhere on the desktop. Keep in mind that these palettes are just examples and the Calculator is not actually functional.

Let’s take a look at our implementation. There are three important classes to look at. JDesktop extends JDesktopPane and implements the cascading and tiling methods. It automatically replaces the DefaultDesktopManager with our ExtendedDesktopManager.

The ExtendedDesktopManager extends DefaultDesktopManager, so all the old behaviors are inherited. New behaviors for constraining windows and implementing rollups are activated only when windows have appropriate properties. While you can set these properties directly in your windows, we’ll implement a JDesktopFrame that extends JInternalFrame and adds suitable accessors and constructor options.

Listing 1 shows the JDesktopFrame. Most of the code up front is for backward compatibility, implementing each of the constructor variants available in JInternalFrame. The last two constructors provide added arguments to optionally set the constrained flag and the rollup behavior. Since it’s more likely the constraint flag will be used on a typical window, it gets listed first and a constructor without the rollup option is made available, consistent with the pattern established for existing JInternalFrame parameters.

There are two protected methods below the constructors that make it easy to set and get boolean properties. Since properties may be missing entirely, the getBooleanProperty method returns false if the property has never been set. It also returns false if the property is not the of expected Boolean data type, protecting us form unexpected mishaps. Unfortunately, it’s not possible to ensure that no code will ever use the same property name, so if the rollup and/or constrain features don’t work, you may have to do some digging. I’ve put the property names in an interface called DesktopConstants, so you can always change the names if you run into conflicts.

The rest of the JDesktopFrame code is straight forward, providing setConstrained and setRollable methods so that you can change the state of each property anytime. You can get the current state of each by calling isConstrained and isRollable. In each case, the methods call the setBooleanProperty or getBooleanProperty methods with a suitable property argument.

Listing 2 shows the ExtendedDesktopManager code, which extends the DefaultDesktopManager, which in turn implements the DesktopManager interface. There are 15 methods in the DesktopManager interface, all of which are implemented by the DefaultDesktopManager. We’re interested in only four of them. In particular, the constraint behavior requires modifications to the dragFrame and resizeFrame methods. The rollup behavior requires extensions to the iconifyFrame and deiconifyFrame methods.

Positional constraints are handled in the dragFrame method. If the CONSTRAINED property is set, we make sure that the x and y values are never below zero and that the width and height never push the window beyond the bottom right of the desktop. In any of these cases, the x and y values are adjusted to avoid the problem. You’ll notice a piece of code that tests to ensure the window is not maximized and then resets the location to the normal window bounds x and y values. The code is there to handle rolled up window movement. If you don’t do this, you end up with an expanded window at the old location when you deiconify after moving the smaller window.

The resizeFrame method does a little more work but also includes code for checking for position constraints. We check for the CONSTRAINED property and adjusts the x, y, w and h values if the user tries to resize beyond the edges of the desktop. Notice that the size constraint are checked first and then the position constraints. These conditional operations could easily have been grouped together, but I found the code more readable when each test was on a single line.

Handling the rollup behavior is fairly simple. Both the iconifyFrame and deiconifyFrame methods call the superclass equivalents of the ROLLABLE property is not set. Otherwise, iconifyFrame figures out the size of the title bar by subtracting the ContentPane height from the normal window bounds. There are other ways of doing this but this approach is safer that trying to get a reference to the tile bar object in the plugable look-and-feel. You’ll notice that we set the normal bounds before iconifying. The deiconifyFrame method uses this information to restore the window to its original size.

Listing 3 shows the code for JDesktop. The constructor sets the ExtendedDesktopManager. The methods of primary interest are cascade, tileVertical and tileHorizontal, but lets cover the getResetFrames and resetFrame protected methods first. The resetFrame method verifies if a frame is maximized or iconified and resets it to normal. This is straight forward with a maximized window, but iconified windows are not the same object as their normal window, so we want to get the correct reference by calling getInternalFrame on the JDesktopIcon.

We’ll use getResetFrames instead of the getComponentsInLayer method because we want the list of frames to be a set of normalized windows. The getResetFrames method calls getComponentsInLayer, then applies resetFrame to each window and refetches the list with another call to getComponentsInLayer. This is important if iconified windows were involved because they are changing their position and size has no impact on the window you want to rearrange. After the first pass, all iconified and maximized windows are normalize, so the second pass gets the actual windows we want to manipulate.

Let’s cover the cascade methods first. I’ve provided two variants. The first takes no arguments and assumes a default initial window size and offsets. The second lets you specify these parameters yourself. Since its likely you may want to reuse the Rectangle passed in as an argument, we clone it first and modify the clone. If we didn’t do this, you might end up with a rectangle, which is passed by reference, with unexpectedly modified coordinates. It’s easy enough to be write defensively. The cascade method merely increments the x and y values of the rectangle by the specified offsets and resets the window bounds.

Tiling is only slightly more interesting. The optimal tiling pattern is a square of equal width and height, but this is fairly unlikely. We start with that assumption and then increment the rows and/or cols values depending on how many of the windows are accounted for. With vertical tiling, we adjust the rows first. We adjust the cols first if we’re tiling horizontally. Unless we are dealing with a perfect square, we end up with a row and column count that is as close to what we need as possible, with a little room to spare.

By iterating through each row and column (with differing priorities depending on whether we are tiling horizontally or vertically), we can lay out each window in sequence as a tile. The tile size is based on the number of rows or columns divided by the height or width of the desktop. On the last row or column, we reset the number of columns or rows (based on which ever has priority) and lay the last line down with equal divisions. The last line may not match the rest of our tiles but the result will fully cover the desktop in a manner that users have come to expect from this tiling behavior.

When I first developed this code, I’d intended to support docking behavior, but that turned out to involve too much code for a single article, so you can expect that material to be covered separately in a future installment. JDesktop is useful on three fronts. You can easily create rollup palettes, constrain windows to the desktop where appropriate and you get the cascading and tiling behavior you need for professional MDI applications. Here’s hoping it serves you well.

Listing 1

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.basic.*;

public class JDesktopFrame
  extends JInternalFrame
  implements DesktopConstants
{
  public JDesktopFrame(String title)
  {
    this(title, false, false, false, false,
      false, false);
  }
  
  public JDesktopFrame(String title, boolean resizable)
  {
    this(title, resizable, false, false, false,
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable, boolean closable)
  {
    this(title, resizable, closable, false, false, 
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable)
  {
    this(title, resizable, closable, maximizable, false, 
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable)
  {
    this(title, resizable, closable, maximizable, 
      iconifiable, false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable,
    boolean constrained)
  {
    this(title, resizable, closable, 
      maximizable, iconifiable,
      constrained, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable,
    boolean constrained,
    boolean rollable)
  {
    super(title, resizable, closable, maximizable, iconifiable);
    setConstrained(constrained);
    setRollable(rollable);
  }
  
  protected void setBooleanProperty(String name, boolean value)
  {
    putClientProperty(name, value ? Boolean.TRUE : Boolean.FALSE);
  }
  
  protected boolean getBooleanProperty(String name)
  {
    Object obj = getClientProperty(name);
    if (obj == null) return false;
    if (obj instanceof Boolean)
      return ((Boolean)obj).booleanValue();
    else return false;
  }
  
  public void setConstrained(boolean constrained)
  {
    setBooleanProperty(CONSTRAINED, constrained);
  }
  
  public boolean isConstrained()
  {
    return getBooleanProperty(CONSTRAINED);
  }

  public void setRollable(boolean rollable)
  {
    setBooleanProperty(ROLLABLE, rollable);
  }
  
  public boolean isRollable()
  {
    return getBooleanProperty(ROLLABLE);
  }
}

Listing 2

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.basic.*;

public class JDesktopFrame
  extends JInternalFrame
  implements DesktopConstants
{
  public JDesktopFrame(String title)
  {
    this(title, false, false, false, false,
      false, false);
  }
  
  public JDesktopFrame(String title, boolean resizable)
  {
    this(title, resizable, false, false, false,
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable, boolean closable)
  {
    this(title, resizable, closable, false, false, 
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable)
  {
    this(title, resizable, closable, maximizable, false, 
      false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable)
  {
    this(title, resizable, closable, maximizable, 
      iconifiable, false, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable,
    boolean constrained)
  {
    this(title, resizable, closable, 
      maximizable, iconifiable,
      constrained, false);
  }
  
  public JDesktopFrame(String title,
    boolean resizable,
    boolean closable,
    boolean maximizable,
    boolean iconifiable,
    boolean constrained,
    boolean rollable)
  {
    super(title, resizable, closable, maximizable, iconifiable);
    setConstrained(constrained);
    setRollable(rollable);
  }
  
  protected void setBooleanProperty(String name, boolean value)
  {
    putClientProperty(name, value ? Boolean.TRUE : Boolean.FALSE);
  }
  
  protected boolean getBooleanProperty(String name)
  {
    Object obj = getClientProperty(name);
    if (obj == null) return false;
    if (obj instanceof Boolean)
      return ((Boolean)obj).booleanValue();
    else return false;
  }
  
  public void setConstrained(boolean constrained)
  {
    setBooleanProperty(CONSTRAINED, constrained);
  }
  
  public boolean isConstrained()
  {
    return getBooleanProperty(CONSTRAINED);
  }

  public void setRollable(boolean rollable)
  {
    setBooleanProperty(ROLLABLE, rollable);
  }
  
  public boolean isRollable()
  {
    return getBooleanProperty(ROLLABLE);
  }
}

Listing 3

import java.awt.*;
import java.beans.*;
import javax.swing.*;

public class JDesktop extends JDesktopPane
{
  public JDesktop()
  {
    setDesktopManager(new ExtendedDesktopManager());
  }
  
  public void cascade()
  {
    cascade(
      new Rectangle(5, 5, 300, 150),
      new Dimension(10, 10));
  }

  public void cascade(
    Rectangle firstWindowBounds,
    Dimension offsets)
  {
    Rectangle bounds = (Rectangle)firstWindowBounds.clone();
    Component[] list = getResetFrames(0);
    for (int i = 0; i < list.length; i++)
    {
      list[i].setBounds(bounds);
      bounds.x += offsets.width;
      bounds.y += offsets.height;
    }
  }

  public void tileVertical()
  {
    Component[] list = getResetFrames(0);
    int count = list.length;
    int cols = (int)Math.sqrt(count);
    int rows = cols;
    if (rows * cols < count) rows++;
    if (rows * cols < count) cols++;
    int w = getSize().width / cols;
    int h = getSize().height / rows;
    int i = 0;
    for (int x = 0; x < cols; x++)
    {
      if (x == cols - 1)
      {
        rows = count - rows * (cols - 1);
        h = getSize().height / rows;
      }
      for (int y = 0; y < rows; y++)
      {
        list[i].setBounds(x * w, y * h, w, h);
        i++;
      }
    }
  }
  
  public void tileHorizontal()
  {
    Component[] list = getResetFrames(0);
    int count = list.length;
    int cols = (int)Math.sqrt(count);
    int rows = cols;
    if (rows * cols < count) cols++;
    if (rows * cols < count) rows++;
    int w = getSize().width / cols;
    int h = getSize().height / rows;
    int i = 0;
    for (int y = 0; y < rows; y++)
    {
      if (y == rows - 1)
      {
        cols = count - cols * (rows - 1);
        w = getSize().width / cols;
      }
      for (int x = 0; x < cols; x++)
      {
        list[i].setBounds(x * w, y * h, w, h);
        i++;
      }
    }
  }

  protected Component[] getResetFrames(int layer)
  {
    Component[] list = getComponentsInLayer(layer);
    for (int i = 0; i < list.length; i++)
    {
      resetFrame(list[i]);
    }
    // Re-fetch to keep the correct order
    return getComponentsInLayer(layer);
  }
  
  protected void resetFrame(Component component)
  {
    // Demaximize
    if (component instanceof JInternalFrame)
    {
      JInternalFrame frame = (JInternalFrame)component;
      try
      {
        if (frame.isMaximum())
          frame.setMaximum(false);
      }
      catch (PropertyVetoException e) {}
    }
    // Deiconify
    if (component instanceof JInternalFrame.JDesktopIcon)
    {
      JInternalFrame frame = 
        ((JInternalFrame.JDesktopIcon)component).
          getInternalFrame();
      try
      {
        frame.setIcon(false);
      }
      catch (PropertyVetoException e) {}
    }
  }
}