ORIGINAL DRAFT

Sometimes the best of intentions can lead you straight into a new adventure. Such was the case this month when I set out to develop an icon editor and found myself in need of a palette-based color selector along the way. As the sculptor visualizes the statue when his chisel first hits stone, the JPallete concept jumped wholly formed from the icon editor project and, before I knew it, took over the focus of this installment. Oh, well. Why get in the way of a good thing?

Figure 1: JPalette at work.

Figure 1: JPalette at work.

Figure 1 shows what JPalette can do. The color swatch and pulldown menu are there for testing and demonstration purposes. The JPalette is displayed inside a JSlidePane, which we’ll also develop in this article. JSlidePane is capable of handling vertical or horizontal component scrolling and optionally allows the automatic hiding of arrow buttons. In this case, the JPalette displays a portion of a 216 color Web Palette. JPalette supports mouse selection, automatically scrolls when keyboard navigation moves off the visible edge and works with custom models and renderers. You can also control the number of cells to paint on a side, so it can work with resizable toolbars in your application.

The JPalette component reads and writes Windows PAL (palette) files. I would have liked to support multiple formats but, to be honest, this is the only one I was able to find documentation for. There are a couple of good graphics file format books kicking around and plenty of web sites that cover raster, vector and three dimensional file formats, but none that provided enough information on any of the pure palette files. Adobe has ACO (Adobe Colors) and ACT (Adobe Color Table) files which would have been nice to support.

The PAL format is based on the Resource Interchange File Format (RIFF). It’s fairly simple and forms the foundation for a variety of formats, including WAV, AVI and other multimedia files. We won’t spend any time looking at file-related code, given our focus on visual components, but I’ll give you a quick explanation of how the PAL format works. You can find full source code online at www.java-pro.com.

The RIFF format is based on nested chunks of data with a name and size element preceding the data. Data chunks tend to have specialized information. PAL files have header information that fit this general pattern and a collection of 4 byte values that represent the red, green and blue color elements, and a flag byte, the purpose of which is not clear (probably related to alpha values but effectively undocumented). This value seems always set to zero in any case. I’ve implemented a PALInputStream and PALOutputStream to abstract header and color information and wrapped high-level load and save functionality in a PALPaletteFile class which implements the PaletteFile interface and operates on a PaletteModel class.

Figure 2: JPalette Classes.

Figure 2: JPalette Classes.

Figure 2 shows the class relationships for the JPalette component. The JSlidePane is independent and can be used anywhere. The rest of the classes provide support for modeling, drawing and file input/output. As you can see, we’ve declared interfaces to make sure we can use alternate models and views, and can plug in other file formats.

Interfaces

Ensuring flexibility is one of our primary concerns, since our needs for this widget may vary between applications. We’ll make it possible to swap out both the model and cell renderers, as well as the file handling functionality. To do that, we need three interfaces, so let’s take a quick look at them before implementing the default classes for each.

The PaletteModel allows us to store color collections and to operate on the colors. My first design used the IndexColorModel from the java.awt.image package but I ran into performance problems creating up to 256 Color objects every time the palette needed to be drawn. Unfortunately, the Graphics object does not have a setColor variant that accepts RGB int values. The solution is to base the model on a collection of Color objects instead, so that integer to Color translation doesn’t have to happen on-the-fly. We need to support getting the palette size, getting a specific color by index, replacing a color by index position and adding a color to the palette.

public interface PaletteModel
{
  public int getPaletteSize();
  public Color getColor(int index);
  public void setColor(int index, Color color);
  public void addColor(Color color);
}

The PaletteCellRenderer is responsible for drawing each of the cells in the palette display. We’ll use a few optimizations to keep the speed at maximum and base our JPalette dimensions on the renderer’s preferred size. This approach allows us to create different shaped renderers to accommodate differing display areas. To render a cell, we need to pass along a Color and an indication of whether the cell is selected and/or has the focus.

public interface PaletteCellRenderer
{
  public JComponent getPaletteCellRendererComponent(
    JPalette palette, int x, int y, Color color,
    boolean isSelected, boolean hasFocus);
}

