001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojava.bio.gui.sequence;
023
024import java.awt.Dimension;
025import java.awt.Font;
026import java.awt.Graphics;
027import java.awt.Graphics2D;
028import java.awt.RenderingHints;
029import java.awt.Shape;
030import java.awt.event.MouseAdapter;
031import java.awt.event.MouseEvent;
032import java.awt.event.MouseListener;
033import java.awt.event.MouseMotionListener;
034import java.awt.geom.Point2D;
035import java.awt.geom.Rectangle2D;
036import java.beans.PropertyChangeEvent;
037import java.beans.PropertyChangeListener;
038import java.beans.PropertyChangeSupport;
039import java.io.Serializable;
040import java.util.ArrayList;
041import java.util.Arrays;
042
043import javax.swing.JComponent;
044import javax.swing.SwingConstants;
045
046import org.biojava.bio.seq.FeatureHolder;
047import org.biojava.bio.seq.Sequence;
048import org.biojava.bio.symbol.RangeLocation;
049import org.biojava.bio.symbol.SymbolList;
050import org.biojava.utils.ChangeAdapter;
051import org.biojava.utils.ChangeEvent;
052import org.biojava.utils.ChangeListener;
053import org.biojava.utils.ChangeSupport;
054import org.biojava.utils.ChangeType;
055import org.biojava.utils.ChangeVetoException;
056import org.biojava.utils.Changeable;
057
058/**
059 * A panel that displays a Sequence.
060 * <p>
061 * A SequencePoster can either display the sequence from left-to-right
062 * (HORIZONTAL) or from top-to-bottom (VERTICAL). It has an associated scale
063 * which is the number of pixels per symbol. It also has a lines property that
064 * controls how to wrap the sequence off one end and onto the other.
065 * <p>
066 * Each line in the SequencePoster is broken down into a list of strips,
067 * each rendered by an individual SequenceRenderer object.
068 * You could add a SequenceRenderer that draws on genes, another that
069 * draws repeats and another that prints out the DNA sequence. They are
070 * responsible for rendering their view of the sequence in the place that the
071 * SequencePoster positions them.
072 *
073 * @author Thomas Down
074 * @author Matthew Pocock
075 * @deprecated This doesn't handle loads of stuff. Use SequencePoster.
076 */
077public class SequencePoster
078  extends
079    JComponent
080  implements
081    SwingConstants,
082    SequenceRenderContext,
083    Changeable
084{
085  public static final ChangeType RENDERER = new ChangeType(
086    "The renderer for this SequencePoster has changed",
087    "org.biojava.bio.gui.sequence.SequencePoster",
088    "RENDERER",
089    SequenceRenderContext.LAYOUT
090  );
091
092  private Sequence sequence;
093  private int direction;
094  private double scale;
095  private int lines;
096  private int spacer;
097
098  private SequenceRenderContext.Border leadingBorder;
099  private SequenceRenderContext.Border trailingBorder;
100
101  private SequenceRenderer renderer;
102  private double[] offsets;
103  private int realLines;
104  private double alongDim = 0.0;
105  private double acrossDim = 0.0;
106  private int symbolsPerLine = 0;
107
108  private RendererMonitor theMonitor;
109  private RenderingHints renderingHints = null;
110
111  private transient ChangeSupport changeSupport = null;
112
113  private SequenceViewerSupport svSupport = new SequenceViewerSupport();
114  private MouseListener mouseListener = new MouseAdapter() {
115    public void mouseClicked(MouseEvent me) {
116      if(!isActive()) {
117        return;
118      }
119      int[] lineExtent = calcLineExtent(me);
120      me.translatePoint(-lineExtent[2], -lineExtent[3]);
121      SequenceViewerEvent sve = renderer.processMouseEvent(
122        new SubSequenceRenderContext(
123          SequencePoster.this, null, null,
124          new RangeLocation(lineExtent[0], lineExtent[1])
125        ),
126        me,
127        new ArrayList()
128      );
129      me.translatePoint(+lineExtent[2], +lineExtent[3]);
130      svSupport.fireMouseClicked(sve);
131    }
132
133    public void mousePressed(MouseEvent me) {
134      if(!isActive()) {
135        return;
136      }
137      int[] lineExtent = calcLineExtent(me);
138      me.translatePoint(-lineExtent[2], -lineExtent[3]);
139      SequenceViewerEvent sve = renderer.processMouseEvent(
140        new SubSequenceRenderContext(
141          SequencePoster.this, null, null,
142          new RangeLocation(lineExtent[0], lineExtent[1])
143        ),
144        me,
145        new ArrayList()
146      );
147      me.translatePoint(+lineExtent[2], +lineExtent[3]);
148      svSupport.fireMousePressed(sve);
149    }
150
151    public void mouseReleased(MouseEvent me) {
152      if(!isActive()) {
153        return;
154      }
155      int[] lineExtent = calcLineExtent(me);
156      me.translatePoint(-lineExtent[2], -lineExtent[3]);
157      SequenceViewerEvent sve = renderer.processMouseEvent(
158        new SubSequenceRenderContext(
159          SequencePoster.this, null, null,
160          new RangeLocation(lineExtent[0], lineExtent[1])
161        ),
162        me,
163        new ArrayList()
164      );
165      me.translatePoint(+lineExtent[2], +lineExtent[3]);
166      svSupport.fireMouseReleased(sve);
167    }
168  };
169  public void addSequenceViewerListener(SequenceViewerListener svl) {
170    svSupport.addSequenceViewerListener(svl);
171  }
172  public void removeSequenceViewerListener(SequenceViewerListener svl) {
173    svSupport.removeSequenceViewerListener(svl);
174  }
175
176  private SequenceViewerMotionSupport svmSupport = new SequenceViewerMotionSupport();
177  private MouseMotionListener mouseMotionListener = new MouseMotionListener() {
178    public void mouseDragged(MouseEvent me) {
179      if(!isActive()) {
180        return;
181      }
182      int[] lineExtent = calcLineExtent(me);
183      me.translatePoint(-lineExtent[2], -lineExtent[3]);
184      SequenceViewerEvent sve = renderer.processMouseEvent(
185        new SubSequenceRenderContext(
186          SequencePoster.this, null, null,
187          new RangeLocation(lineExtent[0], lineExtent[1])
188        ),
189        me,
190        new ArrayList()
191      );
192      me.translatePoint(+lineExtent[2], +lineExtent[3]);
193      svmSupport.fireMouseDragged(sve);
194    }
195
196    public void mouseMoved(MouseEvent me) {
197      if(!isActive()) {
198        return;
199      }
200      int[] lineExtent = calcLineExtent(me);
201      me.translatePoint(-lineExtent[2], -lineExtent[3]);
202      SequenceViewerEvent sve = renderer.processMouseEvent(
203        new SubSequenceRenderContext(
204          SequencePoster.this, null, null,
205          new RangeLocation(lineExtent[0], lineExtent[1])
206        ),
207        me,
208        new ArrayList()
209      );
210      me.translatePoint(+lineExtent[2], +lineExtent[3]);
211      svmSupport.fireMouseMoved(sve);
212    }
213  };
214  public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml) {
215    svmSupport.addSequenceViewerMotionListener(svml);
216  }
217  public void removeSequenceViewerMotionListener(SequenceViewerMotionListener svml) {
218    svmSupport.removeSequenceViewerMotionListener(svml);
219  }
220
221  protected boolean hasChangeListeners() {
222    return changeSupport != null;
223  }
224
225  protected ChangeSupport getChangeSupport(ChangeType ct) {
226    if(changeSupport != null) {
227      return changeSupport;
228    }
229
230    synchronized(this) {
231      if(changeSupport == null) {
232        changeSupport = new ChangeSupport();
233      }
234
235      return changeSupport;
236    }
237  }
238
239  public void addChangeListener(ChangeListener cl) {
240    addChangeListener(cl, ChangeType.UNKNOWN);
241  }
242
243  public void addChangeListener(ChangeListener cl, ChangeType ct) {
244    ChangeSupport cs = getChangeSupport(ct);
245    cs.addChangeListener(cl, ct);
246  }
247
248  public void removeChangeListener(ChangeListener cl) {
249    removeChangeListener(cl, ChangeType.UNKNOWN);
250  }
251
252  public void removeChangeListener(ChangeListener cl, ChangeType ct) {
253    if(hasChangeListeners()) {
254      getChangeSupport(ct).removeChangeListener(cl, ct);
255    }
256  }
257
258  public boolean isUnchanging(ChangeType ct) {
259    return getChangeSupport(ct).isUnchanging(ct);
260  }
261
262  private ChangeListener layoutListener = new ChangeAdapter() {
263    public void postChange(ChangeEvent ce) {
264      resizeAndValidate();
265    }
266  };
267  private ChangeListener repaintListener = new ChangeAdapter() {
268    public void postChange(ChangeEvent ce) {
269      repaint();
270    }
271  };
272
273  /**
274   * Initializer.
275   */
276
277  {
278    direction = HORIZONTAL;
279    scale = 12.0;
280    lines = 1;
281    spacer = 0;
282
283    theMonitor = new RendererMonitor();
284    leadingBorder = new SequenceRenderContext.Border();
285    trailingBorder = new SequenceRenderContext.Border();
286  }
287
288  /**
289   * Create a new SeqeuncePanel.
290   */
291  public SequencePoster() {
292    super();
293    if(getFont() == null) {
294      setFont(new Font("Times New Roman", Font.PLAIN, 12));
295    }
296    this.addPropertyChangeListener(theMonitor);
297    this.addMouseListener(mouseListener);
298    this.addMouseMotionListener(mouseMotionListener);
299  }
300
301  /**
302   * Set the SymboList to be rendered. This symbol list will be passed onto the
303   * SequenceRenderer instances registered with this SequencePoster.
304   *
305   * @param s  the SymboList to render
306   */
307  public void setSequence(Sequence s) {
308    Sequence oldSequence = sequence;
309    if(oldSequence != null) {
310      oldSequence.removeChangeListener(layoutListener);
311    }
312    this.sequence = s;
313    sequence.addChangeListener(layoutListener);
314
315    resizeAndValidate();
316    firePropertyChange("sequence", oldSequence, s);
317  }
318
319  public Sequence getSequence() {
320    return sequence;
321  }
322
323  /**
324   * Retrieve the currently rendered SymbolList
325   *
326   * @return  the current SymbolList
327   */
328  public SymbolList getSymbols() {
329    return sequence;
330  }
331
332  public FeatureHolder getFeatures() {
333    return sequence;
334  }
335
336  public RangeLocation getRange() {
337    return new RangeLocation(1, sequence.length());
338  }
339
340  public RangeLocation getVisibleRange() {
341    return getRange();
342  }
343
344  /**
345   * Set the direction that this SequencePoster renders in. The direction can be
346   * one of HORIZONTAL or VERTICAL. Once the direction is set, the display will
347   * redraw. HORIZONTAL represents left-to-right rendering. VERTICAL represents
348   * AceDB-style vertical rendering.
349   *
350   * @param dir  the new rendering direction
351   */
352  public void setDirection(int dir)
353  throws IllegalArgumentException {
354    if(dir != HORIZONTAL && dir != VERTICAL) {
355      throw new IllegalArgumentException(
356        "Direction must be either HORIZONTAL or VERTICAL"
357      );
358    }
359    int oldDirection = direction;
360    direction = dir;
361    resizeAndValidate();
362    firePropertyChange("direction", oldDirection, direction);
363  }
364
365  /**
366   * Retrieve the current rendering direction.
367   *
368   * @return the rendering direction (one of HORIZONTAL and VERTICAL)
369   */
370  public int getDirection() {
371    return direction;
372  }
373
374  /**
375   * Set the number of pixels to leave blank between each block of sequence
376   * information.
377   * <p>
378   * If the SeqeuncePanel chooses to display the sequence information split
379   * across multiple lines, then the spacer parameter indicates how many pixles
380   * will seperate each line.
381   *
382   * @param spacer  the number of pixels seperating each line of sequence
383   * information
384   */
385  public void setSpacer(int spacer) {
386    int oldSpacer = this.spacer;
387    this.spacer = spacer;
388    resizeAndValidate();
389    firePropertyChange("spacer", oldSpacer, spacer);
390  }
391
392  /**
393   * Retrieve the current spacer value
394   *
395   * @return the number of pixels between each line of sequence information
396   */
397  public int getSpacer() {
398    return spacer;
399  }
400
401  /**
402   * Set the scale.
403   * <p>
404   * The scale parameter is interpreted as the number of pixels per symbol. This
405   * may take on a wide range of values - for example, to render the symbols as
406   * text, you will need a scale of > 8, where as to render chromosome 1 you
407   * will want a scale &lt; 0.00000001
408   *
409   * @param scale the new pixles-per-symbol ratio
410   */
411  public void setScale(double scale) {
412    double oldScale = this.scale;
413    this.scale = scale;
414    resizeAndValidate();
415    firePropertyChange("scale", oldScale, scale);
416  }
417
418  /**
419   * Retrieve the current scale.
420   *
421   * @return the number of pixles used to render one symbol
422   */
423  public double getScale() {
424    return scale;
425  }
426
427  /**
428   * Set the absolute number of lines that the sequence will be rendered on. If
429   * this is set to 0, then the number of lines will be calculated according to
430   * how many lines will be needed to render the sequence in the currently
431   * available space. If it is set to any positive non-zero value, the sequence
432   * will be rendered using that many lines, and the SequencePoster will request
433   * enough space to accomplish this.
434   *
435   * @param lines  the number of lines to split the sequence information over
436   */
437  public void setLines(int lines) {
438    int oldLines = this.lines;
439    this.lines = lines;
440    resizeAndValidate();
441    firePropertyChange("lines", oldLines, lines);
442  }
443
444  /**
445   * Retrieve the number of lines that the sequence will be rendered over.
446   *
447   * @return the current number of lines (0 if autocalculated)
448   */
449  public int getLines() {
450    return lines;
451  }
452
453  /**
454   * Retrieve the object that encapsulates the leading border area - the space
455   * before sequence information is rendered.
456   *
457   * @return a SequenceRenderContext.Border instance
458   */
459  public SequenceRenderContext.Border getLeadingBorder() {
460    return leadingBorder;
461  }
462
463  /**
464   * Retrieve the object that encapsulates the trailing border area - the space
465   * after sequence information is rendered.
466   *
467   * @return a SequenceRenderContext.Border instance
468   */
469  public SequenceRenderContext.Border getTrailingBorder() {
470    return trailingBorder;
471  }
472
473  /**
474   * Paint this component.
475   * <p>
476   * This calls the paint method of the currently registered SequenceRenderer
477   * after setting up the graphics appropriately.
478   */
479  public void paintComponent(Graphics g) {
480    if(!isActive()) {
481      return;
482    }
483
484    Graphics2D g2 = (Graphics2D) g;
485    if(renderingHints != null){
486      g2.setRenderingHints(renderingHints);
487    }
488
489    Rectangle2D currentClip = g2.getClip().getBounds2D();
490    double minPos;
491    double maxPos;
492    if(direction == HORIZONTAL) {
493      minPos = currentClip.getMinY();
494      maxPos = currentClip.getMaxY();
495    } else {
496      minPos = currentClip.getMinX();
497      maxPos = currentClip.getMaxX();
498    }
499
500    //System.out.println("minPos: " + minPos);
501    int minOffset = Arrays.binarySearch(offsets, minPos);
502    if(minOffset < 0) {
503      minOffset = -minOffset - 1;
504    }
505    //System.out.println("minOffset: " + minOffset);
506    double minCoord = (minOffset == 0) ? 0.0 : offsets[minOffset-1];
507    //System.out.println("minCoord: " + minCoord);
508    int minP = 1 + (int) ((double) minOffset * symbolsPerLine);
509    //System.out.println("minP: " + minP);
510
511    Rectangle2D.Double clip = new Rectangle2D.Double();
512    if (direction == HORIZONTAL) {
513        clip.width = alongDim;
514        clip.height = acrossDim;
515        g2.translate(leadingBorder.getSize() - alongDim * minOffset, minCoord);
516    } else {
517        clip.width = acrossDim;
518        clip.height = alongDim;
519        g2.translate(minCoord, leadingBorder.getSize() - alongDim * minOffset);
520    }
521
522    int min = minP;
523    for(int l = minOffset; l < realLines; l++) {
524
525      if (direction == HORIZONTAL) {
526          clip.x = l * alongDim;
527          clip.y = 0.0;
528      } else {
529          clip.x = 0.0;
530          clip.y = l * alongDim;
531      }
532
533      double depth = offsets[l] - spacer;
534      if(l != 0) {
535        depth -= offsets[l-1];
536      }
537
538      if (direction == HORIZONTAL) {
539          clip.height = depth;
540      } else {
541          clip.width = depth;
542      }
543
544      Shape oldClip = g2.getClip();
545      g2.clip(clip);
546      renderer.paint(g2, this);
547      g2.setClip(oldClip);
548
549      if (direction == HORIZONTAL) {
550          g2.translate(-alongDim, spacer + depth);
551      } else {
552          g2.translate(spacer + depth, -alongDim);
553      }
554
555      min += symbolsPerLine;
556
557      if( (min > sequence.length()) || (offsets[l] > maxPos)) {
558        break;
559      }
560    }
561  }
562
563  public void setRenderer(SequenceRenderer r)
564  throws ChangeVetoException {
565    if(hasChangeListeners()) {
566      ChangeEvent ce = new ChangeEvent(
567        this,
568        RENDERER,
569        r,
570        this.renderer
571      );
572      ChangeSupport cs = getChangeSupport(RENDERER);
573      synchronized(cs) {
574        cs.firePreChangeEvent(ce);
575        _setRenderer(r);
576        cs.firePostChangeEvent(ce);
577      }
578    } else {
579      _setRenderer(r);
580    }
581    resizeAndValidate();
582  }
583
584  protected void _setRenderer(SequenceRenderer r) {
585    if( (this.renderer != null) && (this.renderer instanceof Changeable) ) {
586      Changeable c = (Changeable) this.renderer;
587      c.removeChangeListener(layoutListener, SequenceRenderContext.LAYOUT);
588      c.removeChangeListener(repaintListener, SequenceRenderContext.REPAINT);
589    }
590
591    this.renderer = r;
592
593    if( (r != null) && (r instanceof Changeable) ) {
594      Changeable c = (Changeable) r;
595      c.addChangeListener(layoutListener, SequenceRenderContext.LAYOUT);
596      c.addChangeListener(repaintListener, SequenceRenderContext.REPAINT);
597    }
598  }
599
600  public double sequenceToGraphics(int seqPos) {
601    return ((double) (seqPos-1) * scale);
602  }
603
604  public int graphicsToSequence(double gPos) {
605    return (int) (gPos / scale) + 1;
606  }
607
608  public int graphicsToSequence(Point2D point) {
609    if(direction == HORIZONTAL) {
610      return graphicsToSequence(point.getX());
611    } else {
612      return graphicsToSequence(point.getY());
613    }
614  }
615
616  public void resizeAndValidate() {
617    //System.out.println("resizeAndValidate starting");
618    Dimension d = null;
619    double acrossDim;
620
621    if(!isActive()) {
622      System.out.println("No sequence");
623      // no sequence - collapse down to no size at all
624      alongDim = 0.0;
625      acrossDim = 0.0;
626      realLines = 0;
627      leadingBorder.setSize(0.0);
628      trailingBorder.setSize(0.0);
629      d = new Dimension(0, 0);
630    } else {
631      System.out.println("Fitting to sequence");
632
633      int width;
634      Dimension parentSize = (getParent() != null)
635                ? getParent().getSize()
636                : new Dimension(500, 400);
637      if (direction == HORIZONTAL) {
638        width = parentSize.width;
639      } else {
640        width = parentSize.height;
641      }
642
643      System.out.println("Initial width: " + width);
644      // got a sequence - fit the size according to sequence length & preferred
645      // number of lines.
646      alongDim = scale * sequence.length();
647      System.out.println("alongDim (pixles needed for sequence only): "
648      + alongDim);
649      acrossDim = 0.0;
650
651      double insetBefore = renderer.getMinimumLeader(this);
652      double insetAfter = renderer.getMinimumTrailer(this);
653
654      leadingBorder.setSize(insetBefore);
655      trailingBorder.setSize(insetAfter);
656      double insets = insetBefore + insetAfter;
657      //System.out.println("insetBefore: " + insetBefore);
658      //System.out.println("insetAfter: " + insetAfter);
659
660      if(lines > 0) {
661        // Fixed number of lines. Calculate width needed to lay out rectangle.
662        realLines = lines;
663        width = (int) Math.ceil(
664          insets +
665          alongDim / (double) lines
666        );
667      } else {
668        // Calculated number of lines for a fixed width
669        double dWidth = (double) width;
670        dWidth -= insets; // leave space for insets
671        realLines = (int) Math.ceil(alongDim / (double) width);
672        width = (int) Math.ceil(
673          insets +
674          alongDim / (double) realLines
675        );
676      }
677
678      acrossDim = 0.0;
679      symbolsPerLine = (int) Math.ceil((double) width / (double) scale);
680      //System.out.println("symbolsPerLine: " + symbolsPerLine);
681      //System.out.println("width: " + width);
682      //System.out.println("lines: " + lines);
683      //System.out.println("realLines: " + realLines);
684      if(symbolsPerLine < 1) {
685        throw new Error("Pants");
686      }
687      int min = 1;
688      this.offsets = new double[realLines];
689      int li = 0;
690      while(min <= sequence.length()) {
691        int max = min + symbolsPerLine - 1;
692        double depth = renderer.getDepth(this);
693        acrossDim += depth + spacer;
694        offsets[li] = acrossDim;
695        min = max + 1;
696        li++;
697      }
698
699      acrossDim += spacer * (realLines - 1);
700      alongDim = /* Math.ceil((double) width); */ symbolsPerLine * scale;
701      if (direction == HORIZONTAL) {
702        d = new Dimension(
703          (int) Math.ceil(alongDim + insetBefore + insetAfter),
704          (int) acrossDim
705        );
706      } else {
707        d = new Dimension(
708          (int) acrossDim,
709          (int) Math.ceil(alongDim + insetBefore + insetAfter)
710        );
711      }
712    }
713
714    setMinimumSize(d);
715    setPreferredSize(d);
716    revalidate();
717    //System.out.println("resizeAndValidate ending");
718  }
719
720  private class RendererMonitor implements PropertyChangeListener {
721    public void propertyChange(PropertyChangeEvent ev) {
722      repaint();
723    }
724  }
725
726  protected int[] calcLineExtent(MouseEvent me) {
727    int pos;
728    if(direction == HORIZONTAL) {
729      pos = me.getY();
730    } else {
731      pos = me.getX();
732    }
733
734    int minOffset = Arrays.binarySearch(offsets, pos);
735    if(minOffset < 0) {
736      minOffset = -minOffset - 1;
737    }
738    int min = 1 + (int) ((double) minOffset * symbolsPerLine);
739    int max = min + symbolsPerLine - 1;
740    double minPos;
741    if(minOffset > 0) {
742      minPos = offsets[minOffset - 1];
743    } else {
744      minPos = 0.0;
745    }
746
747    double ad = alongDim * minOffset;
748
749    int xdiff;
750    int ydiff;
751    if(direction == HORIZONTAL) {
752      xdiff = (int) -ad;
753      ydiff = (int) minPos;
754    } else {
755      xdiff = (int) minPos;
756      ydiff = (int) -ad;
757    }
758
759    return new int[] { min, max, xdiff, ydiff };
760  }
761
762  protected boolean isActive() {
763    return
764      (sequence != null) &&
765      (renderer != null);
766  }
767
768  /**
769   * @return the current rendering properties
770   */
771  public RenderingHints getRenderingHints() {
772    return renderingHints;
773  }
774  /**
775   * Use this to switch on effects like Anti-aliasing etc
776   * @param renderingHints the desired rendering properties
777   */
778  public void setRenderingHints(RenderingHints renderingHints) {
779    this.renderingHints = renderingHints;
780  }
781
782  public class Border
783  implements Serializable, SwingConstants {
784    protected final PropertyChangeSupport pcs;
785    private double size = 0.0;
786    private int alignment = CENTER;
787
788    public double getSize() {
789      return size;
790    }
791
792    public int getAlignment() {
793      return alignment;
794    }
795
796    public void setAlignment(int alignment)
797        throws IllegalArgumentException
798    {
799        if (alignment == LEADING || alignment == TRAILING || alignment == CENTER) {
800            int old = this.alignment;
801            this.alignment = alignment;
802            pcs.firePropertyChange("alignment", old, alignment);
803        } else {
804            throw new IllegalArgumentException(
805                  "Alignment must be one of the constants LEADING, TRAILING or CENTER"
806            );
807        }
808    }
809
810    private Border() {
811      alignment = CENTER;
812      pcs = new PropertyChangeSupport(this);
813    }
814
815    public void addPropertyChangeListener(PropertyChangeListener listener) {
816      pcs.addPropertyChangeListener(listener);
817    }
818
819    public void removePropertyChangeListener(PropertyChangeListener listener) {
820      pcs.removePropertyChangeListener(listener);
821    }
822  }
823}
824