Project: Clotho-Core
/*
 * Copyright (c) 2007, Romain Guy 
 * All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met: 
 * 
 *   * Redistributions of source code must retain the above copyright 
 *     notice, this list of conditions and the following disclaimer. 
 *   * Redistributions in binary form must reproduce the above 
 *     copyright notice, this list of conditions and the following 
 *     disclaimer in the documentation and/or other materials provided 
 *     with the distribution. 
 *   * Neither the name of the TimingFramework project nor the names of its 
 *     contributors may be used to endorse or promote products derived 
 *     from this software without specific prior written permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
 
package org.clothocore.tool.pluginmanager.gui; 
 
import java.awt.AlphaComposite; 
import java.awt.Color; 
import java.awt.Composite; 
import java.awt.Cursor; 
import java.awt.Dimension; 
import java.awt.Font; 
import java.awt.GradientPaint; 
import java.awt.Graphics; 
import java.awt.Graphics2D; 
import java.awt.GridBagLayout; 
import java.awt.Image; 
import java.awt.Insets; 
import java.awt.Paint; 
import java.awt.Rectangle; 
import java.awt.RenderingHints; 
import java.awt.event.ActionEvent; 
import java.awt.event.ActionListener; 
import java.awt.event.ComponentAdapter; 
import java.awt.event.ComponentEvent; 
import java.awt.event.KeyAdapter; 
import java.awt.event.KeyEvent; 
import java.awt.event.MouseAdapter; 
import java.awt.event.MouseEvent; 
import java.awt.event.MouseMotionAdapter; 
import java.awt.event.MouseWheelEvent; 
import java.awt.event.MouseWheelListener; 
import java.awt.font.FontRenderContext; 
import java.awt.font.TextLayout; 
import java.awt.geom.AffineTransform; 
import java.awt.geom.Area; 
import java.awt.geom.Ellipse2D; 
import java.awt.geom.Rectangle2D; 
import java.awt.geom.RoundRectangle2D; 
import java.awt.image.BufferedImage; 
 
import java.util.HashMap; 
import org.clothocore.api.core.Collator; 
 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.Hashtable; 
import java.util.LinkedList; 
import java.util.List; 
 
import javax.swing.JPanel; 
import javax.swing.Timer; 
import org.clothocore.api.data.ObjType; 
 
import org.clothocore.util.misc.BareBonesBrowserLaunch; 
import org.clothocore.util.basic.ImageSource; 
 
 
/**
 * This is the panel with the images and the button label above them 
 * @author jcanderson 
 */
 
public class ChoosePreferredViewers extends JPanel implements FancyPanel { 
    private Hashtable<Integer, String> labels = new Hashtable<Integer, String>(); 
    private Hashtable<String, String> descriptions = new Hashtable<String, String>(); 
    private RightClickPopup rcPopup; 
    private static final double ANIM_SCROLL_DELAY = 450
 
    private List<Image> avatars = null
 
    private boolean loadingDone = false
 
    private Thread picturesFinder = null
    private Timer scrollerTimer = null
    private Timer faderTimer = null
    private Timer passwordTimer; 
 
    private float veilAlphaLevel = 0.0f
    private float alphaLevel = 0.0f
    private float textAlphaLevel = 0.0f
 
    private int avatarIndex; 
    private double avatarPosition = 0.0
    private double avatarSpacing = 0.4
    private int avatarAmount = 5
 
    private double sigma; 
    private double rho; 
 
    private double exp_multiplier; 
    private double exp_member; 
 
    private boolean damaged = true
 
    private DrawableAvatar[] drawableAvatars; 
    private String textAvatar; 
    private String descriptionAvatar; 
 
    private FocusGrabber focusGrabber; 
    private AvatarScroller avatarScroller; 
    private CursorChanger cursorChanger; 
    private MouseWheelScroller wheelScroller; 
    private KeyScroller keyScroller; 
    private ApplicationFrame _app; 
 
