/* * 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 java.awt.Rectangle; import java.awt.event.MouseWheelEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.swing.SwingUtilities; import org.netbeans.lib.profiler.charts.ChartContext; import org.netbeans.lib.profiler.charts.ChartItem; import org.netbeans.lib.profiler.charts.ChartItemChange; import org.netbeans.lib.profiler.charts.ChartSelectionModel; import org.netbeans.lib.profiler.charts.ItemPainter; import org.netbeans.lib.profiler.charts.PaintersModel; import org.netbeans.lib.profiler.charts.swing.LongRect; import org.netbeans.lib.profiler.charts.swing.Utils; import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYChart; import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYItem; import org.netbeans.lib.profiler.charts.xy.synchronous.SynchronousXYItemsModel; /** * * @author Jiri Sedlacek */ final class TimelineChart extends SynchronousXYChart { static final int MIN_ROW_HEIGHT = 25; static final int MAX_ROW_HEIGHT = 500; static final int DEF_ROW_HEIGHT = 75; static final int ROW_RESIZE_STEP = MIN_ROW_HEIGHT; private static final int ROW_MARGIN_TOP = 3; private static final int ROW_MARGIN_BOTTOM = 3; private int currentRowHeight = DEF_ROW_HEIGHT; private final List<Row> rows; private final Map<ChartItem, Row> itemsToRows; private final Set selectedRows = new TreeSet(new RowComparator()); private final Set selectionBlockers = new HashSet(); private int lastHoverMode; private int lastMoveMode; private final Set<RowListener> rowListeners = new HashSet(); // --- Constructors -------------------------------------------------------- TimelineChart(SynchronousXYItemsModel itemsModel) { super(itemsModel, new PaintersModel.Default()); rows = new ArrayList(); itemsToRows = new HashMap(); setBottomBased(false); setZoomMode(ZOOM_X); setMouseZoomingEnabled(false); setMousePanningEnabled(false); setAccelerationPriority(1f); } // --- Rows management ----------------------------------------------------- Row addRow() { Row row = new Row(); int rowIndex = rows.size(); row.setIndex(rowIndex); rows.add(row); row.setHeight(currentRowHeight, true); row.updateOffset(); // repaintRows(row.getIndex()); updateChart(); notifyRowsAdded(Collections.singletonList(row)); return row; } Row addRow(int rowIndex) { Row row = new Row(); row.setIndex(rowIndex); rows.add(rowIndex, row); row.setHeight(currentRowHeight, true); updateRowOffsets(rowIndex); updateRowIndexes(rowIndex + 1); // repaintRows(rowIndex); updateChart(); notifyRowsAdded(Collections.singletonList(row)); return row; } Row removeRow(int rowIndex) { return removeRow(rows.get(rowIndex)); } Row removeRow(Row row) { row.clearItems(); rows.remove(row); int rowIndex = row.getIndex(); updateRowIndexes(rowIndex); updateRowOffsets(rowIndex); // repaintRows(row.getIndex()); updateChart(); notifyRowsRemoved(Collections.singletonList(row)); return row; } // --- Rows access --------------------------------------------------------- boolean hasRows() { return !rows.isEmpty(); } int getRowsCount() { return rows.size(); } Row getRow(int rowIndex) { return rows.get(rowIndex); } Row getRow(ChartItem item) { return itemsToRows.get(item); } // --- Row appearance ------------------------------------------------------ void setRowHeight(int rowIndex, int rowHeight) { setRowHeight(rowIndex, rowHeight, true); } void setRowHeight(int rowIndex, int rowHeight, boolean checkStep) { Row row = rows.get(rowIndex); boolean changed = row.setHeight(rowHeight, checkStep); updateRowOffsets(rowIndex + 1); if (changed) notifyRowsResized(Collections.singletonList(row)); updateChart(); // TODO: update only affected rows! } int getRowHeight(int rowIndex) { return rows.get(rowIndex).getHeight(); } void increaseRowHeights(boolean step) { if (rows.isEmpty()) return; int incr = step ? ROW_RESIZE_STEP : 1; List<Row> resized = new ArrayList(rows.size()); for (Row row : rows) if (row.setHeight(row.getHeight() + incr, step)) resized.add(row); updateRowOffsets(0); if (!resized.isEmpty()) notifyRowsResized(resized); updateChart(); // TODO: update only affected rows! currentRowHeight += incr; } void decreaseRowHeights(boolean step) { if (rows.isEmpty()) return; int decr = step ? ROW_RESIZE_STEP : 1; List<Row> resized = new ArrayList(rows.size()); for (Row row : rows) if (row.setHeight(row.getHeight() - decr, step)) resized.add(row); updateRowOffsets(0); if (!resized.isEmpty()) notifyRowsResized(resized); updateChart(); // TODO: update only affected rows! currentRowHeight = Math.max(currentRowHeight - decr, MIN_ROW_HEIGHT); } void resetRowHeights() { if (rows.isEmpty()) return; List<Row> resized = new ArrayList(rows.size()); for (Row row : rows) if (row.setHeight(DEF_ROW_HEIGHT, true)) resized.add(row); updateRowOffsets(0); if (!resized.isEmpty()) notifyRowsResized(new ArrayList(rows)); updateChart(); // TODO: update only affected rows! currentRowHeight = DEF_ROW_HEIGHT; } Row getRowAt(int ypos) { ypos += getOffsetY(); for (Row row : rows) { int pos = row.getOffset(); if (ypos < pos) return null; pos += row.getHeight(); if (ypos <= pos) return row; } return null; } Row getNearestRow(int ypos, int range, boolean noFirst) { if (rows.size() == 0) return null; ypos += getOffsetY(); if (noFirst) { Row row = rows.get(0); int pos = row.getOffset() + row.getHeight(); if (ypos < pos - range) return null; } for (Row row : rows) { int pos = row.getOffset(); if (ypos < pos - range) return null; if (ypos <= pos + range) return row; pos += row.getHeight(); if (ypos < pos - range) return null; if (ypos <= pos + range) return row; } return null; } private void updateRowOffsets(int rowIndex) { int rowsCount = rows.size(); if (rowIndex >= rowsCount) return; for (int i = rowIndex; i < rowsCount; i++) rows.get(i).updateOffset(); } // --- Row events ---------------------------------------------------------- void addRowListener(RowListener listener) { rowListeners.add(listener); } void removeRowListener(RowListener listener) { rowListeners.remove(listener); } private void notifyRowsAdded(final List<Row> rows) { SwingUtilities.invokeLater(new Runnable() { public void run() { for (RowListener listener : rowListeners) listener.rowsAdded(rows); } }); } private void notifyRowsRemoved(final List<Row> rows) { SwingUtilities.invokeLater(new Runnable() { public void run() { for (RowListener listener : rowListeners) listener.rowsRemoved(rows); } }); } private void notifyRowsResized(final List<Row> rows) { SwingUtilities.invokeLater(new Runnable() { public void run() { for (RowListener listener : rowListeners) listener.rowsResized(rows); } }); } // --- Selection support --------------------------------------------------- boolean selectRow(Row row) { if (!selectedRows.add(row)) return false; repaintRows(); return true; } boolean unselectRow(Row row) { if (!selectedRows.remove(row)) return false; repaintRows(); return true; } boolean setSelectedRow(Row row) { if (row == null) { return clearRowsSelection(); } else { if (selectedRows.size() == 1 && selectedRows.contains(row)) return false; selectedRows.clear(); selectedRows.add(row); repaintRows(); return true; } } boolean toggleRowSelection(Row row) { if (selectedRows.contains(row)) return unselectRow(row); else return selectRow(row); } boolean clearRowsSelection() { if (selectedRows.isEmpty()) return false; selectedRows.clear(); repaintRows(); return true; } boolean isRowSelected(Row row) { return selectedRows.contains(row); } boolean isRowSelection() { return !selectedRows.isEmpty(); } List<Row> getSelectedRows() { return new ArrayList(selectedRows); } void updateSelection(boolean enable, Object source) { int blockersSize = selectionBlockers.size(); if (enable) selectionBlockers.remove(source); else selectionBlockers.add(source); if (selectionBlockers.size() == blockersSize) return; ChartSelectionModel selectionModel = getSelectionModel(); if (selectionModel == null) return; if (selectionBlockers.isEmpty()) { selectionModel.setHoverMode(lastHoverMode); selectionModel.setMoveMode(lastMoveMode); } else { lastHoverMode = selectionModel.getHoverMode(); lastMoveMode = selectionModel.getMoveMode(); selectionModel.setHoverMode(ChartSelectionModel.HOVER_NONE); selectionModel.setMoveMode(ChartSelectionModel.SELECTION_NONE); } } // --- Internal API to access protected methods ---------------------------- long maxOffsetX() { return super.getMaxOffsetX(); } double viewWidth(double d) { return super.getViewWidth(d); } protected void processMouseWheelEvent(MouseWheelEvent e) { super.processMouseWheelEvent(e); } // --- Protected implementation -------------------------------------------- protected ChartContext getChartContext(ChartItem item) { if (item == null) return super.getChartContext(null); else return itemsToRows.get(item).getContext(); } protected void computeDataBounds() { LongRect.clear(dataBounds); if (rows == null) return; for (Row row : rows) { RowContext context = (RowContext)row.getContext(); if (LongRect.isClear(dataBounds)) LongRect.set(dataBounds, context.bounds); else LongRect.add(dataBounds, context.bounds); } dataBounds.y = 0; Row lastRow = rows.size() > 0 ? rows.get(rows.size() - 1) : null; dataBounds.height = lastRow != null ? lastRow.getOffset() + lastRow.getHeight() : 0; } protected void updateChart() { updateRowBounds(); super.updateChart(); } protected void itemsAdded(List<ChartItem> addedItems) { updateRowBounds(); super.itemsAdded(addedItems); } protected void itemsRemoved(List<ChartItem> removedItems) { updateRowBounds(); super.itemsRemoved(removedItems); } protected void itemsChanged(List<ChartItemChange> itemChanges) { updateRowBounds(); // NOTE: should be computed from itemChanges!!! super.itemsChanged(itemChanges); } protected void paintersChanged(List<ItemPainter> changedPainters) { updateRowBounds(); super.paintersChanged(changedPainters); } // --- Internal implementation --------------------------------------------- void addItemsImpl(SynchronousXYItem[] addedItems, ItemPainter[] addedPainters, Row row) { for (SynchronousXYItem item : addedItems) itemsToRows.put(item, row); paintersModel().addPainters(addedItems, addedPainters); itemsModel().addItems(addedItems); } void removeItemsImpl(SynchronousXYItem[] removedItems) { itemsModel().removeItems(removedItems); paintersModel().removePainters(removedItems); for (SynchronousXYItem item : removedItems) itemsToRows.remove(item); } void invalidateRepaint() { invalidateImage(); repaintDirty(); } // --- Private implementation ---------------------------------------------- private SynchronousXYItemsModel itemsModel() { return (SynchronousXYItemsModel)getItemsModel(); } private PaintersModel.Default paintersModel() { return (PaintersModel.Default)getPaintersModel(); } private void updateRowIndexes(int startIndex) { for (int i = startIndex; i < rows.size(); i++) rows.get(i).setIndex(i); } private void repaintRows() { invalidateImage(); repaintDirty(); } private void repaintRows(final int startIndex) { // SwingUtilities.invokeLater(new Runnable() { // public void run() { for (int i = startIndex; i < rows.size(); i++) { ChartContext rowContext = rows.get(i).getContext(); invalidateImage(new Rectangle(0, Utils.checkedInt(rowContext. getViewportOffsetY()), getWidth(), rowContext. getViewportHeight())); } repaintDirty(); // } // }); } private void updateRowBounds() { if (rows == null) return; // Happens when called from constructor for (Row row : rows) ((RowContext)row.getContext()).updateBounds(); } // --- Row definition ------------------------------------------------------ class Row { private int rowIndex; private int rowOffset; private int rowHeight; private final List<SynchronousXYItem> items; private final RowContext context; // --- Constructors ---------------------------------------------------- Row() { items = new ArrayList(); context = new RowContext(this); } // --- Row telemetry --------------------------------------------------- int getIndex() { return rowIndex; } private void updateOffset() { if (rowIndex != 0) { Row previousRow = rows.get(rowIndex - 1); rowOffset = previousRow.rowOffset + previousRow.rowHeight; } else { rowOffset = 0; } } int getOffset() { return rowOffset; } private boolean setHeight(int height, boolean checkStep) { height = Math.max(MIN_ROW_HEIGHT, height); height = Math.min(MAX_ROW_HEIGHT, height); if (checkStep) height = height / ROW_RESIZE_STEP * ROW_RESIZE_STEP; boolean changed = rowHeight != height; rowHeight = height; return changed; } int getHeight() { return rowHeight; } // --- Items management ------------------------------------------------ void addItems(SynchronousXYItemsModel addedItems, PaintersModel addedPainters) { int itemsCount = addedItems.getItemsCount(); SynchronousXYItem[] addedItemsArr = new SynchronousXYItem[itemsCount]; for (int i = 0; i < itemsCount; i++) addedItemsArr[i] = addedItems.getItem(i); ItemPainter[] addedPaintersArr = new ItemPainter[itemsCount]; for (int i = 0; i < itemsCount; i++) addedPaintersArr[i] = addedPainters.getPainter(addedItemsArr[i]); addItems(addedItemsArr, addedPaintersArr); } void addItems(SynchronousXYItem[] addedItems, ItemPainter[] addedPainters) { for (SynchronousXYItem item : addedItems) items.add(item); addItemsImpl(addedItems, addedPainters, this); } void removeItems(SynchronousXYItemsModel removedItems) { int itemsCount = removedItems.getItemsCount(); SynchronousXYItem[] removedItemsArr = new SynchronousXYItem[itemsCount]; for (int i = 0; i < itemsCount; i++) removedItemsArr[i] = removedItems.getItem(i); removeItems(removedItemsArr); } void removeItems(SynchronousXYItem[] removedItems) { removeItemsImpl(removedItems); for (SynchronousXYItem item : removedItems) items.remove(item); } // --- Items access ---------------------------------------------------- int getItemsCount() { return items.size(); } ChartItem getItem(int itemIndex) { return items.get(itemIndex); } SynchronousXYItem[] getItems() { return items.toArray(new SynchronousXYItem[items.size()]); } @SuppressWarnings("element-type-mismatch") boolean containsItem(ChartItem item) { return items.contains(item); } // --- Row context ----------------------------------------------------- ChartContext getContext() { return context; } // --- Internal interface ---------------------------------------------- private void setIndex(int rowIndex) { this.rowIndex = rowIndex; } private void clearItems() { if (items.size() == 0) return; removeItemsImpl(getItems()); } } // --- RowContext implementation ------------------------------------------- private class RowContext extends SynchronousXYChart.Context { private final Row row; private final LongRect bounds; private double scaleY; private int marginTop; private int marginBottom; RowContext(Row row) { super(TimelineChart.this); this.row = row; marginTop = ROW_MARGIN_TOP; marginBottom = ROW_MARGIN_BOTTOM; bounds = new LongRect(); } protected void updateBounds() { LongRect.clear(bounds); PaintersModel painters = paintersModel(); int itemsCount = row.getItemsCount(); for (int i = 0; i < itemsCount; i++) { ChartItem item = row.getItem(i); ItemPainter painter = painters.getPainter(item); LongRect itemBounds = painter.getItemBounds(item); if (LongRect.isClear(bounds)) { LongRect.set(bounds, itemBounds); } else if (LongRect.isEmpty(itemBounds)) { // Zero height (constant value) LongRect.add(bounds, itemBounds.x, itemBounds.height); } else { LongRect.add(bounds, itemBounds); } } double oldScaleY = scaleY; scaleY = (double)(row.getHeight() - marginTop - marginBottom) / (double)(bounds.height == 0 ? 1 : bounds.height); if (scaleY != oldScaleY) invalidateImage(Utils.checkedRectangle( getViewRect(bounds))); } public boolean isBottomBased() { return true; } public boolean fitsHeight() { return true; } public long getDataOffsetY() { return bounds.y; } public long getDataHeight() { return bounds.height; } public long getViewHeight() { return row.getHeight(); } public long getViewportOffsetY() { return row.getOffset() - getOffsetY(); } public int getViewportHeight() { return row.getHeight(); } public double getViewY(double dataY) { return getViewY(dataY, false); } public double getReversedViewY(double dataY) { return getViewY(dataY, true); } public double getViewHeight(double dataHeight) { return dataHeight * scaleY; } public double getDataY(double viewY) { return getDataY(viewY, false); } public double getReversedDataY(double viewY) { return getDataY(viewY, true); } public double getDataHeight(double viewHeight) { return viewHeight / scaleY; } private double getViewY(double dataY, boolean reverse) { if (isBottomBased() && !reverse || !isBottomBased() && reverse) { return row.getHeight() - (dataY - bounds.y) * scaleY - getOffsetY() + getViewInsets().top - marginBottom + row.getOffset(); } else { return (dataY - bounds.y) * scaleY - getOffsetY() + getViewInsets().top + marginTop + row.getOffset(); } } private double getDataY(double viewY, boolean reverse) { if ((isBottomBased() && !reverse) || (!isBottomBased() && reverse)) { return bounds.y - (viewY + getViewInsets().bottom - marginBottom - getOffsetY() - getHeight()) / scaleY; } else { return (viewY + getOffsetY() - getViewInsets().top - marginTop) / scaleY + bounds.y; } } } private static class RowComparator implements Comparator<Row> { public int compare(Row r1, Row r2) { int r1i = r1.getIndex(); int r2i = r2.getIndex(); return (r1i < r2i ? -1 : (r1i == r2i ? 0 : 1)); } } public static interface RowListener { public void rowsAdded(List<Row> rows); public void rowsRemoved(List<Row> rows); public void rowsResized(List<Row> rows); } }