We allow for potentially more sophisticated behavior by following the Swing renderer convention of passing a reference to the JPalette component. We’ll add the x and y coordinates in case you want to make any position-relative decisions. Our default renderer doesn’t use the JPalette reference or the coordinates to do it’s job, but it’s good to think ahead at design time.

To keep the file-handling flexible, we’ll add one more interface called PaletteFile. This one lets us exchange one file handler for another to support multiple file formats. The interface allows us to read and save a model using any file format that implements this interface. In our case, we’ll implement the PALPaletteFile handler as the default PaletteFile solution for JPalette, but you can create your own and handle custom files or accommodate new formats when they are released (or documented properly).

public interface PaletteFile
{
  public PaletteModel readModel(String filename)
    throws IOException;
  public void writeModel(String filename, PaletteModel model)
    throws IOException;
}

Implementations

Let’s develop default implementations for each of these interfaces. The DefaultPaletteModel and DefaultPaletteCellRenderer classes implement the PaletteModel and PaletteCellRenderer interfaces, respectively. The PALPaletteFile class implements the PaletteFile interface to support the PAL file format we talked about earlier.

Listing 1 shows the code for DefaultPaletteModel. There’s nothing much to it. We initialize an ArrayList from the Java Collections API and use that to implement our functionality. The getColor method casts the lookup into a Color object, using a get method call to fetch it from the array. The rest of the methods map directly onto the size, set and add methods from the List interface. I could have extended the ArrayList to do the same thing, but its actually safer to encapsulate the behavior, in effect discouraging extensions that might rely on non-interface access to the default model.

Listing 2 shows the DefaultPaletteCellRenderer class. This is another fairly simple class. The constructor ensures that the JPanel extension we use is consider opaque. The getPaletteCellRendererComponent call sets internal variables to represent the state we want to draw and the paintComponent draws the cell, based on those internal variable settings. We provide an implementation of getPreferredSize and this is important to JPalette. Make sure you define the cell dimensions this way if you write your own renderer. JPalette uses these dimensions in all its geometry calculations to avoid coupling to a specific rendering shape or size.

Listing 3 shows the source code for the PALPaletteFile class. Since we hide most of the file format-specific access in the PALInputStream and PALOutputStream classes (which you can find online), we can read the file header and then read each color into our model or write the header and each respective color to a file. Both of these are simple loops with a little setup work to manage the color count and respective allocations. The methods throw IOExceptions if we run into any file problems.

JPalette

The JPalette component ties everything together to make it easy to use palettes in your user interfaces. JPalette fires an ActionEvent when the user makes a selection, so you can register to listen for these events and get the currently selected color by calling getSelectedColor method in JPalette.

The constructor expects orientation, length, visible and editable arguments. The orientation is either VERTICAL or HORIZONTAL and defines a context for the length and visible arguments. JPalette will draw the palette with a fixed length on the side you specify in the orientation. The visible argument determines how much of the secondary orientation will be visible in a scrolling area by default and is measured in cells. The editable argument is a boolean value that sets whether color editing will be enabled. You can change these settings later by using the setGeometry and setEditable methods.

It helps to look at examples to see how the orientation, length and visible argument behave. Figure 1 uses a VERTICAL orientation, a length of 3 and a value of 16 for visible. The result is a palette that has a fixed vertical height of 3 and shows 16 horizontal cells in the scrolling area. Figure 3 uses a HORIZONTAL orientation and values of 16 for both the length and visible arguments. The resulting display is 16 by 16 and would show the whole 256 cell palette if it were placed in a scrolling area.

The visible dimension is applicable primarily to JPalette instances used with a JViewPort, JScrollPane or JSlidePane. To accomplish this, JPalette implements the Scrollable interface. The preferred view size is calculated based on the orientation, length and visible settings. We set units to the size of each cell and blocks to the visible area. We also need to be sensitive to the orientation, which is passed in as an argument in several of the Scrollable interface methods. We don’t need to constrain the view size to a wrapping width or height, so the two tracking methods return false.

Figure 3: JPalette displaying a grey 
scale. We use a Load and Save button to test respective functionality.

Figure 3: JPalette displaying a grey scale. We use a Load and Save button to test respective functionality.

