MIASCT

March 31, 2009

External Collapsible Panel

Filed under: Swing — Tags: , , — Devon @ 10:39 pm

I’ve been playing around for a while with different ways of displaying collapsible information panels in MIASCT.  I wanted a way of keeping the instrument views clean and simple but allowing users to display additional context information without cluttering up the main view.

At first, I looked at using a JXCollapsiblePane from the SwingX library (since I’m already using many of their components).  But this didn’t quite work the way I was hoping.  If your main content panel is inside a scrollable pane, the expanded pane will shrink the viewport on your main panel, which is reasonable.  Unfortunately, if your main content panel is not in a scroll pane, it gets pushed out of the view.  One way around this issue that I tried was to  listen for resize events on the collapsible pane and resize the frame on each event.  While this did achieve the effect I was hoping for, it made the animation pretty choppy.

Quicksilver (one of my favorite applications) has a really cool hidden information panel component in their configuration preferences (shown below).  When selected, the information panel slides out from underneath the main frame.  The panel is external to the main frame, but is anchored to it.

Quicksilver Collapsible Pane

So, I decided to try and re-create a Swing version of this component, a CollapsibleAttachedPanel.  Here’s a screenshot of the attached panel component:

Collapsible Attached Panel

And here’s a video of the attached panel in action:

Since MIASCT supports both MDI and SDI modes, the attached panel works with both JFrames and JInternalFrames.  Creating a collapsible attached panel is as simple as:

CollapsibleAttachedPanel panel = new CollapsibleAttachedPanel(“Panel Name”, parentFrame, Orientation.RIGHT);

The panels constructor takes 3 arguments:

  • The panel name
  • The parent frame (can be a JInternalFrame or a JFrame). This is the frame that the panel will attach to
  • Orientation (LEFT, RIGHT, TOP, or BOTTOM). This tells the panel what side of the parent frame to attach to

The attached panel is just a subclass of JXPanel.  When an attached panel is created, internally it creates an undecorated parent frame (JFrame or JInternalFrame) so that it’s displayable outside of the frame it’s attached to.  To anchor itself to the other frame, the panel listens for component and window events to relocate itself when necessary.

When attached to a JFrame, here’s the code used to calculate the panels location:

Point p = parentFrame.getLocationOnScreen();
int x = 0;
int y = 0;

// Calculate the new x,y coordinates for this frame based on the parents location
switch (orientation)
{
	case TOP:
		x = p.x;
		y = p.y - getHeight();
		break;

	case BOTTOM:
		x = p.x;
		y = p.y + parentFrame.getHeight();
		break;

	case LEFT:
		x = p.x - getWidth();
		y = parentFrame.getContentPane().getLocationOnScreen().y;
		break;

	case RIGHT:
		x = p.x + parentFrame.getWidth();
		y = parentFrame.getContentPane().getLocationOnScreen().y;
		break;
}

// set the location of this frame
setLocation(x, y);

The expand and collapse animations use shape/mask clipping and frame positioning to simulate sliding in and out from underneath the frame they’re attached to.  The animations are mostly controlled through PropertySetters using Chet Haase’s Timing Framework.  The following code shows an example of how the opening animation is setup for a panel that slides up from underneath a frame:


// Create a y offset setter for the panel
keyFrames = new KeyFrames(KeyValues.create(getPreferredSize().height, 0));
propSetter = new PropertySetter(this, "yOffset", keyFrames);
animator.addTarget(propSetter);

// Create a height setter for the shape mask
keyFrames = new KeyFrames(KeyValues.create(1, getPreferredSize().height));
propSetter = new PropertySetter(this, "maskHeight", keyFrames);
animator.addTarget(propSetter);

One of the interesting things you’ll notice about this code is that the height of the shape starts at “1” instead of “0”.  This is because I am using JNA‘s WindowUtils class for shape clipping on JFrames and passing a shape with a width or height of 0 will cause the clipping to be ignored and the whole window will be rendered.  Here is the code for setting a clip mask on a JFrame using WindowUtils:


WindowUtils.setWindowMask(frame, mask);

For JInternalFrames, clipping the external panel during animation is as simple as overriding the paint method and setting a graphics clip:


@Override
public void paint(Graphics inG)
{
    Shape clip = inG.getClip();

    // Set the mask as a clip
    if (mask != null)
    {
        inG.setClip(mask);
    }
    super.paint(inG);

    inG.setClip(clip);
}

Create a free website or blog at WordPress.com.