    public ChoosePreferredViewers(ApplicationFrame app) { 
        _app = app; 
        descriptions.put("PART""Compositional primitives such as BioBricks"); 
        descriptions.put("VECTOR""Backbone portion of a DNA composition"); 
        descriptions.put("PLASMID""A complete DNA composition"); 
        descriptions.put("OLIGO""Theoretical oligonucleotides"); 
        descriptions.put("NOTE""Information related to a topic including factoids"); 
        descriptions.put("FACTOID""Nugget of information related to a reference"); 
        descriptions.put("CONTAINER""Vessels containing samples such as barcoded wells and microcentrifuge tubes"); 
        descriptions.put("SAMPLE""Liquid containing a biological entity"); 
        descriptions.put("SAMPLE_DATA""Sangar sequencing read with a .abi file"); 
        descriptions.put("PERSON""A human being"); 
        descriptions.put("LAB""Group of people that do research"); 
        descriptions.put("INSTITUTION""University, college, institute, or company"); 
        descriptions.put("PLATE""A vessel with arrayed containers"); 
        descriptions.put("PLATE_TYPE""Physical configuration and appearance of a plate"); 
        descriptions.put("FORMAT""Compositional standards such as RFC10"); 
        descriptions.put("STRAIN""Cells or other clonal biological species"); 
        descriptions.put("FEATURE""Biological primitives such as a GFP coding sequence"); 
        descriptions.put("FAMILY""Biological ontology relationships between features"); 
        descriptions.put("GRAMMAR""Grammatical rules for combining parts"); 
        descriptions.put("COLLECTION""User-defined list of related objects"); 
        descriptions.put("NUCSEQ""DNA or RNA sequence"); 
        descriptions.put("ANNOTATION""A labeled region of a sequence, often with a feature"); 
        descriptions.put("WIKITEXT""A formatted text region with images and attachments"); 
        descriptions.put("ATTACHMENT""An image, genbank file, or any other file type"); 
        descriptions.put("FLEX_FIELD""Custom data for plugins"); 
 
 
        GridBagLayout layout = new GridBagLayout(); 
        setLayout(layout); 
 
        findAvatars(); 
        setSigma(0.5); 
 
        addComponentListener(new DamageManager()); 
 
        initInputListeners(); 
        addInputListeners(); 
    } 
 
    public void setAmount(int amount) { 
        if (amount > avatars.size()) { 
            throw new IllegalArgumentException("Too many avatars"); 
        } 
 
        this.avatarAmount = amount; 
        repaint(); 
    } 
 
    // XXX package access for debugging purpose only 
    public void setPosition(double position) { 
        this.avatarPosition = position; 
        this.damaged = true
        repaint(); 
    } 
 
    public void setSigma(double sigma) { 
        this.sigma = sigma; 
        this.rho = 1.0
        computeEquationParts(); 
        this.rho = computeModifierUnprotected(0.0); 
        computeEquationParts(); 
        this.damaged = true
        repaint(); 
    } 
 
    public void setSpacing(double avatarSpacing) { 
        if (avatarSpacing < 0.0 || avatarSpacing > 1.0) { 
            throw new IllegalArgumentException("Spacing must be < 1.0 and > 0.0"); 
        } 
 
        this.avatarSpacing = avatarSpacing; 
        this.damaged = true
        repaint(); 
    } 
 
    @Override 
    public Dimension getPreferredSize() { 
        return new Dimension(128 * 3128 * 2); 
    } 
 