Listing 4 shows the JPalette class. Aside from the constructor and setScaling method we just mentioned, we provide a set of accessor (set/get) methods for the Model, Renderer and FileHandler. The savePalette method is trivial, delegating its work to the FileHandler implementation, but loadPalette has to do more setup work to calculate the width and height from the orientation and length settings, and the palette size. We reset the current position to 0, 0 and clear the background image before repainting and firing off an action event to listeners.

The preferred size is calculated from the width and height, along with the renderer cell size. In fact, getting the renderer’s preferred size is so frequent that I implemented an internal method to make this easier. The getPreferredSize method returns the preferred size for JPalette by multiplying the cell size by the number of cells on each axis. All our dimensions are based on the renderer’s preferred size, so keep this in mind when you switch renderers to avoid unexpected results.

The paintComponent method optimizes drawing by using a background image buffer. JPalette could have been implemented using independent components in a grid but the overhead was too high when I tried this. Painting each cell by calling the renderer 256 times can be slow when you need to repaint frequently, especially when you’re moving the cursor using a keyboard. To avoid unnecessary repainting, we use a background image to draw unselected cells and only draw the selected cell on top of the background image when the cursor moves. If the palette itself changes, the background image is regenerated.

The paintComponent method uses a drawCell internal method to work with the renderer. We call getPaletteCellRendererComponent with a reference to the JPalette object (this), the current x and y positions, the color we want drawn, and flags indicating whether we are selected and/or have the focus. The color is drawn from the PaletteModel, but we use the background color if the cell is outside the palette size. This can happen when the palette size is not an exact multiple of the length argument we specified earlier.

To handle focus properly, we implement the isFocusTraversable method in JPalette and return true. We also implement the FocusListener interface and repaint when the focus changes. This is another one of those cases where the drawing optimization is necessary, but it’s handled by the paintComponent call so we don’t have to do anything special to ensure the repaint events don’t slow us down. JPalette registers itself as a FocusListener, as well as a KeyListener and MouseListener.

For keyboard events, we respond to the keyPressed method and calculate new cursor positions based on the key code, checking for boundary conditions along the way. Because several calls are required after each keystroke, I’ve used an internal method called refresh to group them together. Aside from repainting and firing an action event when the cursor moves, we also call the scrollRectToVisible method which is part of every JComponent. This makes sure that the cursor is always visible, even if it requires scrolling in a view port.

All our mouse handling takes place in the mousePressed method, which figures out where we are, relative to the cell size, and selects the cell under the mouse. If the cell is outside the palette range, nothing happens, we return immediately. If editing is enabled and the action was a double-click, we pop up a JColorChooser and allow that color to be edited. You can use the savePalette method to make changes persistent or discard them when you’re done.

The last set of methods in JPalette help us manage a set of ActionListener objects and fire off action events. We implement the addActionListener and removeActionListener methods, using an ArrayList to hold the listeners. We also implement a fireActionEvent internal method to handle walking through the list to send an action event when we need to. Notice that we clone the list to avoid concurrency problems.

JSlidePane

This Visual Component installment has a little bonus class I think you’ll like. JSlidePane provides a JScrollPane-type implementation that lets you scroll components in a single orientation. Used vertically, this is similar to something you’ll find in Microsoft Outlook bar-style interfaces, allowing shortcut icons below or above the visible display to be brought into view easily. We can use it horizontally as well, as demonstrated in Figure 1. JSlidePane uses a JViewPort and supports all the fundamental behavior you’d expect from that.

Listing 5 shows the JSlidePane class. The three constructors let you manage the component to be scrolled, the orientation and an autohide feature. The first two constructors simply provide fewer arguments with default values for the orientation and autohide. If you set autohide to true, the scroll buttons will be removed when the component is against the edge. This is the behavior you would see in an Outlook bar, for example. Autohide is off by default. The orientation is expected to be either VERTICAL or HORIZONTAL, defaulting to VERTICAL.

The main JSlidePane constructor sets up internal variables, the JViewPort, and arrow buttons. We add our JViewPort to the CENTER of a BorderLayout, in a JPanel extension, so that it covers the whole area. The buttons are added either to the NORTH and SOUTH or to the WEST and EAST, depending on the orientation. You can use any Java Component in a JSlidePane.

