/*
* Copyright (c) 2007-2012 The Broad Institute, Inc.
* SOFTWARE COPYRIGHT NOTICE
* This software and its documentation are the copyright of the Broad Institute, Inc. All rights are reserved.
*
* This software is supplied without any warranty or guaranteed support whatsoever. The Broad Institute is not responsible for its use, misuse, or functionality.
*
* This software is licensed under the terms of the GNU Lesser General Public License (LGPL),
* Version 2.1 which is available at http://www.opensource.org/licenses/lgpl-2.1.php.
*/
/*
* TrackPanel.java
*
* Created on Sep 5, 2007, 4:09:39 PM
*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.broad.igv.ui.panel;
import com.google.common.base.Objects;
import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.PreferenceManager;
import org.broad.igv.feature.RegionOfInterest;
import org.broad.igv.track.*;
import org.broad.igv.ui.AbstractDataPanelTool;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.UIConstants;
import org.broad.igv.ui.WaitCursorManager;
import org.broad.igv.ui.util.DataPanelTool;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.text.DecimalFormat;
import java.util.*;
import java.util.List;
/**
* The batch panel for displaying tracks and data. A DataPanel is always associated with a ReferenceFrame. Normally
* there is a single reference frame (and thus panel), but when "gene list" or other split screen views are
* invoked there can be multiple panels.
*
* @author jrobinso
*/
public class DataPanel extends JComponent implements Paintable {
private static Logger log = Logger.getLogger(DataPanel.class);
private boolean isWaitingForToolTipText = false;
private DataPanelTool defaultTool;
private DataPanelTool currentTool;
// private Point tooltipTextPosition;
private ReferenceFrame frame;
DataPanelContainer parent;
private DataPanelPainter painter;
private String tooltipText = "";
public DataPanel(ReferenceFrame frame, DataPanelContainer parent) {
init();
this.defaultTool = new PanTool(this);
this.currentTool = defaultTool;
this.frame = frame;
this.parent = parent;
setFocusable(true);
setAutoscrolls(true);
setToolTipText("");
painter = new DataPanelPainter();
setBackground(PreferenceManager.getInstance().getAsColor(PreferenceManager.BACKGROUND_COLOR));
ToolTipManager.sharedInstance().registerComponent(this);
}
/**
* @return
*/
public JScrollBar getVerticalScrollbar() {
Component sp = getParent();
while (sp != null && !(sp instanceof JScrollPane)) {
sp = sp.getParent();
}
return sp == null ? null : ((JScrollPane) sp).getVerticalScrollBar();
}
public void setCurrentTool(final AbstractDataPanelTool tool) {
this.currentTool = (tool == null) ? defaultTool : tool;
if (currentTool != null) {
setCursor(currentTool.getCursor());
}
}
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
RenderContext context = null;
try {
Rectangle clipBounds = g.getClipBounds();
final Rectangle visibleRect = getVisibleRect();
final Rectangle damageRect = clipBounds == null ? visibleRect : clipBounds.intersection(visibleRect);
Graphics2D graphics2D = (Graphics2D) g; //(Graphics2D) g.create();
context = new RenderContextImpl(this, graphics2D, frame, visibleRect);
if (Globals.IS_MAC) {
this.applyMacPerformanceHints((Graphics2D) g);
}
final Collection<TrackGroup> groups = parent.getTrackGroups();
// If there are no tracks to paint, just exit
boolean hasTracks = false;
for (TrackGroup group : groups) {
if (group.getTracks().size() > 0) {
hasTracks = true;
break;
}
}
if (!hasTracks) {
removeMousableRegions();
return;
}
int trackWidth = getWidth();
computeMousableRegions(groups, trackWidth);
painter.paint(groups, context, trackWidth, getBackground(), damageRect);
// If there is a partial ROI in progress draw it first
if (currentTool instanceof RegionOfInterestTool) {
int startLoc = ((RegionOfInterestTool) currentTool).getRoiStart();
if (startLoc > 0) {
int start = frame.getScreenPosition(startLoc);
g.setColor(Color.BLACK);
graphics2D.drawLine(start, 0, start, getHeight());
}
}
drawAllRegions(g);
} finally {
if (context != null) {
context.dispose();
}
}
}
/**
* TODO -- move this to a "layout" command, to layout tracks and assign positions
*/
private void computeMousableRegions(Collection<TrackGroup> groups, int width) {
final List<MouseableRegion> mouseableRegions = parent.getMouseRegions();
mouseableRegions.clear();
int trackX = 0;
int trackY = 0;
for (Iterator<TrackGroup> groupIter = groups.iterator(); groupIter.hasNext(); ) {
TrackGroup group = groupIter.next();
if (group.isVisible()) {
if (groups.size() > 1) {
trackY += UIConstants.groupGap;
}
List<Track> trackList = group.getTracks();
for (Track track : trackList) {
if (track == null) continue;
int trackHeight = track.getHeight();
if (track.isVisible()) {
Rectangle rect = new Rectangle(trackX, trackY, width, trackHeight);
if (mouseableRegions != null) {
mouseableRegions.add(new MouseableRegion(rect, track));
}
trackY += trackHeight;
}
}
}
}
}
/**
* Paint method designed to paint to an offscreen image
*
* @param g
* @param rect
*/
public void paintOffscreen(final Graphics2D g, Rectangle rect) {
RenderContext context = null;
try {
context = new RenderContextImpl(null, g, frame, rect);
final Collection<TrackGroup> groups = new ArrayList(parent.getTrackGroups());
int width = rect.width;
painter.paint(groups, context, width, getBackground(), rect);
drawAllRegions(g);
Color c = g.getColor();
g.setColor(Color.darkGray);
g.drawRect(rect.x, rect.y, rect.width, rect.height);
g.setColor(c); //super.paintBorder(g);
} finally {
if (context != null) {
context.dispose();
}
}
}
/**
* Draw vertical lines demarcating regions of interest.
*/
public void drawAllRegions(final Graphics g) {
// TODO -- get rid of this ugly reference to IGV
Collection<RegionOfInterest> regions =
IGV.getInstance().getSession().getRegionsOfInterest(frame.getChrName());
if ((regions == null) || regions.isEmpty()) {
return;
}
boolean drawBars = PreferenceManager.getInstance().getAsBoolean(PreferenceManager.SHOW_REGION_BARS);
Graphics2D graphics2D = (Graphics2D) g.create();
try {
for (RegionOfInterest regionOfInterest : regions) {
if (drawBars || regionOfInterest == RegionOfInterestPanel.getSelectedRegion()) {
drawRegion(graphics2D, regionOfInterest);
}
}
} finally {
if (graphics2D != null) {
graphics2D.dispose();
}
}
}
private boolean drawRegion(Graphics2D graphics2D, RegionOfInterest regionOfInterest) {
Integer regionStart = regionOfInterest.getStart();
if (regionStart == null) {
return true;
}
Integer regionEnd = regionOfInterest.getEnd();
if (regionEnd == null) {
regionEnd = regionStart;
}
ReferenceFrame referenceFrame = frame;
int start = referenceFrame.getScreenPosition(regionStart);
int end = referenceFrame.getScreenPosition(regionEnd);
// Set foreground color of boundaries
int height = getHeight();
graphics2D.setColor(regionOfInterest.getForegroundColor());
graphics2D.drawLine(start, 0, start, height);
graphics2D.drawLine(end, 0, end, height);
return false;
}
protected String generateTileKey(final String chr, int t,
final int zoomLevel) {
// Fetch image for this chromosome, zoomlevel, and tile. If found
// draw immediately
final String key = chr + "_z_" + zoomLevel + "_t_" + t;
return key;
}
/**
* Do not remove - Used for debugging only
*
* @param trackName
*/
public void debugDump(String trackName) {
// Get the view that holds the track name, attribute and data panels
TrackPanel trackView = (TrackPanel) getParent();
if (trackView == null) {
return;
}
if (trackView.hasTracks()) {
String name = parent.getTrackSetID().toString();
System.out.println(
"\n\n" + name + " Track COUNT:" + trackView.getTracks().size());
System.out.println(
"\t\t\t\t" + name + " scrollpane height = " + trackView.getScrollPane().getHeight());
System.out.println(
"\t\t\t\t" + name + " viewport height = " + trackView.getViewportHeight());
System.out.println(
"\t\t\t\t" + name + " TrackView min height = " + trackView.getMinimumSize().getHeight());
System.out.println(
"\t\t\t\t" + name + " TrackView pref height = " + trackView.getPreferredSize().getHeight());
System.out.println(
"\t\t\t\t" + name + " TrackView height = " + trackView.getSize().getHeight());
}
}
/**
* Return html formatted text for mouse position (pixels).
* TODO this will be a lot easier when each track has its own panel.
*/
static DecimalFormat locationFormatter = new DecimalFormat();
/**
* Method description
*
* @param x
* @param y
* @return
*/
public Track getTrack(int x, int y) {
for (MouseableRegion mouseRegion : parent.getMouseRegions()) {
if (mouseRegion.containsPoint(x, y)) {
return mouseRegion.getTracks().iterator().next();
}
}
return null;
}
@Override
public void setToolTipText(String text) {
if (!Objects.equal(tooltipText, text)) {
IGV.getInstance().setStatusWindowText(text);
this.tooltipText = text;
putClientProperty(TOOL_TIP_TEXT_KEY, text);
}
}
/**
* {@inheritDoc}
* <p/>
* The tooltip text may be null, in which case no tooltip is displayed
*/
@Override
final public String getToolTipText() {
//TODO Suppress tooltips instead. This is hard to get exactly right
//TODO with our different tooltip settings
if (currentTool instanceof RegionOfInterestTool) {
return null;
}
return tooltipText;
}
/**
* Update tooltip text for the current mouse position (x, y)
*
* @param x Mouse x position in pixels
* @param y Mouse y position in pixels
*/
public void updateTooltipText(int x, int y) {
//Tooltip here specifically means text that is shown on hover
//We disable it unless that option is specified
if (!IGV.getInstance().isShowDetailsOnHover()) {
setToolTipText(null);
return;
}
double position = frame.getChromosomePosition(x);
Track track = null;
List<MouseableRegion> regions = parent.getMouseRegions();
StringBuffer popupTextBuffer = new StringBuffer();
popupTextBuffer.append("<html>");
for (MouseableRegion mouseRegion : regions) {
if (mouseRegion.containsPoint(x, y)) {
track = mouseRegion.getTracks().iterator().next();
if (track != null) {
// First see if there is an overlay track. If there is, give
// it first crack
List<Track> overlays = IGV.getInstance().getOverlayTracks(track);
boolean foundOverlaidFeature = false;
if (overlays != null) {
for (Track overlay : overlays) {
if ((overlay != track) && (overlay.getValueStringAt(
frame.getChrName(), position, y, frame) != null)) {
String valueString = overlay.getValueStringAt(frame.getChrName(), position, y, frame);
if (valueString != null) {
popupTextBuffer.append(valueString);
popupTextBuffer.append("<br>");
foundOverlaidFeature = true;
break;
}
}
}
}
if (!foundOverlaidFeature) {
String valueString = track.getValueStringAt(frame.getChrName(), position, y, frame);
if (valueString != null) {
if (foundOverlaidFeature) {
popupTextBuffer.append("---------------------<br>");
}
popupTextBuffer.append(valueString);
popupTextBuffer.append("<br>");
break;
}
}
}
}
}
if (popupTextBuffer.length() > 6) { // 6 characters for <html>
//popupTextBuffer.append("<br>--------------------------");
//popupTextBuffer.append(positionString);
String puText = popupTextBuffer.toString().trim();
if (!puText.equals(tooltipText)) {
setToolTipText(puText);
}
} else {
setToolTipText(null);
}
}
private void init() {
setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));
setRequestFocusEnabled(false);
// Key Events
KeyAdapter keyAdapter = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int shiftOriginPixels = Integer.MIN_VALUE;
int zoomIncr = Integer.MIN_VALUE;
boolean showWaitCursor = false;
if (e.getKeyChar() == '+' || e.getKeyCode() == KeyEvent.VK_PLUS) {
zoomIncr = +1;
showWaitCursor = true;
} else if (e.getKeyChar() == '-' || e.getKeyCode() == KeyEvent.VK_PLUS) {
zoomIncr = -1;
showWaitCursor = true;
} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
shiftOriginPixels = 50;
} else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
shiftOriginPixels = -50;
} else if (e.getKeyCode() == KeyEvent.VK_HOME) {
shiftOriginPixels = -getWidth();
showWaitCursor = true;
} else if (e.getKeyCode() == KeyEvent.VK_END) {
shiftOriginPixels = getWidth();
showWaitCursor = true;
} else if (e.getKeyCode() == KeyEvent.VK_PLUS) {
} else if (e.getKeyCode() == KeyEvent.VK_MINUS) {
}
WaitCursorManager.CursorToken token = null;
if(showWaitCursor) token = WaitCursorManager.showWaitCursor();
try {
if(zoomIncr > Integer.MIN_VALUE){
frame.doZoomIncrement(zoomIncr);
}else if(shiftOriginPixels > Integer.MIN_VALUE){
frame.shiftOriginPixels(shiftOriginPixels);
}else{
return;
}
//Assume that anything special enough to warrant a wait cursor
//should be in history
if(showWaitCursor){
frame.recordHistory();
}
} finally {
if(token != null) WaitCursorManager.removeWaitCursor(token);
}
}
};
addKeyListener(keyAdapter);
// Mouse Events
MouseInputAdapter mouseAdapter = new DataPanelMouseAdapter();
addMouseMotionListener(mouseAdapter);
addMouseListener(mouseAdapter);
addMouseWheelListener(mouseAdapter);
}
/**
* Some performance hings from the Mac developer mailing list. Might improve
* graphics performance?
* <p/>
* // TODO do timing tests with and without these hints
*
* @param g2D
*/
private void applyMacPerformanceHints(Graphics2D g2D) {
// From mac mailint list. Might help performance ?
g2D.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g2D.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
g2D.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
g2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
}
protected void removeMousableRegions() {
parent.getMouseRegions().clear();
}
public ReferenceFrame getFrame() {
return frame;
}
/**
* Receives all mouse events for a data panel. Handling of some events are delegated to the current tool or track.
*/
class DataPanelMouseAdapter extends MouseInputAdapter {
/**
* A scheduler is used to distinguish a click from a double click.
*/
private ClickTaskScheduler clickScheduler = new ClickTaskScheduler();
@Override
public void mouseMoved(MouseEvent e) {
String position = null;
if (!frame.getChrName().equals(Globals.CHR_ALL)) {
int location = (int) frame.getChromosomePosition(e.getX()) + 1;
position = frame.getChrName() + ":" + locationFormatter.format(location);
IGV.getInstance().setStatusBarPosition(position);
}
updateTooltipText(e.getX(), e.getY());
}
/**
* The mouse has been pressed. If this is the platform's popup trigger select the track and popup a menu.
* Otherwise delegate handling to the current tool.
*/
@Override
public void mousePressed(final MouseEvent e) {
if (SwingUtilities.getWindowAncestor(DataPanel.this).isActive()) {
DataPanel.this.requestFocus();
}
if (e.isPopupTrigger()) {
doPopupMenu(e);
} else {
if (currentTool != null)
currentTool.mousePressed(e);
}
}
/**
* The mouse has been released. If this is the platform's popup trigger select the track and popup a menu.
* Otherwise delegate handling to the current tool.
*/
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
doPopupMenu(e);
} else {
if (currentTool != null)
currentTool.mouseReleased(e);
}
}
private void doPopupMenu(MouseEvent e) {
IGV.getInstance().clearSelections();
parent.selectTracks(e);
TrackClickEvent te = new TrackClickEvent(e, frame);
parent.openPopupMenu(te);
}
/**
* The mouse has been dragged. Delegate to current tool.
*
* @param e
*/
@Override
public void mouseDragged(MouseEvent e) {
if (currentTool != null)
currentTool.mouseDragged(e);
}
/**
* The mouse was clicked. If this is the second click of a double click, cancel the scheduled single click task.
* The shift and alt keys are alternative zoom options
* shift zooms in by 8x, alt zooms out by 2x
* <p/>
* TODO -- the "currentTool" is also a mouselistener, so there are two. This makes mouse event handling
* TODO -- needlessly complicated, which handler has preference, etc. Move this code to the default
* TODO -- PanAndZoomTool
*
* @param e
*/
@Override
public void mouseClicked(final MouseEvent e) {
// ctrl-mouse down is the mac popup trigger, but you will also get a clck even. Ignore the click.
if (Globals.IS_MAC && e.isControlDown()) {
return;
}
if (currentTool instanceof RegionOfInterestTool) {
currentTool.mouseClicked(e);
return;
}
if (e.isPopupTrigger()) {
doPopupMenu(e);
return;
}
Object source = e.getSource();
if (source instanceof DataPanel && e.getButton() == MouseEvent.BUTTON1) {
final Track track = ((DataPanel) e.getSource()).getTrack(e.getX(), e.getY());
if (e.isShiftDown()) {
final double locationClicked = frame.getChromosomePosition(e.getX());
frame.doIncrementZoom(3, locationClicked);
} else if (e.isAltDown()) {
final double locationClicked = frame.getChromosomePosition(e.getX());
frame.doIncrementZoom(-1, locationClicked);
} else if ((e.isMetaDown() || e.isControlDown()) && track != null) {
TrackClickEvent te = new TrackClickEvent(e, frame);
track.handleDataClick(te);
} else {
// No modifier, left-click. Defer processing with a timer until we are sure this is not the
// first of a "double-click".
if (e.getClickCount() > 1) {
clickScheduler.cancelClickTask();
final double locationClicked = frame.getChromosomePosition(e.getX());
frame.doIncrementZoom(1, locationClicked);
} else {
// Unhandled single click. Delegate to track or tool unless second click arrives within
// double-click interval.
TimerTask clickTask = new TimerTask() {
@Override
public void run() {
Object source = e.getSource();
if (source instanceof DataPanel) {
if (track != null) {
TrackClickEvent te = new TrackClickEvent(e, frame);
List<Track> overlays = IGV.getInstance().getOverlayTracks(track);
boolean handled = false;
if (overlays != null) {
for (Track overlay : overlays) {
if (overlay.getFeatureAtMousePosition(te) != null) {
overlay.handleDataClick(te);
handled = true;
}
}
}
if (!handled) {
handled = track.handleDataClick(te);
}
if (handled) {
return;
} else {
if (currentTool != null)
currentTool.mouseClicked(e);
}
}
}
}
};
clickScheduler.scheduleClickTask(clickTask);
}
}
}
}
/**
* Zoom in/out when modifier + scroll wheel used
*
* @param e
*/
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
//we use either ctrl or meta to deal with PCs and Macs
if (e.isControlDown() || e.isMetaDown()) {
int wheelRotation = e.getWheelRotation();
//Mouse move up is negative, that should zoom in
int zoomIncr = -wheelRotation / 2;
getFrame().doZoomIncrement(zoomIncr);
}
//TODO Use this to pan. Seems weird, but it's how side scrolling on my mouse gets interpreted,
//so could be handy for people with 2D wheels
// else if(e.isShiftDown()){
// System.out.println(e);
// }
else {
//Default action if no modifier
e.getComponent().getParent().dispatchEvent(e);
}
}
}
/**
* A utility class for sceduling single-click actions "in the future",
*
* @author jrobinso
* @date Dec 17, 2010
*/
public class PopupTextUpdater {
private TimerTask currentClickTask;
public void cancelClickTask() {
if (currentClickTask != null) {
currentClickTask.cancel();
currentClickTask = null;
}
}
public void scheduleUpdateTask(TimerTask task) {
cancelClickTask();
currentClickTask = task;
(new java.util.Timer()).schedule(currentClickTask, UIConstants.getDoubleClickInterval());
}
}
}