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.Insets;
029import java.awt.RenderingHints;
030import java.awt.Shape;
031import java.awt.event.MouseAdapter;
032import java.awt.event.MouseEvent;
033import java.awt.event.MouseListener;
034import java.awt.event.MouseMotionListener;
035import java.awt.geom.AffineTransform;
036import java.awt.geom.Point2D;
037import java.awt.geom.Rectangle2D;
038import java.beans.PropertyChangeEvent;
039import java.beans.PropertyChangeListener;
040import java.util.ArrayList;
041
042import javax.swing.JComponent;
043
044import org.biojava.bio.seq.FeatureHolder;
045import org.biojava.bio.symbol.RangeLocation;
046import org.biojava.bio.symbol.SymbolList;
047import org.biojava.utils.ChangeAdapter;
048import org.biojava.utils.ChangeEvent;
049import org.biojava.utils.ChangeListener;
050import org.biojava.utils.ChangeSupport;
051import org.biojava.utils.ChangeType;
052import org.biojava.utils.ChangeVetoException;
053import org.biojava.utils.Changeable;
054
055/**
056 * <p><code>TranslatedSequencePanel</code> is a panel that displays a
057 * Sequence. Its features are that it will always draw at low pixel
058 * coordinates when using Java2D to render very long sequences and it
059 * is quite fast (approximately 8x faster than
060 * <code>SequencePanel</code></p>.
061 *
062 * <p>A <code>TranslatedSequencePanel</code> can either display the
063 * sequence from left-to-right (HORIZONTAL) or from top-to-bottom
064 * (VERTICAL). It has an associated scale which is the number of
065 * pixels per symbol and a translation which is the number of
066 * <code>Symbol</code>s to skip before rendering starts. In order to
067 * produce a scrolling effect, the <code>setSymbolTranslation</code>
068 * method may be hooked up to an <code>Adjustable</code> such as
069 * <code>JScrollBar</code> or to an event listener.</p>
070 *
071 * <p>The exact number of <code>Symbol</code>s rendered depends on the
072 * width of the panel and the scale. Resizing the panel will cause the
073 * number of <code>Symbol</code>s rendered to change accordingly.</p>
074 *
075 * <p>The panel will fill its background to the <code>Color</code>
076 * defined by the <code>setBackground()</code> method provided that it
077 * has been defined as opaque using <code>setOpaque()</code>.</p>
078 *
079 * <p>The change event handling code is based on the original panel
080 * and other BioJava components by Matthew and Thomas.</p>
081 *
082 * @author Keith James
083 * @author Matthew Pocock
084 * @author Thomas Down
085 * @author Jolyon Holdstock
086 * @since 1.2
087 */
088public class TranslatedSequencePanel extends JComponent
089    implements SequenceRenderContext, Changeable
090{
091    /**
092         * Generated Serial Version UID
093         */
094        private static final long serialVersionUID = 3269477379497205817L;
095
096        /**
097     * Constant <code>RENDERER</code> is a <code>ChangeType</code>
098     * which indicates a change to the renderer, requiring a layout
099     * update.
100     */
101    public static final ChangeType RENDERER =
102        new ChangeType("The renderer for this TranslatedSequencePanel has changed",
103                       "org.biojava.bio.gui.sequence.TranslatedSequencePanel",
104                       "RENDERER", SequenceRenderContext.LAYOUT);
105
106    /**
107     * Constant <code>TRANSLATION</code> is a <code>ChangeType</code>
108     * which indicates a change to the translation, requiring a paint
109     * update.
110     */
111    public static final ChangeType TRANSLATION =
112        new ChangeType("The translation for this TranslatedSequencePanel has changed",
113                       "org.biojava.bio.gui.sequence.TranslatedSequencePanel",
114                       "TRANSLATION", SequenceRenderContext.REPAINT);
115
116    // The sequence to be rendered
117    private SymbolList sequence;
118    // The number of residues to skip before starting to render
119    private int translation;
120    // The rendering direction (HORIZONTAL or VERTICAL)
121    private int direction;
122    // The rendering scale in pixels per residue
123    private double scale;
124    // The sequence renderer
125    private SequenceRenderer renderer;
126    // The total border size of the renderer. Cached to avoid
127    // recursive method calls between us and the renderer.
128    private double rendererBorders;
129    // RenderingHints to be used by renderers
130    private RenderingHints hints;
131
132    // The rendering context borders
133    private SequenceRenderContext.Border leadingBorder;
134    private SequenceRenderContext.Border trailingBorder;
135
136    // Listens for bound property changes which require a repaint
137    // afterwards
138    private PropertyChangeListener propertyListener =
139        new PropertyChangeListener()
140        {
141            public void propertyChange(PropertyChangeEvent pce)
142            {
143                repaint();
144            }
145        };
146
147    // ChangeSupport helper for BioJava ChangeListeners
148    private transient ChangeSupport changeSupport = null;
149
150    // Listens for BioJava changes which will require a repaint
151    // afterwards
152    private ChangeListener repaintListener = new ChangeAdapter()
153    {
154        public void postChange(ChangeEvent ce)
155        {
156            repaint();
157        }
158    };
159
160    // Listens for BioJava changes which will require a revalidation
161    // afterwards
162    private ChangeListener layoutListener = new ChangeAdapter()
163    {
164        public void postChange(ChangeEvent ce)
165        {
166            revalidate();
167        }
168    };
169
170    // SequenceViewerSupport helper for BioJava SequenceViewerListeners
171    private SequenceViewerSupport svSupport = new SequenceViewerSupport();
172
173    // Listens for mouse click events
174    private MouseListener mouseListener = new MouseAdapter()
175    {
176        public void mouseClicked(MouseEvent me)
177        {
178            if (! isActive())
179                return;
180
181            Insets insets = getInsets();
182            me.translatePoint(-insets.left, -insets.top);
183
184            SequenceViewerEvent sve =
185                renderer.processMouseEvent(TranslatedSequencePanel.this, me,
186                                           new ArrayList());
187
188            me.translatePoint(insets.left, insets.top);
189            svSupport.fireMouseClicked(sve);
190        }
191
192        public void mousePressed(MouseEvent me)
193        {
194            if (! isActive())
195                return;
196
197            Insets insets = getInsets();
198            me.translatePoint(-insets.left, -insets.top);
199
200            SequenceViewerEvent sve =
201                renderer.processMouseEvent(TranslatedSequencePanel.this, me,
202                                           new ArrayList());
203            me.translatePoint(insets.left, insets.top);
204            svSupport.fireMousePressed(sve);
205        }
206
207        public void mouseReleased(MouseEvent me)
208        {
209            if (! isActive())
210                return;
211
212            Insets insets = getInsets();
213            me.translatePoint(-insets.left, -insets.top);
214
215            SequenceViewerEvent sve =
216                renderer.processMouseEvent(TranslatedSequencePanel.this, me,
217                                           new ArrayList());
218
219            me.translatePoint(insets.left, insets.top);
220            svSupport.fireMouseReleased(sve);
221        }
222    };
223
224    // SequenceViewerMotionSupport helper for BioJava
225    // SequenceViewerMotionListeners
226    private SequenceViewerMotionSupport svmSupport =
227        new SequenceViewerMotionSupport();
228
229    // Listens for mouse movement events
230    private MouseMotionListener mouseMotionListener = new MouseMotionListener()
231    {
232        public void mouseDragged(MouseEvent me)
233        {
234            if (! isActive())
235                return;
236
237            Insets insets = getInsets();
238            me.translatePoint(-insets.left, -insets.top);
239
240            SequenceViewerEvent sve =
241                renderer.processMouseEvent(TranslatedSequencePanel.this, me,
242                                           new ArrayList());
243
244            me.translatePoint(insets.left, insets.top);
245            svmSupport.fireMouseDragged(sve);
246        }
247
248        public void mouseMoved(MouseEvent me)
249        {
250            if (! isActive())
251                return;
252
253            Insets insets = getInsets();
254            me.translatePoint(-insets.left, -insets.top);
255
256            SequenceViewerEvent sve =
257                renderer.processMouseEvent(TranslatedSequencePanel.this, me,
258                                           new ArrayList());
259
260            me.translatePoint(insets.left, insets.top);
261            svmSupport.fireMouseMoved(sve);
262        }
263    };
264
265    /**
266     * Creates a new <code>TranslatedSequencePanel</code> with the
267     * default settings (direction HORIZONTAL, scale 10.0 pixels per
268     * symbol, symbol translation 0, leading border 0.0, trailing
269     * border 0.0, 12 point sanserif font).
270     */
271    public TranslatedSequencePanel()
272    {
273        super();
274
275        if (getFont() == null)
276            setFont(new Font("sanserif", Font.PLAIN, 12));
277
278        direction   = HORIZONTAL;
279        scale       = 10.0;
280        translation = 0;
281
282        leadingBorder  = new SequenceRenderContext.Border();
283        trailingBorder = new SequenceRenderContext.Border();
284
285        leadingBorder.setSize(0.0);
286        trailingBorder.setSize(0.0);
287
288        hints = new RenderingHints(null);
289
290        this.addPropertyChangeListener(propertyListener);
291        this.addMouseListener(mouseListener);
292        this.addMouseMotionListener(mouseMotionListener);
293    }
294
295    /**
296     * <code>getSequence</code> returns the entire
297     * <code>Sequence</code> currently being rendered.
298     *
299     * @return a <code>Sequence</code>.
300     */
301    public SymbolList getSequence()
302    {
303        return sequence;
304    }
305
306    /**
307     * <code>setSequence</code> sets the <code>Sequence</code> to be
308     * rendered.
309     *
310     * @param sequence a <code>Sequence</code>.
311     */
312    public void setSequence(SymbolList sequence)
313    {
314        SymbolList prevSequence = this.sequence;
315
316        // Remove out listener from the sequence, if necessary
317        if (prevSequence != null)
318            prevSequence.removeChangeListener(layoutListener);
319
320        this.sequence = sequence;
321
322        // Add our listener to the sequence, if necessary. Also update
323        // rendererBorders cache which can be affected by changes in
324        // sequence
325        if (sequence != null)
326        {
327            if (renderer != null)
328                rendererBorders = renderer.getMinimumLeader(this)
329                + renderer.getMinimumTrailer(this);
330
331            sequence.addChangeListener(layoutListener);
332        }
333
334        firePropertyChange("sequence", prevSequence, sequence);
335        resizeAndValidate();
336    }
337
338    /**
339     * <code>getSymbols</code> returns all of the <code>Symbol</code>s
340     * belonging to the currently rendered <code>Sequence</code>.
341     *
342     * @return a <code>SymbolList</code>.
343     */
344    public SymbolList getSymbols()
345    {
346        return sequence;
347    }
348
349    /**
350     * <code>getFeatures</code> returns all of the
351     * <code>Feature</code>s belonging to the currently rendered
352     * <code>Sequence</code>.
353     *
354     * @return a <code>FeatureHolder</code>.
355     */
356    public FeatureHolder getFeatures()
357    {
358      if(sequence instanceof FeatureHolder) {
359        return (FeatureHolder) sequence;
360      } else {
361        return FeatureHolder.EMPTY_FEATURE_HOLDER;
362      }
363    }
364
365    /**
366     * <code>getRange</code> returns a <code>RangeLocation</code>
367     * representing the region of the sequence currently being
368     * rendered. This is calculated from the size of the
369     * <code>TranslatedSequencePanel</code>, minus its
370     * <code>SequenceRenderContext.Border</code>s and its delegate
371     * renderer borders (if any), the current rendering translation
372     * and the current scale. The value will therefore change when the
373     * <code>TranslatedSequencePanel</code> is resized or "scrolled"
374     * by changing the translation.
375     *
376     * @return a <code>RangeLocation</code>.
377     */
378    public RangeLocation getRange()
379    {
380        int visibleSymbols = getVisibleSymbolCount();
381
382        // This is a fudge as we have to return a RangeLocation, which
383        // can not have start == end
384        if (visibleSymbols == 0)
385            return new RangeLocation(translation + 1,
386                                     translation + 2);
387        else
388            return new RangeLocation(translation + 1, visibleSymbols);
389    }
390
391    /**
392     * <code>getDirection</code> returns the direction in which this
393     * context expects sequences to be rendered - HORIZONTAL or
394     * VERTICAL.
395     *
396     * @return an <code>int</code>.
397     */
398    public int getDirection()
399    {
400        return direction;
401    }
402
403    /**
404     * <code>setDirection</code> sets the direction in which this
405     * context will render sequences - HORIZONTAL or VERTICAL.
406     *
407     * @param direction an <code>int</code>.
408     *
409     * @exception IllegalArgumentException if an error occurs.
410     */
411    public void setDirection(int direction) throws IllegalArgumentException
412    {
413        if (direction != HORIZONTAL && direction != VERTICAL)
414            throw new IllegalArgumentException("Direction must be either HORIZONTAL or VERTICAL");
415
416        int prevDirection = this.direction;
417        this.direction = direction;
418
419        firePropertyChange("direction", prevDirection, direction);
420        resizeAndValidate();
421    }
422
423    /**
424     * <code>getScale</code> returns the scale in pixels per
425     * <code>Symbol</code>.
426     *
427     * @return a <code>double</code>.
428     */
429    public double getScale()
430    {
431        return scale;
432    }
433
434    /**
435     * <code>setScale</code> sets the scale in pixels per
436     * <code>Symbol</code>.
437     *
438     * @param scale a <code>double</code>.
439     */
440    public void setScale(double scale)
441    {
442        double prevScale = this.scale;
443        this.scale = scale;
444
445        firePropertyChange("scale", prevScale, scale);
446        resizeAndValidate();
447    }
448
449    /**
450     * <code>getSymbolTranslation</code> returns the current
451     * translation in <code>Symbol</code>s which will be applied when
452     * rendering. The sequence will be rendered, immediately after any
453     * borders, starting at this translation. Values may be from 0 to
454     * the length of the rendered sequence.
455     *
456     * @return an <code>int</code>.
457     */
458    public int getSymbolTranslation()
459    {
460        return translation;
461    }
462
463    /**
464     * <code>setSymbolTranslation</code> sets the translation in
465     * <code>Symbol</code>s which will be applied when rendering. The
466     * sequence will be rendered, immediately after any borders,
467     * starting at that translation. Values may be from 0 to the
468     * length of the rendered sequence.
469     *
470     * @param translation an <code>int</code>.
471     *
472     * @exception IndexOutOfBoundsException if the translation is
473     * greater than the sequence length.
474     */
475    public void setSymbolTranslation(int translation)
476        throws IndexOutOfBoundsException
477    {
478        if (translation >= sequence.length())
479            throw new IndexOutOfBoundsException("Tried to set symbol translation offset equal to or greater than SymbolList length");
480
481        int prevTranslation = this.translation;
482
483        if (hasListeners())
484        {
485            ChangeSupport cs = getChangeSupport(TRANSLATION);
486
487            ChangeEvent ce = new ChangeEvent(this, TRANSLATION);
488
489            cs.firePostChangeEvent(ce);
490            this.translation = translation;
491            cs.firePostChangeEvent(ce);
492        }
493        else
494        {
495            this.translation = translation;
496        }
497
498        firePropertyChange("translation", prevTranslation, translation);
499        resizeAndValidate();
500    }
501
502    /**
503     * <code>getLeadingBorder</code> returns the leading border.
504     *
505     * @return a <code>SequenceRenderContext.Border</code>.
506     */
507    public SequenceRenderContext.Border getLeadingBorder()
508    {
509        return leadingBorder;
510    }
511
512    /**
513     * <code>getTrailingBorder</code> returns the trailing border.
514     *
515     * @return a <code>SequenceRenderContext.Border</code>.
516     */
517    public SequenceRenderContext.Border getTrailingBorder()
518    {
519        return trailingBorder;
520    }
521
522    /**
523     * <code>getRenderer</code> returns the current
524     * <code>SequenceRenderer</code>.
525     *
526     * @return a <code>SequenceRenderer</code>.
527     */
528    public SequenceRenderer getRenderer()
529    {
530        return renderer;
531    }
532
533    /**
534     * <code>setRenderer</code> sets the current
535     * <code>SequenceRenderer</code>.
536     *
537     * @param renderer  set the <code>SequenceRenderer</code> used
538     */
539    public void setRenderer(SequenceRenderer renderer)
540        throws ChangeVetoException
541    {
542        if (! isActive())
543            _setRenderer(renderer);
544
545        if (hasListeners())
546        {
547            ChangeSupport cs = getChangeSupport(RENDERER);
548
549            // We are originating a change event so use this
550            // constructor
551            ChangeEvent ce = new ChangeEvent(this, RENDERER,
552                                             renderer, this.renderer);
553
554            synchronized(cs)
555            {
556                cs.firePreChangeEvent(ce);
557                _setRenderer(renderer);
558                cs.firePostChangeEvent(ce);
559            }
560        }
561        else
562        {
563            _setRenderer(renderer);
564        }
565        resizeAndValidate();
566    }
567
568    /**
569     * <code>getRenderingHints</code> returns the
570     * <code>RenderingHints</code> currently being used by the
571     * <code>Graphics2D</code> instances of delegate renderers. If
572     * none is set, the constructor creates one with a null
573     * <code>Map</code>.
574     *
575     * @return a <code>RenderingHints</code>.
576     */
577    public RenderingHints getRenderingHints()
578    {
579        return hints;
580    }
581
582    /**
583     * <code>setRenderingHints</code> sets the
584     * <code>RenderingHints</code> which will be used by the
585     * <code>Graphics2D</code> instances of delegate renderers.
586     *
587     * @param hints a <code>RenderingHints</code>.
588     */
589    public void setRenderingHints(RenderingHints hints)
590    {
591        RenderingHints prevHints = this.hints;
592        this.hints = hints;
593
594        firePropertyChange("hints", prevHints, hints);
595    }
596
597    /**
598     * <code>sequenceToGraphics</code> converts a sequence index to a
599     * graphical position.
600     *
601     * @param sequencePos an <code>int</code>.
602     *
603     * @return a <code>double</code>.
604     */
605    public double sequenceToGraphics(int sequencePos)
606    {
607        return (sequencePos - translation - 1) * scale;
608    }
609
610    /**
611     * <code>graphicsToSequence</code> converts a graphical position
612     * to a sequence index.
613     *
614     * @param graphicsPos a <code>double</code>.
615     *
616     * @return an <code>int</code>.
617     */
618    public int graphicsToSequence(double graphicsPos)
619    {
620        return ((int) (graphicsPos / scale)) + translation + 1;
621    }
622
623    /**
624     * <code>graphicsToSequence</code> converts a graphical position
625     * to a sequence index.
626     *
627     * @param point the <code>Point2D</code> to transform
628     *
629     * @return an <code>int</code>.
630     */
631    public int graphicsToSequence(Point2D point)
632    {
633        if (direction == HORIZONTAL)
634            return graphicsToSequence(point.getX());
635        else
636            return graphicsToSequence(point.getY());
637    }
638
639    /**
640     * <code>getVisibleSymbolCount</code> returns the
641     * <strong>maximum</strong> number of <code>Symbol</code>s which
642     * can be rendered in the visible area (excluding all borders) of
643     * the <code>TranslatedSequencePanel</code> at the current
644     * scale. Note that if the translation is greater than 0, the
645     * actual number of <code>Symbol</code>s rendered will be less.
646     *
647     * @return an <code>int</code>.
648     */
649    public int getVisibleSymbolCount()
650    {
651        // The BioJava borders
652        double totalBorders = leadingBorder.getSize()
653            + trailingBorder.getSize() + rendererBorders;
654
655        // The Insets
656        Insets insets = getInsets();
657
658        int visible;
659
660        if (direction == HORIZONTAL)
661        {
662            int width = getWidth() - insets.left - insets.right;
663
664            if (width <= totalBorders) 
665                return 0;
666            else
667                visible = width - (int) totalBorders;
668        }
669        else
670        {
671            int height = getHeight() - insets.top - insets.bottom;
672
673            if (height <= totalBorders)
674                return 0;
675            else
676                visible = height - (int) totalBorders;
677        }
678
679        return Math.min(graphicsToSequence(visible), sequence.length());
680    }
681
682    /**
683     * <code>paintComponent</code> paints this component.
684     *
685     * @param g a <code>Graphics</code> object.
686     */
687    public void paintComponent(Graphics g)
688    {
689        if (! isActive())
690            return;
691
692        super.paintComponent(g);
693
694        // Set hints
695        Graphics2D g2 = (Graphics2D) g;
696        g2.addRenderingHints(hints);
697
698        // Save current transform and clip
699        AffineTransform prevTransform = g2.getTransform();
700        Shape                prevClip = g2.getClip();
701        Insets                 insets = getInsets();
702
703        // As we subclass JComponent we have to paint our own
704        // background, but only if we are opaque
705        if (isOpaque())
706        {
707            g2.setPaint(getBackground());
708            g2.fillRect(0, 0, getWidth(), getHeight());
709        }
710
711         Rectangle2D.Double clip = new Rectangle2D.Double();
712         if (direction == HORIZONTAL) {
713                  // Clip x to edge of delegate renderer's leader
714                  //clip.x = renderer.getMinimumLeader(this);
715                  clip.x = 0 - renderer.getMinimumLeader(this);
716                  clip.y = 0.0;
717                  // Set the width to visible symbols + the delegate
718                  // renderer's minimum trailer (which may have something in
719                  // it to render).
720                  clip.width = sequenceToGraphics(getVisibleSymbolCount() + 1) +
721                renderer.getMinimumLeader(this) + renderer.getMinimumTrailer(this);
722                  clip.height = renderer.getDepth(this);
723                  g2.translate(leadingBorder.getSize() - clip.x + insets.left, insets.top); 
724         }
725         else
726         {
727             clip.x = 0.0;
728             // Clip y to edge of delegate renderer's leader
729             clip.y = renderer.getMinimumLeader(this);
730             clip.width = renderer.getDepth(this);
731             // Set the height to visible symbols + the delegate
732             // renderer's minimum trailer (which may have something in
733             // it to render).
734             clip.height = sequenceToGraphics(getVisibleSymbolCount() + 1)
735                 + renderer.getMinimumTrailer(this);
736
737             g2.translate(insets.left, leadingBorder.getSize() + insets.top);
738         }
739
740         // Clip and paint
741         g2.clip(clip);
742         renderer.paint(g2, this);
743
744         // Restore
745         g2.setTransform(prevTransform);
746         g2.setClip(prevClip);
747    }
748
749    /**
750     * <code>resizeAndValidate</code> sets the minimum, preferred and
751     * maximum sizes of the component according to the current leading
752     * and trailing borders, renderer depth and visible symbol count.
753     */
754    public void resizeAndValidate()
755    {
756        Dimension d = null;
757
758        if (! isActive())
759        {
760            d = new Dimension(0, 0);
761        }
762        else
763        {
764            double width = sequenceToGraphics(getVisibleSymbolCount())
765                + rendererBorders;
766            double depth = renderer.getDepth(this);
767
768            if (direction == HORIZONTAL)
769            {
770                d = new Dimension((int) Math.ceil(width),
771                                  (int) Math.ceil(depth));
772            }
773            else
774            {
775                d = new Dimension((int) Math.ceil(depth),
776                                  (int) Math.ceil(width));
777            }
778        }
779
780        setMinimumSize(d);
781        setPreferredSize(d);
782        setMaximumSize(d);
783        revalidate();
784    }
785
786    /**
787     * <code>addChangeListener</code> adds a listener for all types of
788     * change.
789     *
790     * @param cl a <code>ChangeListener</code>.
791     */
792    public void addChangeListener(ChangeListener cl)
793    {
794        addChangeListener(cl, ChangeType.UNKNOWN);
795    }
796  
797    /**
798     * <code>addChangeListener</code> adds a listener for specific
799     * types of change.
800     *
801     * @param cl a <code>ChangeListener</code>.
802     * @param ct a <code>ChangeType</code>.
803     */
804    public void addChangeListener(ChangeListener cl, ChangeType ct)
805    {
806        ChangeSupport cs = getChangeSupport(ct);
807        cs.addChangeListener(cl, ct);
808    }
809  
810    /**
811     * <code>removeChangeListener</code> removes a listener.
812     *
813     * @param cl a <code>ChangeListener</code>.
814     */
815    public void removeChangeListener(ChangeListener cl)
816    {
817        removeChangeListener(cl, ChangeType.UNKNOWN);
818    }
819  
820    /**
821     * <code>removeChangeListener</code> removes a listener.
822     *
823     * @param cl a <code>ChangeListener</code>.
824     * @param ct a <code>ChangeType</code>.
825     */
826    public void removeChangeListener(ChangeListener cl, ChangeType ct)
827    {
828        if (hasListeners())
829        {
830            ChangeSupport cs = getChangeSupport(ct);
831            cs.removeChangeListener(cl, ct);
832        }
833    }
834    
835    public boolean isUnchanging(ChangeType ct)
836    {
837        ChangeSupport cs = getChangeSupport(ct);
838        return cs.isUnchanging(ct);
839    }
840
841    /**
842     * <code>addSequenceViewerListener</code> adds a listener for
843     * mouse click <code>SequenceViewerEvent</code>s.
844     *
845     * @param svl a <code>SequenceViewerListener</code>.
846     */
847    public void addSequenceViewerListener(SequenceViewerListener svl)
848    {
849        svSupport.addSequenceViewerListener(svl);
850    }
851
852    /**
853     * <code>removeSequenceViewerListener</code> removes a listener
854     * for mouse click <code>SequenceViewerEvent</code>s.
855     *
856     * @param svl a <code>SequenceViewerListener</code>.
857     */
858    public void removeSequenceViewerListener(SequenceViewerListener svl)
859    {
860        svSupport.removeSequenceViewerListener(svl);
861    }
862
863    /**
864     * <code>addSequenceViewerMotionListener</code> adds a listener for
865     * mouse motion <code>SequenceViewerEvent</code>s.
866     *
867     * @param svml a <code>SequenceViewerMotionListener</code>.
868     */
869    public void addSequenceViewerMotionListener(SequenceViewerMotionListener svml)
870    {
871        svmSupport.addSequenceViewerMotionListener(svml);
872    }
873
874    /**
875     * <code>addSequenceViewerMotionListener</code> removes a listener for
876     * mouse motion <code>SequenceViewerEvent</code>s.
877     *
878     * @param svml a <code>SequenceViewerMotionListener</code>.
879     */
880    public void removeSequenceViewerMotionListener(SequenceViewerMotionListener svml)
881    {
882        svmSupport.removeSequenceViewerMotionListener(svml);
883    }
884
885    /**
886     * <code>getChangeSupport</code> lazily instantiates a helper for
887     * change listeners.
888     *
889     * @param ct a <code>ChangeType</code>.
890     *
891     * @return a <code>ChangeSupport</code> object.
892     */
893    protected ChangeSupport getChangeSupport(ChangeType ct)
894    {
895        if (changeSupport != null)
896        {
897            return changeSupport;
898        }
899      
900        synchronized(this)
901        {
902            if (changeSupport == null)
903            {
904                changeSupport = new ChangeSupport();
905            }
906
907            return changeSupport;
908        }
909    }
910
911    /**
912     * <code>hasListeners</code> returns true if there are active
913     * listeners for BioJava events.
914     *
915     * @return a <code>boolean</code> value.
916     */
917    protected boolean hasListeners()
918    {
919        return changeSupport != null;
920    }
921
922    /**
923     * <code>isActive</code> returns true if both the
924     * <code>Sequence</code> to be rendered and the
925     * <code>SequenceRenderer</code> are not null.
926     *
927     * @return a <code>boolean</code> value.
928     */
929    protected boolean isActive()
930    {
931        return (sequence != null) && (renderer != null);
932    }
933
934    /**
935     * <code>_setRenderer</code> handles the details of listeners
936     * during changes.
937     *
938     * @param renderer a <code>SequenceRenderer</code>.
939     */
940    private void _setRenderer(SequenceRenderer renderer)
941    {
942        // Remove our listeners from the old renderer, if necessary
943        if (this.renderer != null &&
944            Changeable.class.isInstance(this.renderer))
945        {
946            Changeable c = (Changeable) this.renderer;
947
948            c.removeChangeListener(layoutListener,  SequenceRenderContext.LAYOUT);
949            c.removeChangeListener(repaintListener, SequenceRenderContext.REPAINT);
950        }
951
952        this.renderer = renderer;
953
954        // Update our cache of the renderer's total border size, but
955        // only is the sequence is not null. If the sequence was null
956        // this value is no longer correct, so we have to update
957        // rendererBorders in setSequence too.
958        if (sequence != null)
959            rendererBorders = renderer.getMinimumLeader(this)
960                + renderer.getMinimumTrailer(this);
961
962        // Add our listeners to the new renderer, if necessary
963        if (renderer != null &&
964            Changeable.class.isInstance(renderer))
965        {
966            Changeable c = (Changeable) renderer;
967            c.addChangeListener(layoutListener,  SequenceRenderContext.LAYOUT);
968            c.addChangeListener(repaintListener, SequenceRenderContext.REPAINT);
969        }
970    }
971}
972