We’ll override the setBounds method to handle resize events. Depending on the orientation and the autohide setting, we add or remove buttons to accommodate the view. If autohide is on, for example, end buttons may not be required, depending on the component size. After adding or removing buttons, we call doLayout to force the BorderLayout to reposition child components.

The JSlidePane class implements the ActionListener interface and registers to listen for button events. When a button is pressed, the actionPerformed method decides what to do, based on which button was involved. In each case, we calculate the new position, based on the scrolling distance, and determine what the new position should be, setting it with the JViewPort setViewPosition method. The scrolling distance is set to 32 by default but we use the scrollable unit increment if the component being scrolled implements the Scrollable interface. This is the preferred approach.

JSlidePane is useful whenever you want to avoid the added real estate requirements of a complete scroll bar. While it’s not a direct replacement for JScrollPane, it can be used as a substitute in many cases. It’s particularly useful when you don’t need to support both vertical and horizontal scrolling at the same time.

We’ve implemented another useful widget, with minimal effort, and maximized our flexibility through the use of interfaces for the model, view and file handling. JPalette can load and save Windows PAL files by default and offers a practical default model and cell renderer implementation. You can extend the widget directly or through new implementations of each of the interfaces. JPalette is useful as it is, but never imposes unnecessary limitations on the way you might like to use it. I hope it serves you well on your programming adventures.

Listing 1

import java.awt.*;
import java.util.*;

public class DefaultPaletteModel
  implements PaletteModel
{
  protected ArrayList colors = new ArrayList();
  
  public int getPaletteSize()
  {
    return colors.size();
  }
  
  public Color getColor(int index)
  {
    return (Color)colors.get(index);
  }
  
  public void setColor(int index, Color color)
  {
    colors.set(index, color);
  }
  
  public void addColor(Color color)
  {
    colors.add(color);
  }
}

Listing 2

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

public class DefaultPaletteCellRenderer extends JPanel
  implements PaletteCellRenderer
{
  protected Dimension preferredSize = new Dimension(10, 10);
  protected JPalette palette;
  protected int x, y;
  protected Color color;
  protected boolean hasFocus;
  protected boolean isSelected;
  
  public DefaultPaletteCellRenderer()
  {
    setOpaque(true);
  }

  public JComponent getPaletteCellRendererComponent(
    JPalette palette, int x, int y, Color color,
    boolean isSelected, boolean hasFocus)
  {
    this.palette = palette;
    this.x = x;
    this.y = y;
    this.color = color;
    this.isSelected = isSelected;
    this.hasFocus = hasFocus;
    return this;
  }
  
  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    g.setColor(color);
    g.fillRect(0, 0, w, h);
    g.setColor(isSelected ?
      (hasFocus ? Color.black : Color.white) :
        palette.getBackground());
    g.drawRect(0, 0, w - 1, h - 1);
  }
  
  public Dimension getPreferredSize()
  {
    return preferredSize;
  }
}

Listing 3

import java.io.*;
import java.awt.*;
import java.awt.image.*;

public class PALPaletteFile implements PaletteFile
{
  public PaletteModel readModel(String filename)
    throws IOException
  {
    FileInputStream file = new FileInputStream(filename);
    PALInputStream pal = new PALInputStream(file);
    int size = pal.readSize();
    PaletteModel model = new DefaultPaletteModel();
    for (int i = 0; i < size; i++)
    {
      model.addColor(pal.readColor());
    }
    pal.close();
    file.close();
    return model;
  }

  public void writeModel(String filename, PaletteModel model)
    throws IOException
  {
    FileOutputStream file = new FileOutputStream(filename);
    PALOutputStream pal = new PALOutputStream(file);
    int size = model.getPaletteSize();
    pal.writeHeader(size);
    for (int i = 0; i < size; i++)
    {
      pal.writeColor(model.getColor(i));
    }
    pal.close();
    file.close();
  }
}

Listing 4

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;

