savant.view.swing.Frame.java Source code

Java tutorial

Introduction

Here is the source code for savant.view.swing.Frame.java

Source

/**
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package savant.view.swing;

import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;

import com.jidesoft.docking.DockableFrame;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import savant.api.adapter.FrameAdapter;
import savant.api.adapter.GraphPaneAdapter;
import savant.api.data.DataFormat;
import savant.api.event.GenomeChangedEvent;
import savant.api.event.DataRetrievalEvent;
import savant.api.util.DialogUtils;
import savant.api.util.Listener;
import savant.controller.FrameController;
import savant.controller.GenomeController;
import savant.controller.LocationController;
import savant.controller.TrackController;
import savant.plugin.SavantPanelPlugin;
import savant.settings.InterfaceSettings;
import savant.util.DrawingMode;
import savant.util.Range;
import savant.util.swing.ProgressPanel;
import savant.view.icon.SavantIconFactory;
import savant.view.tracks.SequenceTrack;
import savant.view.tracks.Track;
import savant.view.tracks.TrackCreationEvent;
import savant.view.tracks.TrackFactory.TrackCreationListener;

/**
 *
 * @author mfiume, AndrewBrook
 */
public class Frame extends DockableFrame implements FrameAdapter, TrackCreationListener {

    private static final Log LOG = LogFactory.getLog(Frame.class);
    /**
     * If true, the frame's construction was halted by an error or by the user
     * cancelling.
     */
    private boolean aborted;
    /**
     * If true, the frame will be holding a sequence track, so it can be shorter
     * than usual.
     */
    private boolean sequence;
    private GraphPane graphPane;
    private JLayeredPane frameLandscape;
    private Track[] tracks = new Track[0];
    private DrawingMode initialDrawingMode;
    private JLayeredPane jlp;
    private FrameCommandBar commandBar;
    private JComponent legend;
    private JPanel sidePanel;
    private JLabel yMaxPanel;
    private Map<SavantPanelPlugin, JPanel> pluginLayers = new HashMap<SavantPanelPlugin, JPanel>();
    private String progressMessage = null;
    private double progressFraction = -1.0;
    private boolean closeable = true;

