/* * The MIT License (MIT) * * Copyright (c) 2007-2015 Broad Institute * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * 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.feature.RegionOfInterest; import org.broad.igv.lists.Preloader; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.track.RenderContext; import org.broad.igv.track.Track; import org.broad.igv.track.TrackClickEvent; import org.broad.igv.track.TrackGroup; 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.event.DataLoadedEvent; import org.broad.igv.event.IGVEventObserver; 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; import java.util.stream.Collectors; /** * 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, IGVEventObserver { 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 boolean loadInProgress = false; 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(PreferencesManager.getPreferences().getAsColor(Constants.BACKGROUND_COLOR)); ToolTipManager.sharedInstance().registerComponent(this); // IGVEventBus.getInstance().subscribe(DataLoadedEvent.class, this); } @Override public void receiveEvent(Object event) { if(event instanceof DataLoadedEvent) { if(((DataLoadedEvent) event).referenceFrame == frame) { log.info("Data loaded repaint " + frame); repaint(); } } } /** * @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 { long t0 = System.currentTimeMillis(); if (!allTracksLoaded()) { if (!loadInProgress) { loadInProgress = true; Preloader.load(this); } if(!Globals.isBatch()) return; } 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 RenderContext(this, graphics2D, frame, visibleRect); final Collection<TrackGroup> groups = parent.getTrackGroups(); 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); long dt = System.currentTimeMillis() - t0; PanTool.repaintTime(dt); } finally { if (context != null) { context.dispose(); } } } public boolean allTracksLoaded() { return parent.getTrackGroups().stream(). filter(TrackGroup::isVisible). flatMap(trackGroup -> trackGroup.getVisibleTracks().stream()). allMatch(track -> track.isReadyToPaint(frame)); } public List<Track> notloadedTracks() { return parent.getTrackGroups().stream(). filter(TrackGroup::isVisible). flatMap(trackGroup -> trackGroup.getVisibleTracks().stream()). filter(track -> track.isReadyToPaint(frame) == false). collect(Collectors.toList()); } public List<Track> visibleTracks() { return parent.getTrackGroups().stream(). filter(TrackGroup::isVisible). flatMap(trackGroup -> trackGroup.getVisibleTracks().stream()). collect(Collectors.toList()); } /** * 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.getVisibleTracks(); 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 RenderContext(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 = PreferencesManager.getPreferences().getAsBoolean(Constants.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, x, y, frame) != null)) { String valueString = overlay.getValueStringAt(frame.getChrName(), position, x, y, frame); if (valueString != null) { popupTextBuffer.append(valueString); popupTextBuffer.append("<br>"); foundOverlaidFeature = true; break; } } } } if (!foundOverlaidFeature) { String valueString = track.getValueStringAt(frame.getChrName(), position, x, 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); } 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(); long lastClickTime = 0; @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()); if (IGV.getInstance().isRulerEnabled()) { IGV.getInstance().revalidateTrackPanels(); } } /** * 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) { long clickTime = System.currentTimeMillis(); // 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); e.consume(); return; } if (e.isPopupTrigger()) { doPopupMenu(e); e.consume(); 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); e.consume(); } else if (e.isAltDown()) { final double locationClicked = frame.getChromosomePosition(e.getX()); frame.doIncrementZoom(-1, locationClicked); e.consume(); } else if ((e.isMetaDown() || e.isControlDown()) && track != null) { TrackClickEvent te = new TrackClickEvent(e, frame); if(track.handleDataClick(te)) { e.consume(); return; } } else { // No modifier, left-click. Defer processing with a timer until we are sure this is not the // first of a "double-click". if (clickTime - lastClickTime < UIConstants.getDoubleClickInterval()) { clickScheduler.cancelClickTask(); final double locationClicked = frame.getChromosomePosition(e.getX()); frame.doIncrementZoom(1, locationClicked); } else { lastClickTime = clickTime; // 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()); } } }