public class JPalette extends JPanel
  implements SwingConstants, Scrollable,
    FocusListener, KeyListener, MouseListener
{
  protected int orientation, length, visible;
  protected boolean editable = true;
  protected CellRendererPane rendererPane =
    new CellRendererPane();
  protected PaletteCellRenderer renderer =
    new DefaultPaletteCellRenderer();
  protected JColorChooser chooser =
    new JColorChooser();
  protected PaletteFile fileHandler =
    new PALPaletteFile();
  protected BufferedImage background;
  protected PaletteModel model;
  protected ArrayList listeners = new ArrayList();
  protected int x = 0, y = 0;
  protected int w, h;

  public JPalette(int orientation, int length,
    int visible, boolean editable) throws IOException
  {
    addKeyListener(this);
    addFocusListener(this);
    addMouseListener(this);
    setGeometry(orientation, length, visible);
    setEditable(editable);
  }
  
  public void setGeometry(int orientation, int length, int visible)
  {
    this.orientation = orientation;
    this.length = length;
    this.visible = visible;
    background = null;
    x = 0; y = 0;
    repaint();
  }
  
  public void setEditable(boolean editable)
  {
    this.editable = editable;
  }
  
  public PaletteModel getModel()
  {
    return model;
  }
  
  public void setModel(PaletteModel model)
  {
    this.model = model;
  }
  
  public PaletteCellRenderer getRenderer()
  {
    return renderer;
  }
  
  public void setRenderer(PaletteCellRenderer renderer)
  {
    this.renderer = renderer;
    revalidate();
  }
  
  public PaletteFile getFileHandler()
  {
    return fileHandler;
  }
  
  public void setFileHandler(PaletteFile fileHandler)
  {
    this.fileHandler = fileHandler;
  }
  
  public void savePalette(String filename)
    throws IOException
  {
    fileHandler.writeModel(filename, model);
  }

  public void loadPalette(String filename) throws IOException
  {
    model = fileHandler.readModel(filename);
    int max = model.getPaletteSize();
    if (orientation == VERTICAL)
    {
      h = length;
      w = max / length;
      if (w * h < max) w++;
    }
    else
    {
      w = length;
      h = max / length;
      if (w * h < max) h++;
    }
    background = null;
    x = 0; y = 0;
    repaint();
    fireActionEvent();
  }
  
  public Color getSelectedColor()
  {
    return model.getColor(x + y * w);
  }
  
  protected Dimension getPreferredRendererSize()
  {
    return ((JComponent)renderer).getPreferredSize();
  }
  
  public Dimension getPreferredSize()
  {
    Dimension size = getPreferredRendererSize();
    return new Dimension(w * size.width, h * size.height);
  }
  
  public void paintComponent(Graphics gc)
  {
    Dimension size = getPreferredRendererSize();
    if (background == null ||
      background.getWidth() != w * size.width ||
      background.getHeight() != h * size.height)
    {
      background = new BufferedImage(
        w * size.width, h * size.height,
        BufferedImage.TYPE_INT_RGB);
      Graphics g = background.getGraphics();
      int max = model.getPaletteSize();
      for (int xx = 0; xx < w; xx++)
      {
        for (int yy = 0; yy < h ; yy++)
        {
          int i = xx + yy * w;
          drawCell(g, i, xx, yy, size, false, false);
        }
      }
    }
    gc.drawImage(background, 0, 0, this);
    drawCell(gc, x + y * w, x, y, size, true, hasFocus());
  }

  protected void drawCell(Graphics g, 
    int index, int xPos, int yPos, Dimension size,
    boolean isSelected, boolean hasFocus)
  {
    int xPix = xPos * size.width;
    int yPix = yPos * size.height;
    int max = model.getPaletteSize();
    JComponent component =
      renderer.getPaletteCellRendererComponent(
        this, xPos, yPos, index < max ?
          model.getColor(index) : getBackground(),
        isSelected, hasFocus);
    rendererPane.paintComponent(g, component,
      this, xPix, yPix, size.width, size.height);
  }	
  
  public boolean isFocusTraversable()
  {
    return true;
  }

  public void focusGained(FocusEvent event)
  {
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    repaint();
  }
  
  public void keyTyped(KeyEvent event) {}
  public void keyReleased(KeyEvent event) {}
  public void keyPressed(KeyEvent event)
  {
    int code = event.getKeyCode();
    int max = model.getPaletteSize();
    if (code == KeyEvent.VK_RIGHT &&
      x < w - 1 && (x + 1) + y * w < max)
    {
      x++;
      refresh();
    }
    if (code == KeyEvent.VK_LEFT && x > 0)
    {
      x--;
      refresh();
    }
    if (code == KeyEvent.VK_DOWN &&
      y < h - 1 && x + (y + 1) * w < max)
    {
      y++;
      refresh();
    }
    if (code == KeyEvent.VK_UP & y > 0)
    {
      y--;
      refresh();
    }
    if (code == KeyEvent.VK_PAGE_UP)
    {
      y = 0;
      refresh();
    }
    if (code == KeyEvent.VK_PAGE_DOWN)
    {
      y = h - 1;
      if (x + y * w >= max)
        y = y - 1;
      refresh();
    }
    if (code == KeyEvent.VK_HOME)
    {
      x = 0;
      refresh();
    }
    if (code == KeyEvent.VK_END)
    {
      x = w - 1;
      if (x + y * w > max)
        x = max - y * w - 1;
      refresh();
    }
    if (editable && code == KeyEvent.VK_ENTER)
    {
      showColorEditor();
    }
  }

  protected void refresh()
  {
    Dimension size = getPreferredRendererSize();
    scrollRectToVisible(new Rectangle(
      x * size.width, y * size.height,
      size.width, size.height));
    repaint();
    fireActionEvent();
  }
  
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    requestFocus();
    Dimension size = getPreferredRendererSize();
    x = event.getX() / size.width;
    y = event.getY() / size.height;
    int max = model.getPaletteSize();
    if (x + y * w >= max) return;
    if (editable && event.getClickCount() == 2)
    {
      showColorEditor();
    }
    repaint();
    fireActionEvent();
  }
  
  protected void showColorEditor()
  {
    {
      int index = x + y * w;
      Color oldColor = model.getColor(index);
      Color newColor = chooser.showDialog(
        this, "Change Palette Color", oldColor);
      if (newColor != null &&
        newColor.getRGB() != oldColor.getRGB())
      {
        model.setColor(index, newColor);
        background = null;
      }
    }
  }
  
  public int getScrollableUnitIncrement(
    Rectangle visibleRect, int orientation, int direction)
  {
    Dimension size = getPreferredRendererSize();
    if (orientation == VERTICAL)
      return size.height;
    else
      return size.width;
  }

  public int getScrollableBlockIncrement(
    Rectangle visibleRect, int orientation, int direction)
  {
    Dimension size = getPreferredRendererSize();
    if (orientation == VERTICAL)
      return size.height * 10;
    else
      return size.width * 10;
  }
  
  public Dimension getPreferredScrollableViewportSize()
  {
    int width, height;
    Dimension render = getPreferredRendererSize();
    Dimension size = getPreferredSize();
    if (orientation == VERTICAL)
    {
      width = render.width * visible;
      height = size.height;
    }
    else
    {
      width = size.width;
      height = render.height * visible;
    }
    return new Dimension(width, height);
  }
 
  public boolean getScrollableTracksViewportHeight()
  {
    return false;
  }
 
  public boolean getScrollableTracksViewportWidth()
  {
    return false;
  }
  
  public void addActionListener(ActionListener listener)
  {
    listeners.add(listener);
  }
  
  public void removeActionListener(ActionListener listener)
  {
    listeners.remove(listener);
  }
  
  protected void fireActionEvent()
  {
    ArrayList list = (ArrayList)listeners.clone();
    ActionEvent event = new ActionEvent(
      this, ActionEvent.ACTION_PERFORMED, "Palette");
    for (int i = 0; i < list.size(); i++)
    {
      ActionListener listener = (ActionListener)list.get(i);
      listener.actionPerformed(event);
    }
  }
}