    /**
     * Construct a new Frame for holding a track.
     *
     * @param df the DataFormat, so the frame can do any format-specific
     * initialisation (e.g. smaller height for sequence tracks)
     */
    public Frame(DataFormat df) {
        super(SavantIconFactory.getInstance().getIcon(SavantIconFactory.StandardIcon.TRACK));
        sequence = df == DataFormat.SEQUENCE;

        // Component which displays the legend component.
        legend = new JComponent() {
            @Override
            public Dimension getPreferredSize() {
                for (Track t : tracks) {
                    Dimension d = t.getRenderer().getLegendSize(t.getDrawingMode());
                    if (d != null) {
                        return d;
                    }
                }
                return new Dimension(0, 0);
            }

            @Override
            public Dimension getMinimumSize() {
                return getPreferredSize();
            }

            @Override
            public void paintComponent(Graphics g) {
                for (Track t : tracks) {
                    Dimension d = t.getRenderer().getLegendSize(t.getDrawingMode());
                    if (d != null) {
                        Graphics2D g2 = (Graphics2D) g;
                        GradientPaint gp = new GradientPaint(0, 0, Color.WHITE, 0, 60, new Color(230, 230, 230));
                        g2.setPaint(gp);
                        g2.fillRect(0, 0, d.width, d.height);

                        g2.setColor(Color.BLACK);
                        g2.draw(new Rectangle2D.Double(0, 0, d.width - 1, d.height - 1));
                        t.getRenderer().drawLegend(g2, t.getDrawingMode());
                        return;
                    }
                }
            }
        };
        legend.setVisible(false);

        frameLandscape = new JLayeredPane();

        //add graphPane -> jlp -> scrollPane
        jlp = new JLayeredPane();
        jlp.setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.gridx = 0;
        gbc.gridy = 0;

        //scrollpane
        JScrollPane scrollPane = new JScrollPane();
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setWheelScrollingEnabled(false);
        scrollPane.setBorder(null);

        graphPane = new GraphPane(this);
        jlp.add(graphPane, gbc, 0);

        scrollPane.getViewport().add(jlp);

        //GRID FRAMEWORK AND COMPONENT ADDING...
        frameLandscape.setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.HORIZONTAL;

        //add sidepanel
        sidePanel = new JPanel() {
            @Override
            public Dimension getMinimumSize() {
                return new Dimension(0, 0);
            }
        };
        sidePanel.setLayout(new GridBagLayout());
        sidePanel.setOpaque(false);
        sidePanel.setVisible(false);
        c.weightx = 1.0;
        c.weighty = 1.0;
        c.fill = GridBagConstraints.BOTH;
        c.gridx = 1;
        c.gridy = 0;
        c.insets = new Insets(0, 0, 0, 16); // Leave 16 pixels so that we don't sit on top of the scroll-bar.
        frameLandscape.setLayer(sidePanel, JLayeredPane.PALETTE_LAYER);
        frameLandscape.add(sidePanel, c);

        addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                Dimension dim = getSize();
                if (dim != null) {
                    // TODO: The following shouldn't be necessary, but it seems to be.
                    int expectedWidth = frameLandscape.getWidth();
                    if (expectedWidth != graphPane.getWidth()) {
                        Dimension goodSize = new Dimension(expectedWidth, graphPane.getHeight());
                        graphPane.setPreferredSize(goodSize);
                        graphPane.setSize(goodSize);
                    }

                    setLegendVisible(true);
                }
            }
        });

        //add graphPane to all cells
        c.fill = GridBagConstraints.BOTH;
        c.weightx = 1.0;
        c.weighty = 1.0;
        c.gridx = 0;
        c.gridy = 0;
        c.gridwidth = 2;
        c.gridheight = 1;
        c.insets = new Insets(0, 0, 0, 0);

        frameLandscape.setLayer(scrollPane, JLayeredPane.DEFAULT_LAYER);
        frameLandscape.add(scrollPane, c);

        // Add our progress-panel.  If setTracks is called promptly, it will be cleared
        // away before it ever has a chance to draw.
        getContentPane().add(new ProgressPanel(null), BorderLayout.CENTER);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(100, sequence ? 100 : 200);
    }

    public JLayeredPane getFrameLandscape() {
        return frameLandscape;
    }

    @Override
    public GraphPaneAdapter getGraphPane() {
        return graphPane;
    }

    @Override
    public final Track[] getTracks() {
        return tracks;
    }

    /**
     * Set the tracks associated with this frame. Normally, this should only be
     * done once, since the Frame also uses this opportunity to set up some GUI
     * elements which depend on the presence of loaded tracks.
     *
     * @param newTracks the tracks to be displayed in this frame
     */
    public void setTracks(Track[] newTracks) {
        Track t0 = newTracks[0];
        DataFormat df = t0.getDataFormat();

        if (!GenomeController.getInstance().isGenomeLoaded() && df != DataFormat.SEQUENCE) {
            handleEvent(new TrackCreationEvent(new Exception()));
            for (Track track : newTracks) {
                TrackController.getInstance().removeTrack(track);
            }
            DialogUtils.displayError("Sorry",
                    "This does not appear to be a genome track. Please load a genome first.");
            return;
        }
        if (df == DataFormat.SEQUENCE) {
            GenomeController.getInstance().setSequence((SequenceTrack) newTracks[0]);
        }

        tracks = newTracks;
        graphPane.setTracks(tracks);

        for (Track t : tracks) {
            t.setFrame(this, initialDrawingMode);

            // Adds the track to the TrackController's internal list, and fires a TrackEvent.ADDED event to all listeners.
            TrackController.getInstance().addTrack(t);
        }

        commandBar = new FrameCommandBar(this);

        // We get the name and other properties from the zero'th track.
        setKey(t0.getName());

        if (df != DataFormat.SEQUENCE && df != DataFormat.RICH_INTERVAL) {
            yMaxPanel = new JLabel();
            yMaxPanel.setBorder(BorderFactory.createLineBorder(Color.darkGray));
            yMaxPanel.setBackground(new Color(240, 240, 240));
            yMaxPanel.setOpaque(true);
            yMaxPanel.setAlignmentX(0.5f);

            if (df == DataFormat.ALIGNMENT) {
                // We need to listen to genome changes so that we can redraw mismatches as appropriate.
                GenomeController.getInstance().addListener(new Listener<GenomeChangedEvent>() {
                    @Override
                    public void handleEvent(GenomeChangedEvent event) {
                        // In certain BAM modes, we care about whether the sequence has been set (or unset).
                        if (event.getNewGenome() == event.getOldGenome()) {
                            forceRedraw();
                        }
                    }
                });
            }
        }

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.anchor = GridBagConstraints.NORTHEAST;
        gbc.weightx = 1.0;
        gbc.insets = new Insets(4, 0, 0, 0);
        sidePanel.add(commandBar, gbc);
        sidePanel.add(legend, gbc);
        if (yMaxPanel != null) {
            sidePanel.add(yMaxPanel, gbc);
        }
        gbc.weighty = 1.0;
        gbc.fill = GridBagConstraints.VERTICAL;
        JPanel filler = new JPanel();
        filler.setOpaque(false);
        sidePanel.add(filler, gbc);

        drawTracksInRange(LocationController.getInstance().getReferenceName(),
                LocationController.getInstance().getRange());

        JPanel contentPane = (JPanel) getContentPane();
        contentPane.setLayout(new BorderLayout());
        contentPane.add(frameLandscape);
    }

    /**
     * Convenience method. More often than not, we just want the frame to host a
     * single track.
     *
     * @param newTrack the track to be used for this frame.
     */
    public void setTrack(Track newTrack) {
        setTracks(new Track[] { newTrack });
    }

    public void setActiveFrame(boolean value) {
        sidePanel.setVisible(value);
        setLegendVisible(value);
    }

    @Override
    public void setCloseable(boolean closeable) {
        if (this.closeable != closeable) {

            this.closeable = closeable;
            if (closeable) {
                setAvailableButtons(
                        DockableFrame.BUTTON_AUTOHIDE | DockableFrame.BUTTON_MAXIMIZE | DockableFrame.BUTTON_CLOSE);
            } else {
                setAvailableButtons(DockableFrame.BUTTON_AUTOHIDE | DockableFrame.BUTTON_MAXIMIZE);
            }

        }
    }

    public boolean isCloseable() {
        return closeable;
    }

    /**
     * Make the legend and ymax visible, but only if we have enough space for
     * it.
     */
    public void setLegendVisible(boolean value) {
        if (value) {
            // Need to show everything first so that sidePanel.getPreferredSize has a meaningful value.
            setYMaxVisible(true);
            legend.setVisible(!InterfaceSettings.areLegendsDisabled() && legend.getPreferredSize().height > 0);

            // We have 24 pixels of slop to allow for margins and title-bars.
            if (getHeight() < sidePanel.getPreferredSize().height + 24) {
                setYMaxVisible(false);
                legend.setVisible(false);
            }
        } else {
            setYMaxVisible(false);
            legend.setVisible(false);

        }
        legend.invalidate();
    }

    public void setInitialDrawingMode(DrawingMode dm) {
        initialDrawingMode = dm;
    }

    public void updateYMax(int value) {
        updateYMax(" ymax=%d ", value);
    }

    public void updateYMax(String format, Object... args) {
        if (yMaxPanel != null) {
            yMaxPanel.setText(String.format(format, args));
        }
    }

    /**
     * When the GraphPane's y-axis type has been updated, update the visibility
     * of the yMax value.
     */
    void setYMaxVisible(boolean flag) {
        if (yMaxPanel != null) {
            yMaxPanel.setVisible(flag);
        }
    }

    /**
     * Force the associated track to redraw. Used when the colour scheme has
     * been changed by the Preferences dialog.
     */
    @Override
    public void forceRedraw() {
        graphPane.setRenderForced();
        drawTracksInRange(LocationController.getInstance().getReferenceName(), graphPane.getXRange());
    }

    /**
     * Prepare the data for the tracks in range and fire off a repaint.
     *
     * @param range
     */
    public void drawTracksInRange(String reference, Range range) {
        if (graphPane.isLocked()) {
            return;
        }

        graphPane.setXRange(range);

        for (Track t : tracks) {
            t.getRenderer().clearInstructions();
            t.prepareForRendering(reference, range);
        }
        graphPane.repaint();
    }

    /**
     * Tells the frame that the draw mode for this track has changed. Gives the
     * frame a chance to rebuild its user interface and request a repaint.
     *
     * @param t the track whose mode has changed
     */
    @Override
    public void drawModeChanged(Track t) {
        DrawingMode mode = t.getDrawingMode();
        commandBar.drawModeChanged(mode);
        setLegendVisible(true);
        validate();
        drawTracksInRange(LocationController.getInstance().getReferenceName(),
                LocationController.getInstance().getRange());
    }

    /**
     * Get a panel for a plugin to draw on. If necessary, a new one will be
     * created.
     */
    @Override
    public JPanel getLayerCanvas(SavantPanelPlugin plugin, boolean mayCreate) {
        JPanel p = pluginLayers.get(plugin);
        if (p == null && mayCreate) {
            p = new JPanel();
            p.setOpaque(false);
            GridBagConstraints c = new GridBagConstraints();
            c.fill = GridBagConstraints.BOTH;
            c.weightx = 1.0;
            c.weighty = 1.0;
            c.gridx = 0;
            c.gridy = 0;
            c.gridwidth = 3;
            c.gridheight = 2;
            jlp.add(p, c, 2);
            jlp.setLayer(p, 50);
            pluginLayers.put(plugin, p);
            if (plugin != null) {
                p.setVisible(plugin.isVisible());
            }
        }
        return p;
    }

    /**
     * Export this frame as an image.
     */
    public BufferedImage frameToImage(int baseSelected) {
        BufferedImage bufferedImage = new BufferedImage(graphPane.getWidth(), graphPane.getHeight(),
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = bufferedImage.createGraphics();
        graphPane.setRenderForced();
        graphPane.forceFullHeight();
        graphPane.render(g2);
        graphPane.unforceFullHeight();
        g2.setColor(Color.black);
        if (baseSelected > 0) {
            double h = (double) graphPane.getHeight();
            double spos = graphPane.transformXPos(baseSelected);
            g2.draw(new Line2D.Double(spos, 0.0, spos, h));
            double rpos = graphPane.transformXPos(baseSelected + 1);
            g2.draw(new Line2D.Double(rpos, 0.0, rpos, h));
        }
        g2.setFont(new Font(null, Font.BOLD, 13));
        g2.drawString(tracks[0].getName(), 2, 15);
        return bufferedImage;
    }

    @Override
    public void handleEvent(DataRetrievalEvent evt) {
        // We're only interested in the event if it's for the range we're looking at.  This prevents
        // moving around on the VariantMap from repainting our variant tracks.
        if (LocationController.getInstance().getRange().equals(evt.getRange())) {
            switch (evt.getType()) {
            case COMPLETED:
                LOG.trace("Frame " + getKey() + " received dataRetrievalCompleted.  Forcing full render.");
                setYMaxVisible(evt.getData() != null && evt.getData().size() > 0);
                graphPane.setRenderForced();
                graphPane.repaint();
                break;
            case FAILED:
                LOG.trace("Frame " + getKey() + " received dataRetrievalFailed.  Forcing full render.");
                setYMaxVisible(false);
                graphPane.setRenderForced();
                graphPane.repaint();
                break;
            }
        } else {
            LOG.debug("Rejecting DataRetrievalEvent." + evt.getType() + " for " + evt.getRange()
                    + " because real range=" + LocationController.getInstance().getRange());
        }
    }

    @Override
    public void handleEvent(TrackCreationEvent evt) {
        switch (evt.getType()) {
        case PROGRESS:
            progressMessage = evt.getProgressMessage();
            progressFraction = evt.getProgressFraction();
            updateProgress();
            break;
        case COMPLETED:
            if (!aborted) {
                setTracks(evt.getTracks());
            }
            break;
        case FAILED:
            aborted = true;
            FrameController.getInstance().closeFrame(this, false);
            break;
        }
    }

    @Override
    public int getIntervalHeight() {
        return commandBar.getIntervalHeight();
    }

    @Override
    public void setHeightFromSlider() {
        int h = commandBar.getIntervalHeight();
        graphPane.setUnitHeight(h);
        graphPane.setScaledToFit(false);
    }

    /**
     * GraphPane wants to know what progress message to display.
     */
    public void updateProgress() {
        if (progressMessage == null) {
            graphPane.showProgress("Creating track...", -1.0);
        } else {
            graphPane.showProgress(progressMessage, progressFraction);
        }
    }
}