/*
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.visualvm.modules.tracer.impl.timeline;
import com.sun.tools.visualvm.modules.tracer.impl.swing.EnhancedLabelRenderer;
import com.sun.tools.visualvm.modules.tracer.impl.swing.HeaderLabel;
import com.sun.tools.visualvm.modules.tracer.impl.swing.HeaderPanel;
import com.sun.tools.visualvm.modules.tracer.impl.swing.LegendFont;
import com.sun.tools.visualvm.modules.tracer.impl.swing.TimelineMarksPainter;
import com.sun.tools.visualvm.uisupport.UISupport;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import org.netbeans.lib.profiler.charts.ChartConfigurationListener;
import org.netbeans.lib.profiler.charts.ChartContext;
import org.netbeans.lib.profiler.charts.Timeline;
import org.netbeans.lib.profiler.charts.axis.AxisComponent;
import org.netbeans.lib.profiler.charts.axis.AxisMark;
import org.netbeans.lib.profiler.charts.axis.TimeAxisUtils;
import org.netbeans.lib.profiler.charts.axis.TimelineMarksComputer;
import org.netbeans.lib.profiler.charts.swing.Utils;
import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYChartContext;
import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYItemsModel;
import org.openide.util.ImageUtilities;
/**
*
* @author Jiri Sedlacek
*/
final class TimelineAxis extends JPanel {
private final HeaderRenderer painter;
private final AxisComponent axis;
private final MarksComponent marks;
private int preferredHeight;
private int pointerX;
TimelineAxis(final TimelineChart chart, TimelineSupport support) {
super(null);
painter = new HeaderRenderer();
Timeline timeline = ((SynchronousXYItemsModel)chart.getItemsModel()).getTimeline();
axis = new Axis(chart, new MarksComputer(timeline, chart.getChartContext()));
marks = new MarksComponent(support);
preferredHeight = HeaderLabel.DEFAULT_HEIGHT;
add(marks);
add(axis);
add(painter);
chart.addConfigurationListener(new ChartConfigurationListener.Adapter() {
private final Runnable updater = new Runnable() {
public void run() {
if (!axis.isVisible()) {
marks.setupTicks();
marks.refreshHoverMark(pointerX);
marks.repaint();
}
}
};
public void contentsUpdated(long offsetX, long offsetY,
double scaleX, double scaleY,
long lastOffsetX, long lastOffsetY,
double lastScaleX, double lastScaleY,
int shiftX, int shiftY) {
if (lastOffsetX != offsetX || lastScaleX != scaleX)
marks.refreshMarks();
SwingUtilities.invokeLater(updater);
}
});
support.addSelectionListener(new TimelineSupport.SelectionListener() {
public void rowSelectionChanged(boolean rowsSelected) {}
public void timeSelectionChanged(boolean timestampsSelected, boolean justHovering) {
marks.refreshMarks();
marks.repaint();
}
});
marks.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
pointerX = e.getX();
marks.setupTicks();
marks.refreshHoverMark(pointerX);
axis.setVisible(false);
}
public void mouseExited(MouseEvent e) {
axis.setVisible(true);
marks.clearTicks();
marks.refreshHoverMark(-10);
}
public void mouseClicked(MouseEvent e) {
marks.handleAction();
marks.repaint();
}
});
marks.addMouseMotionListener(new MouseMotionListener() {
public void mouseDragged(MouseEvent e) {
pointerX = e.getX();
}
public void mouseMoved(MouseEvent e) {
pointerX = e.getX();
if (!axis.isVisible()) marks.refreshHoverMark(pointerX);
}
});
marks.addMouseWheelListener(new MouseWheelListener() {
public void mouseWheelMoved(MouseWheelEvent e) {
e.setSource(chart);
chart.processMouseWheelEvent(e);
}
});
}
public boolean isOptimizedDrawingEnabled() {
return false;
}
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
size.height = preferredHeight;
return size;
}
public void validate() {}
public void doLayout() {}
public void reshape(int x, int y, int width, int height) {
super.reshape(x, y, width, height);
painter.reshape(0, 0, width, height);
axis.reshape(1, 1, width - 2, height - 2);
marks.reshape(0, 0, width, height);
}
private static class HeaderRenderer extends HeaderPanel {
private Image offscreen;
public void reshape(int x, int y, int width, int height) {
if (getWidth() != width || getHeight() != height) offscreen = null;
super.reshape(x, y, width, height);
}
long total = 0;
int count = 0;
public void validate() {}
public void paint(Graphics g) {
if (offscreen == null) {
offscreen = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
super.paint(offscreen.getGraphics());
}
g.drawImage(offscreen, 0, 0, null);
}
}
private static class MarksComponent extends JComponent {
private static final Image MARK = ImageUtilities.loadImage(
"com/sun/tools/visualvm/modules/tracer/impl/resources/mark.png"); // NOI18N
private static final Image MARK_HIGHL = ImageUtilities.loadImage(
"com/sun/tools/visualvm/modules/tracer/impl/resources/markHighl.png"); // NOI18N
private static final int MARK_EXTENT = MARK.getWidth(null) / 2;
private static final int MARK_HEIGHT = MARK.getHeight(null);
private final TimelineSupport support;
private final Timeline timeline;
private final SynchronousXYChartContext context;
private final EnhancedLabelRenderer timeRenderer;
private final Format timeFormat;
private int[] ticks;
private int hoverIndex = -1;
private int hoverX = -10;
private boolean wasSelected;
private long hoverTime;
private final List<Integer> selections = new ArrayList();
private final int markExtent = 2;
MarksComponent(TimelineSupport support) {
this.support = support;
TimelineChart chart = support.getChart();
SynchronousXYItemsModel model = (SynchronousXYItemsModel)chart.getItemsModel();
context = (SynchronousXYChartContext)chart.getChartContext();
timeline = model.getTimeline();
timeRenderer = new EnhancedLabelRenderer();
if (UISupport.isAquaLookAndFeel()) {
Font f = new LegendFont();
timeRenderer.setFont(f.deriveFont(f.getSize2D() - 1));
timeRenderer.setMargin(new Insets(0, 2, 0, 2));
} else {
timeRenderer.setFont(new LegendFont());
timeRenderer.setMargin(new Insets(1, 2, 1, 2));
}
timeRenderer.setBackground(Color.WHITE);
timeRenderer.setBorder(BorderFactory.createLineBorder(Color.BLACK));
timeFormat = new SimpleDateFormat(TimeAxisUtils.getFormatString(1, 1, 1));
setToolTipText("Click a mark to select or unselect it");
setOpaque(false);
}
public void validate() {}
public void doLayout() {}
void refreshMarks() {
Set<Integer> selectedIndexes = support.getSelectedTimestamps();
if (selectedIndexes.size() == 0 && selections.isEmpty()) return;
selections.clear();
for (int selectedIndex : selectedIndexes) {
long time = timeline.getTimestamp(selectedIndex);
int x = Utils.checkedInt(context.getViewX(time));
if (x > -markExtent && x < getWidth() + markExtent)
selections.add(x + 1);
}
}
void setupTicks() {
int[][] idxs = support.getPointsComputer().getVisible(getBounds(),
timeline.getTimestampsCount(), context, 1, 0);
ticks = idxs == null ? null : idxs[0];
if (ticks != null) for (int i = 0; i < idxs[1][0]; i++)
ticks[i] = Utils.checkedInt(context.getViewX(timeline.
getTimestamp(ticks[i]))) + 1;
}
void refreshHoverMark(int pointerX) {
int lastHoverIndex = hoverIndex;
hoverIndex = context.getNearestTimestampIndex(pointerX - 1, 0);
hoverX = hoverIndex == -1 ? -10 : Utils.checkedInt(context.getViewX(
timeline.getTimestamp(hoverIndex))) + 1;
if (Math.abs(hoverX - pointerX + 1) > MARK_EXTENT) {
hoverIndex = -1;
hoverX = -10;
}
if (lastHoverIndex != hoverIndex) {
if (!wasSelected) support.unselectTimestamp(lastHoverIndex);
wasSelected = hoverIndex != -1 && support.isTimestampSelected(hoverIndex);
support.setTimestampHovering(hoverIndex != -1, wasSelected);
if (hoverIndex != -1) {
support.selectTimestamp(hoverIndex, false);
hoverTime = timeline.getTimestamp(hoverIndex);
if (wasSelected) repaint();
} else {
if (!wasSelected) repaint();
}
if (hoverIndex == -1) setCursor(Cursor.getDefaultCursor());
else setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
}
void clearTicks() {
ticks = null;
}
void handleAction() {
wasSelected = !wasSelected;
support.setTimestampHovering(hoverIndex != -1, wasSelected);
}
public void paint(Graphics g) {
int top = getHeight() / 2 - 1;
int bottom = top + 2;
if (ticks != null)
for (int i = 0; i < ticks.length; i++)
g.drawLine(ticks[i], top, ticks[i], bottom);
if (selections == null || selections.isEmpty()) return;
int y = getHeight() - 5 - MARK_HEIGHT;
for (int x : selections)
g.drawImage((x == hoverX && wasSelected) ? MARK_HIGHL :
MARK, x - MARK_EXTENT + 1, y, null);
if (hoverIndex != -1) {
timeRenderer.setText(timeFormat.format(hoverTime));
Dimension timeSize = timeRenderer.getPreferredSize();
int timeWidth = timeSize.width;
int extraWidth = MARK_EXTENT + TimelineTooltipOverlay.TOOLTIP_OFFSET;
int timeX = hoverX + extraWidth;
if (timeX > getWidth() - timeWidth - TimelineTooltipOverlay.TOOLTIP_MARGIN)
timeX = hoverX - timeWidth - extraWidth;
timeRenderer.setLocation(timeX, top - timeSize.height / 2);
timeRenderer.paint(g);
}
}
}
private static class MarksComputer extends TimelineMarksComputer {
MarksComputer(Timeline timeline, ChartContext context) {
super(timeline, context, SwingConstants.HORIZONTAL);
}
void refresh() {
refreshConfiguration();
}
}
private static class Axis extends AxisComponent {
private static final int LAF_OFFSET = resolveOffset();
private final Paint meshPaint = Utils.checkedColor(new Color(180, 180, 180, 50));
private final Stroke meshStroke = new BasicStroke(1);
private final TimelineChart chart;
private final MarksComputer marksComputer;
private final TimelineMarksPainter marksPainter;
private boolean hadTicks = false;
private final Runnable repainter;
Axis(TimelineChart chart, MarksComputer marksComputer) {
super(chart, marksComputer, null, SwingConstants.NORTH,
AxisComponent.MESH_FOREGROUND);
this.chart = chart;
this.marksComputer = marksComputer;
this.marksPainter = new TimelineMarksPainter();
repainter = new Runnable() {
public void run() { Axis.this.chart.invalidateRepaint(); }
};
}
public void validate() {}
public void doLayout() {}
public void paint(Graphics g) {
Rectangle clip = g.getClipBounds();
if (clip == null) clip = new Rectangle(0, 0, getWidth(), getHeight());
marksComputer.refresh();
paintHorizontalAxis(g, clip);
}
protected void paintHorizontalMesh(Graphics2D g, Rectangle clip, Rectangle chartMask) {
Iterator<AxisMark> marks =
marksComputer.marksIterator(chartMask.x, chartMask.x + chartMask.width);
boolean hasTicks = false;
while (marks.hasNext()) {
hasTicks = true;
AxisMark mark = marks.next();
int x = mark.getPosition();
g.setPaint(meshPaint);
g.setStroke(meshStroke);
g.drawLine(x, chartMask.y, x, chartMask.y + chartMask.height);
}
if (!hadTicks && hasTicks) SwingUtilities.invokeLater(repainter);
hadTicks = hasTicks;
}
protected void paintHorizontalAxis(Graphics g, Rectangle clip) {
int viewStart = -1; // -1: extra 1px for axis
int viewEnd = viewStart + chart.getWidth() + 2; // +2 extra 1px + 1px for axis
Iterator<AxisMark> marks = marksComputer.marksIterator(viewStart, viewEnd);
int lZeroOffset = chart.isRightBased() ? 0 : 1;
int rZeroOffset = chart.isRightBased() ? 1 : 0;
while (marks.hasNext()) {
AxisMark mark = marks.next();
int x = mark.getPosition() - 1;
if (x < -1 - lZeroOffset ||
x >= -1 + chart.getWidth() + rZeroOffset) continue;
TimelineMarksPainter painter =
(TimelineMarksPainter)marksPainter.getPainter(mark);
Dimension painterSize = painter.getPreferredSize();
int markOffsetX = painterSize.width / 2;
if (x + markOffsetX < clip.x ||
x - markOffsetX >= clip.x + clip.width) continue;
g.setColor(getForeground());
g.drawLine(x, 1, x, 3);
int markOffsetY = (getHeight() - painterSize.height) / 2 + LAF_OFFSET;
painter.setLocation(x - markOffsetX, markOffsetY);
painter.paint(g);
}
}
private static int resolveOffset() {
if (UISupport.isWindowsLookAndFeel() ||
UISupport.isMetalLookAndFeel() ||
UISupport.isGTKLookAndFeel()) return 1;
return 0;
}
}
}