Listing 5

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

public class JSlidePane extends JPanel
  implements SwingConstants, ActionListener
{
  protected Component component;
  protected int orientation = VERTICAL;
  protected boolean autohide = true;
  protected JButton north, south, west, east;
  protected JViewport viewport;
  protected int incr = 30;

  public JSlidePane(Component component)
  {	
    this(component, VERTICAL, true);
  }
  
  public JSlidePane(Component component, int orientation)
  {	
    this(component, orientation, true);
  }
  
  public JSlidePane(Component component,
    int orientation, boolean autohide)
  {
    this.component = component;
    this.orientation = orientation;
    this.autohide = autohide;
    setLayout(new BorderLayout());
    viewport = new JViewport();
    add(BorderLayout.CENTER, viewport);
    viewport.setView(component);
    if (orientation == VERTICAL)
    {
      north = new BasicArrowButton(BasicArrowButton.NORTH);
      south = new BasicArrowButton(BasicArrowButton.SOUTH);
      north.addActionListener(this);
      south.addActionListener(this);
      add(BorderLayout.NORTH, north);
      add(BorderLayout.SOUTH, south);
    }
    if (orientation == HORIZONTAL)
    {
      west = new BasicArrowButton(BasicArrowButton.WEST);
      east = new BasicArrowButton(BasicArrowButton.EAST);
      west.addActionListener(this);
      east.addActionListener(this);
      add(BorderLayout.EAST, east);
      add(BorderLayout.WEST, west);
    }
  }

  public void setBounds(int x, int y, int w, int h)
  {
    super.setBounds(x, y, w, h);
    Dimension view = new Dimension(w, h);
    Dimension pane = viewport.getView().getPreferredSize();
    viewport.setViewPosition(new Point(0, 0));
    
    if (orientation == VERTICAL)
    {
      if (autohide)
      {
        remove(north);
        if (pane.height >= view.height)
          add(BorderLayout.SOUTH, south);
        else remove(south);
      }
      else
      {
        add(BorderLayout.NORTH, north);
        add(BorderLayout.SOUTH, south);
      }
    }
    if (orientation == HORIZONTAL)
    {
      if (autohide)
      {
        remove(west);
        if (pane.width >= view.width)
          add(BorderLayout.EAST, east);
        else remove(east);
      }
      else
      {
        add(BorderLayout.EAST, east);
        add(BorderLayout.WEST, west);
      }
    }
    doLayout();
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Dimension view = getSize();
    Dimension pane = viewport.getView().getPreferredSize();
    Point origin = viewport.getViewPosition();
    
    if (component instanceof Scrollable)
    {
      Scrollable scrollable = (Scrollable)component;
      incr = scrollable.getScrollableUnitIncrement(
        viewport.getViewRect(), orientation, 0);
    }
    
    if (event.getSource() == north)
    {
      if (pane.height > view.height)
        add(BorderLayout.SOUTH, south);
      if (origin.y < incr)
      {
        viewport.setViewPosition(new Point(0, 0));
        if (autohide) remove(north);
      }
      else
      {
        viewport.setViewPosition(new Point(0, origin.y - incr));
      }
      doLayout();
    }
    
    if (event.getSource() == west)
    {
      if (pane.width > view.width)
        add(BorderLayout.EAST, east);
      if (origin.x < incr)
      {
        viewport.setViewPosition(new Point(0, 0));
        if (autohide) remove(west);
      }
      else
      {
        viewport.setViewPosition(new Point(origin.x - incr, 0));
      }
      doLayout();
    }
    
    if (event.getSource() == south)
    {
      if (pane.height > view.height)
        add(BorderLayout.NORTH, north);
      int max = pane.height - view.height;
      if (origin.y > (max - incr))
      {
        if (autohide)
        {
          remove(south);
          doLayout();
        }
        view = viewport.getExtentSize();
        max = pane.height - view.height;
        viewport.setViewPosition(new Point(0, max));
      }
      else
      {
        viewport.setViewPosition(new Point(0, origin.y + incr));
      }
      doLayout();
    }
    
    if (event.getSource() == east)
    {
      if (pane.width > view.width)
        add(BorderLayout.WEST, west);
      int max = pane.width - view.width;
      if (origin.x > (max - incr))
      {
        if (autohide)
        {
          remove(east);
          doLayout();
        }
        view = viewport.getExtentSize();
        max = pane.width - view.width;
        viewport.setViewPosition(new Point(max, 0));
      }
      else
      {
        viewport.setViewPosition(new Point(origin.x + incr, 0));
      }
      doLayout();
    }
  }
}