    @Override 
    public Dimension getMaximumSize() { 
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); 
    } 
 
    @Override 
    public boolean isOpaque() { 
        return false
    } 
 
    @Override 
    public boolean isFocusable() { 
        return true
    } 
 
    @Override 
    protected void paintChildren(Graphics g) { 
        Graphics2D g2 = (Graphics2D) g; 
 
        Composite oldComposite = g2.getComposite(); 
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
                                                   veilAlphaLevel)); 
        super.paintChildren(g); 
        g2.setComposite(oldComposite); 
    } 
 
    @Override 
    protected void paintComponent(Graphics g) { 
        super.paintComponent(g); 
 
        if (!loadingDone && faderTimer == null) { 
            return
        } 
 
        Insets insets = getInsets(); 
 
        int x = insets.left; 
        int y = insets.top; 
 
        int width = getWidth() - insets.left - insets.right; 
        int height = getHeight() - insets.top - insets.bottom; 
 
        Graphics2D g2 = (Graphics2D) g; 
        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, 
                            RenderingHints.VALUE_INTERPOLATION_BILINEAR); 
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                            RenderingHints.VALUE_ANTIALIAS_ON); 
        Composite oldComposite = g2.getComposite(); 
 
        if (damaged) { 
            drawableAvatars = sortAvatarsByDepth(x, y, width, height); 
            damaged = false
        } 
 
        drawAvatars(g2, drawableAvatars); 
 
        if (drawableAvatars.length > 0) { 
            drawAvatarName(g2); 
        } 
 
        g2.setComposite(oldComposite); 
    } 
 
    private void drawAvatars(Graphics2D g2, DrawableAvatar[] drawableAvatars) { 
        for (DrawableAvatar avatar: drawableAvatars) { 
            AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,  
                                                                  (float) avatar.getAlpha()); 
            g2.setComposite(composite); 
            g2.drawImage(avatars.get(avatar.getIndex()), 
                         (int) avatar.getX(), (int) avatar.getY(), 
                         avatar.getWidth(), avatar.getHeight(), null); 
        } 
    } 
 
    private DrawableAvatar[] sortAvatarsByDepth(int x, int y, 
                                                int width, int height) { 
        List<DrawableAvatar> drawables = new LinkedList<DrawableAvatar>(); 
        for (int i = 0; i < avatars.size(); i++) { 
            promoteAvatarToDrawable(drawables, 
                                    x, y, width, height, i - avatarIndex); 
        } 
 
        DrawableAvatar[] drawable_avatars = new DrawableAvatar[drawables.size()]; 
        drawable_avatars = drawables.toArray(drawable_avatars); 
        Arrays.sort(drawable_avatars); 
        return drawable_avatars; 
    } 
 
    //This is the label over the images, or at least where its size is set 
    private void drawAvatarName(Graphics2D g2) { 
        Composite composite = g2.getComposite(); 
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
                                                   textAlphaLevel)); 
        //This is the height and width of the little bullet 
        double bulletWidth = 150.0
        double bulletHeight = 30.0
 
        double x = (getWidth() - bulletWidth) / 2.0
        double y = (getHeight() - 154) / 2.0 - bulletHeight * 1.4
 
        drawAvatarBullet(g2, x, y, bulletWidth, bulletHeight); 
        drawAvatarText(g2, y, bulletHeight); 
        drawDescriptionText(g2, y, bulletHeight); 
 
        g2.setComposite(composite); 
    } 
 
    //Writes the actual text of the avatar.  textAvatar is the String it writes 
    private void drawAvatarText(Graphics2D g2, double y, double bulletHeight) { 
        FontRenderContext context = g2.getFontRenderContext(); 
        Font font = new Font("Dialog", Font.PLAIN, 18); 
        TextLayout layout = new TextLayout(textAvatar, font, context); 
        Rectangle2D bounds = layout.getBounds(); 
 
        float text_x = (float) ((getWidth() - bounds.getWidth()) / 2.0); 
        float text_y = (float) (y + (bulletHeight - layout.getAscent() -  
                                     layout.getDescent()) / 2.0) + 
                                     layout.getAscent() - layout.getLeading(); 
 
        g2.setColor(Color.BLACK); 
        layout.draw(g2, text_x, text_y + 1); 
        g2.setColor(Color.WHITE); 
        layout.draw(g2, text_x, text_y); 
    } 
 
    //Writes the Description, added by JCA 
    private void drawDescriptionText(Graphics2D g2, double y, double bulletHeight) { 
        FontRenderContext context = g2.getFontRenderContext(); 
        Font font = new Font("Dialog", Font.PLAIN, 15); 
        TextLayout layout = new TextLayout(descriptionAvatar, font, context); 
        Rectangle2D bounds = layout.getBounds(); 
 
        float text_x = (float) ((getWidth() - bounds.getWidth()) / 2.0); 
        float text_y = (float) (y + (bulletHeight - layout.getAscent() - 
                                     layout.getDescent()) / 2.0) + 
                                     layout.getAscent() - layout.getLeading() + 40
 
        g2.setColor(Color.BLACK); 
        layout.draw(g2, text_x, text_y + 1); 
        g2.setColor(new Color(180,180,180)); 
        layout.draw(g2, text_x, text_y); 
    } 
 
    //JCA this is the little label over the images with the name like "part" 
    private void drawAvatarBullet(Graphics2D g2, 
                                  double x, double y, 
                                  double bulletWidth, double bulletHeight) { 
        RoundRectangle2D bullet = new RoundRectangle2D.Double(0.00.0
                                                              bulletWidth, bulletHeight, 
                                                              bulletHeight, bulletHeight); 
        Ellipse2D curve = new Ellipse2D.Double(-20.0, bulletHeight / 2.0
                                               bulletWidth + 120.0, bulletHeight); 
 
        g2.translate(x, y); 
 
        g2.translate(-1, -2); 
        g2.setColor(new Color(000170)); 
        g2.fill(new RoundRectangle2D.Double(0.00.0
                                            bulletWidth + 2, bulletHeight + 4
                                            bulletHeight + 4, bulletHeight + 4)); 
        g2.translate(12); 
 
        Color startColor = new Color(303030); 
        Color endColor = new Color(798377); 
 
        Paint paint = g2.getPaint(); 
        g2.setPaint(new GradientPaint(0.0f0.0f, startColor, 
                                      0.0f, (float) bulletHeight, endColor)); 
        g2.fill(bullet); 
 
        startColor = new Color(505050); 
        endColor = new Color(707070); 
        g2.setPaint(new GradientPaint(0.0f0.0f, startColor, 
                                      0.0f, (float) bulletHeight, endColor)); 
 
        Area area = new Area(bullet); 
        area.intersect(new Area(curve)); 
        g2.fill(area); 
 
        g2.setPaint(paint); 
        g2.translate(-x, -y); 
    } 
 
    private void promoteAvatarToDrawable(List<DrawableAvatar> drawables, 
                                         int x, int y, int width, int height, 
                                         int offset) { 
        double spacing = offset * avatarSpacing; 
        double avatarPos = this.avatarPosition + spacing; 
 
        if (avatarIndex + offset < 0 || 
            avatarIndex + offset >= avatars.size()) { 
            return
        } 
 
        Image avatar = avatars.get(avatarIndex + offset); 
 
        int avatarWidth = avatar.getWidth(null); 
        int avatarHeight = avatar.getHeight(null); 
 
        double result = computeModifier(avatarPos); 
 
        int newWidth = (int) (avatarWidth * result); 
        if (newWidth == 0) { 
            return
        } 
 
        int newHeight = (int) (avatarHeight * result); 
        if (newHeight == 0) { 
            return
        } 
 
        //This is what determines the position of the avatars on the JFrame, is all relative to size of frame 
        double avatar_x = x + (width - newWidth) / 2.0
        double avatar_y = y + (height - newHeight / 2.0) / 1.7
 
        double semiWidth = width / 2.0
 
        avatar_x += avatarPos * semiWidth; 
 
        if (avatar_x >= width || avatar_x < -newWidth) { 
            return
        } 
 
        drawables.add(new DrawableAvatar(avatarIndex + offset, 
                                         avatar_x, avatar_y, 
                                         newWidth, newHeight, 
                                         avatarPos, result)); 
    } 
 
    private void startFader() { 
        faderTimer = new Timer(35new FaderAction()); 
        faderTimer.start(); 
    } 
     
    private void computeEquationParts() { 
        exp_multiplier = Math.sqrt(2.0 * Math.PI) / sigma / rho; 
        exp_member = 4.0 * sigma * sigma; 
    } 
 
    // XXX package access for debug purpose only 
    public double computeModifier(double x) { 
        double result = computeModifierUnprotected(x); 
        if (result > 1.0) { 
            result = 1.0
        } else if (result < -1.0) { 
            result = -1.0
        } 
 
        return result; 
    } 
 
    private double computeModifierUnprotected(double x) { 
        return exp_multiplier * Math.exp((-x * x) / exp_member); 
    } 
 
    private void addInputListeners() { 
        addMouseListener(focusGrabber); 
        addMouseListener(avatarScroller); 
        addMouseMotionListener(cursorChanger); 
        addMouseWheelListener(wheelScroller); 
        addKeyListener(keyScroller); 
    } 
 
    private void initInputListeners() { 
        // input listeners are all stateless 
        // hence they can be instantiated once 
        focusGrabber = new FocusGrabber(); 
        avatarScroller = new AvatarScroller(); 
        cursorChanger = new CursorChanger(); 
        wheelScroller = new MouseWheelScroller(); 
        keyScroller = new KeyScroller(); 
    } 
 
    private void removeInputListeners() { 
        removeMouseListener(focusGrabber); 
        removeMouseListener(avatarScroller); 
        removeMouseMotionListener(cursorChanger); 
        removeMouseWheelListener(wheelScroller); 
        removeKeyListener(keyScroller); 
    } 
 
    private void findAvatars() { 
        avatars = new ArrayList<Image>(); 
 
        picturesFinder = new Thread(new PicturesFinderThread()); 
        picturesFinder.start(); 
    } 
 
    private void setAvatarIndex(int index) { 
        avatarIndex = index; 
       //This is where the label gets set 
        textAvatar = labels.get(index); 
        descriptionAvatar = descriptions.get(textAvatar); 
    } 
 
    private void scrollBy(int increment) { 
        if (loadingDone) { 
            setAvatarIndex(avatarIndex + increment); 
 
            if (avatarIndex < 0) { 
                setAvatarIndex(0); 
            } else if (avatarIndex >= avatars.size()) { 
                setAvatarIndex(avatars.size() - 1); 
            } 
 
            damaged = true
            repaint(); 
        } 
    } 
     
    private void scrollAndAnimateBy(int increment) { 
        if (loadingDone && (scrollerTimer == null || !scrollerTimer.isRunning())) { 
            int index = avatarIndex + increment; 
            if (index < 0) { 
                index = 0
            } else if (index >= avatars.size()) { 
                index = avatars.size() - 1
            } 
             
            DrawableAvatar drawable = null
 
            for (DrawableAvatar avatar: drawableAvatars) { 
                if (avatar.index == index) { 
                    drawable = avatar; 
                    break
                } 
            } 
 
            if (drawable != null) { 
                scrollAndAnimate(drawable); 
            } 
        } 
    } 
 
    private void scrollAndAnimate(DrawableAvatar avatar) { 
        if (loadingDone) { 
            scrollerTimer = new Timer(10new AutoScroller(avatar)); 
            scrollerTimer.start(); 
        } 
    } 
 
    private BufferedImage createReflectedPicture(BufferedImage avatar) { 
        int avatarWidth = avatar.getWidth(); 
        int avatarHeight = avatar.getHeight(); 
 
        BufferedImage alphaMask = createGradientMask(avatarWidth, avatarHeight); 
 
        return createReflectedPicture(avatar, alphaMask); 
    } 
 
    private BufferedImage createReflectedPicture(BufferedImage avatar, 
                                                 BufferedImage alphaMask) { 
        int avatarWidth = avatar.getWidth(); 
        int avatarHeight = avatar.getHeight(); 
 
        BufferedImage buffer = createReflection(avatar, 
                                                avatarWidth, avatarHeight); 
        applyAlphaMask(buffer, alphaMask, avatarWidth, avatarHeight); 
 
        return buffer; 
    } 
 
    private void applyAlphaMask(BufferedImage buffer, 
                                BufferedImage alphaMask, 
                                int avatarWidth, int avatarHeight) { 
        Graphics2D g2 = buffer.createGraphics(); 
        g2.setComposite(AlphaComposite.DstOut); 
        g2.drawImage(alphaMask, null0, avatarHeight); 
        g2.dispose(); 
    } 
 
    private BufferedImage createReflection(BufferedImage avatar, 
                                           int avatarWidth, 
                                           int avatarHeight) { 
        BufferedImage buffer = new BufferedImage(avatarWidth, avatarHeight << 1
                                                 BufferedImage.TYPE_INT_ARGB); 
 
        Graphics2D g = buffer.createGraphics(); 
        g.drawImage(avatar, nullnull); 
        g.translate(0, avatarHeight << 1); 
 
        AffineTransform reflectTransform = AffineTransform.getScaleInstance(1.0, -1.0); 
        g.drawImage(avatar, reflectTransform, null); 
        g.translate(0, -(avatarHeight << 1)); 
 
        g.dispose(); 
 
        return buffer; 
    } 
 
    private BufferedImage createGradientMask(int avatarWidth, int avatarHeight) { 
        BufferedImage gradient = new BufferedImage(avatarWidth, avatarHeight, 
                                                   BufferedImage.TYPE_INT_ARGB); 
        Graphics2D g = gradient.createGraphics(); 
        GradientPaint painter = new GradientPaint(0.0f0.0f
                                                  new Color(1.0f1.0f1.0f0.5f), 
                                                  0.0f, avatarHeight / 2.0f
                                                  new Color(1.0f1.0f1.0f1.0f)); 
        g.setPaint(painter); 
        g.fill(new Rectangle2D.Double(00, avatarWidth, avatarHeight)); 
 
        g.dispose(); 
 
        return gradient; 
    } 
 
    private DrawableAvatar getHitAvatar(int x, int y) { 
        for (DrawableAvatar avatar: drawableAvatars) { 
            Rectangle hit = new Rectangle((int) avatar.getX(), (int) avatar.getY(), 
                                          avatar.getWidth(), avatar.getHeight() / 2); 
            if (hit.contains(x, y)) { 
                return avatar; 
            } 
        } 
 
        return null
    } 
 
    private class PicturesFinderThread implements Runnable { 
        @Override 
        public void run() { 
 
            HashMap<ObjType, BufferedImage> imgHash = ImageSource.getObjectImageSet(100); 
             
            for (int i = 0; i < imgHash.size(); i++) { 
                BufferedImage image = imgHash.get(ObjType.values()[i]); 
                avatars.add(createReflectedPicture(image)); 
                String filename = ObjType.values()[i].toString() + ".png"
                labels.put(i, ObjType.values()[i].toString() ); 
 
                if (i == (imgHash.size() / 2) + avatarAmount / 2) { 
                    setAvatarIndex(i - avatarAmount / 2); 
                    startFader(); 
                } 
            } 
 
            loadingDone = true
        } 
    } 
 
    private class FaderAction implements ActionListener { 
        private long start = 0
 
        private FaderAction() { 
            alphaLevel = 0.0f
            textAlphaLevel = 0.0f
        } 
 
        @Override 
        public void actionPerformed(ActionEvent e) { 
            if (start == 0) { 
                start = System.currentTimeMillis(); 
            } 
 
            alphaLevel = (System.currentTimeMillis() - start) / 500.0f
            textAlphaLevel = alphaLevel; 
 
            if (alphaLevel > 1.0f) { 
                alphaLevel = 1.0f
                textAlphaLevel = 1.0f
                faderTimer.stop(); 
            } 
 
            repaint(); 
        } 
    } 
 
    private class DrawableAvatar implements Comparable { 
        private int index; 
        private double x; 
        private double y; 
        private int width; 
        private int height; 
        private double zOrder; 
        private double position; 
 
        private DrawableAvatar(int index, 
                               double x, double y, int width, int height, 
                               double position, double zOrder) { 
            this.index = index; 
            this.x = x; 
            this.y = y; 
            this.width = width; 
            this.height = height; 
            this.position = position; 
            this.zOrder = zOrder; 
        } 
 
        public int compareTo(Object o) { 
            double zOrder2 = ((DrawableAvatar) o).zOrder; 
 
            if (zOrder < zOrder2) { 
                return -1
            } else if (zOrder > zOrder2) { 
                return 1
            } 
            return 0
        } 
        
        public double getPosition() { 
            return position; 
        } 
 
        public double getAlpha() { 
            return zOrder * alphaLevel; 
        } 
 
        public int getHeight() { 
            return height; 
        } 
 
        public int getIndex() { 
            return index; 
        } 
 
        public int getWidth() { 
            return width; 
        } 
 
        public double getX() { 
            return x; 
        } 
 
        public double getY() { 
            return y; 
        } 
    } 
 
    private class MouseWheelScroller implements MouseWheelListener { 
        public void mouseWheelMoved(MouseWheelEvent e) { 
            int increment = e.getWheelRotation(); 
            scrollAndAnimateBy(increment); 
        } 
    } 
 
    private class KeyScroller extends KeyAdapter { 
        @Override 
        public void keyPressed(KeyEvent e) { 
            int keyCode = e.getKeyCode(); 
            switch (keyCode) { 
                case KeyEvent.VK_LEFT: 
                case KeyEvent.VK_UP: 
                    scrollAndAnimateBy(-1); 
                    break
                case KeyEvent.VK_RIGHT: 
                case KeyEvent.VK_DOWN: 
                    scrollAndAnimateBy(1); 
                    break
                case KeyEvent.VK_END: 
                    scrollBy(avatars.size() - avatarIndex - 1); 
                    break
                case KeyEvent.VK_HOME: 
                    scrollBy(-avatarIndex - 1); 
                    break
                case KeyEvent.VK_PAGE_UP: 
                    scrollAndAnimateBy(-avatarAmount / 2); 
                    break
                case KeyEvent.VK_PAGE_DOWN: 
                    scrollAndAnimateBy(avatarAmount / 2); 
                    break
            } 
        } 
    } 
 
    private class FocusGrabber extends MouseAdapter { 
        @Override 
        public void mouseClicked(MouseEvent e) { 
            requestFocus(); 
        } 
    } 
 
    private class AvatarScroller extends MouseAdapter { 
        @Override 
        public void mouseClicked(MouseEvent e) { 
            if(rcPopup!=null) { 
                rcPopup.remove(); 
            } 
            if ((faderTimer != null && faderTimer.isRunning()) || 
                (scrollerTimer != null && scrollerTimer.isRunning()) || 
                drawableAvatars == null) { 
                return
            } 
 
            //Scroll to that avatar 
            DrawableAvatar avatar = getHitAvatar(e.getX(), e.getY()); 
            if (avatar != null && avatar.getIndex() != avatarIndex) { 
                scrollAndAnimate(avatar); 
                return
            } 
 
            if(e.getY()<50) { 
                _app.close(); 
            } 
 
            //If it's a double-click, open the help page describing the part type 
            if(e.getClickCount()==2) { 
                //BareBonesBrowserLaunch.openURL(Collator.helpURLBase + "help/ObjectDescriptions/" + textAvatar + ".html"); 
                BareBonesBrowserLaunch.openURL(Collator.helpURLBase + "/" + textAvatar.toLowerCase()); 
                return
            } 
 
            //If it's a right click, make a popup window and allow user to put in default viewer 
            String objtypestring = textAvatar; 
            if(objtypestring.equals("sequenceRead")) { 
                objtypestring = "SEQUENCE_READ"
            } 
            if(objtypestring.equals("plateType")) { 
                objtypestring = "PLATE_TYPE"
            } 
            if(e.getModifiers()==4) { 
                rcPopup = new RightClickPopup(e.getLocationOnScreen().x, e.getLocationOnScreen().y, objtypestring); 
                return
            } 
 
        } 
    } 
 
    private class DamageManager extends ComponentAdapter { 
        @Override 
        public void componentResized(ComponentEvent e) { 
            damaged = true
        } 
    } 
 
    private class AutoScroller implements ActionListener { 
        private double position; 
        private int index; 
        private long start; 
 
        private AutoScroller(DrawableAvatar avatar) { 
            this.index = avatar.getIndex(); 
            this.position = avatar.getPosition(); 
            this.start = System.currentTimeMillis(); 
        } 
 
        public void actionPerformed(ActionEvent e) { 
            long elapsed = System.currentTimeMillis() - start; 
            if (elapsed < ANIM_SCROLL_DELAY / 2.0) { 
                textAlphaLevel = (float) (1.0 - 2.0 * (elapsed / ANIM_SCROLL_DELAY)); 
            } else { 
                textAlphaLevel = (float) (((elapsed / ANIM_SCROLL_DELAY) - 0.5) * 2.0); 
                if (textAlphaLevel > 1.0f) { 
                    textAlphaLevel = 1.0f
                } 
            } 
 
            if (textAlphaLevel < 0.1f) { 
                textAlphaLevel = 0.1f
                textAvatar = " "
            } 
 
            double newPosition = (elapsed / ANIM_SCROLL_DELAY) * -position; 
 
            if (elapsed >= ANIM_SCROLL_DELAY) { 
                ((Timer) e.getSource()).stop(); 
 
                setAvatarIndex(index); 
                setPosition(0.0); 
                return
            } 
 
            setPosition(newPosition); 
        } 
    } 
 
    private class CursorChanger extends MouseMotionAdapter { 
        @Override 
        public void mouseMoved(MouseEvent e) { 
            if ((scrollerTimer != null && scrollerTimer.isRunning()) || 
                drawableAvatars == null) { 
                return
            } 
 
            DrawableAvatar avatar = getHitAvatar(e.getX(), e.getY()); 
            if (avatar != null) { 
                getParent().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 
            } else { 
                getParent().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 
            } 
        } 
    } 
}