/*
* Copyright (c) 2009, 2012, 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.
*
* 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.max.ins.gui;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.table.*;
import com.sun.cri.ci.*;
import com.sun.max.ins.*;
import com.sun.max.ins.debug.*;
import com.sun.max.ins.view.*;
import com.sun.max.tele.*;
/**
* A table specialized for use in the VM Inspector.
* <p>
* This table dispatches mouse events to table cell renderers, after first giving
* implementations an opportunity to respond in the case of left
* or middle-clicks. A right click pops up a menu, if provided by
* an implementation, over the cell where the click took place.
* <p>
* After all special handling has completed, the event is passed
* along to the renderer for the cell where the click took place.
* <p>
* This table overrides the Swing display of row selection, which is
* normally shown with an alternate background shading, using
* instead a box drawn around the row.
* <p>
* Table cells can act as sources for drag and drop operations, but
* only for <code>Copy</code> (not <code>Move</code>). The request for something that can
* be dragged is by default delegated to the specific table renderer,
* if it is an instance of {@link InspectorLabel}. Subclasses can customize
* how transferables are created by overriding {@link #getTransferable(int, int)}.
*/
public abstract class InspectorTable extends JTable implements Prober, InspectionHolder, InspectorViewElement {
public static final int MAXIMUM_ROWS_FOR_COMPUTING_COLUMN_WIDTHS = 100;
/**
* Notification service for table based views that allow columns to be turned on and off.
*/
public interface ColumnChangeListener {
/**
* Notifies that the set of visible columns has been changed.
*/
void columnPreferenceChanged();
}
/**
* Support for allowing table cells to be sources for Drag and Drop.
* <br>
* Only supports Copy from a cell (not Move).
* <br>
* Only supports outbound copy, i.e. Drag but not Drop.
*/
private final class InspectorTableTransferHandler extends TransferHandler {
private MouseEvent mouseEvent = null;
@Override
public int getSourceActions(JComponent c) {
return COPY;
}
@Override
public void exportAsDrag(JComponent comp, InputEvent inputEvent, int action) {
// This gets called when a drag sequence starts; cache the location.
if (inputEvent instanceof MouseEvent) {
mouseEvent = (MouseEvent) inputEvent;
} else {
mouseEvent = null;
}
super.exportAsDrag(comp, inputEvent, action);
}
@Override
protected Transferable createTransferable(JComponent component) {
// To get something that might be dragged from the
// table cell where the request originated.
InspectorTable table = InspectorTable.this;
if (mouseEvent != null) {
final Point p = mouseEvent.getPoint();
final int col = table.columnAtPoint(p);
final int row = table.rowAtPoint(p);
return table.getTransferable(row, col);
}
return null;
}
}
private final class InspectorTableMouseListener extends MouseAdapter {
@Override
public void mouseClicked(final MouseEvent mouseEvent) {
final Point p = mouseEvent.getPoint();
final int col = columnAtPoint(p);
final int modelCol = getColumnModel().getColumn(col).getModelIndex();
final int row = rowAtPoint(p);
if ((col != -1) && (row != -1)) {
switch(inspection().gui().getButton(mouseEvent)) {
case MouseEvent.BUTTON1:
// Give subclass an opportunity to handle a left-click specially.
mouseButton1Clicked(row, modelCol, mouseEvent);
break;
case MouseEvent.BUTTON2:
// Give subclass an opportunity to handle a middle-click specially.
mouseButton2Clicked(row, modelCol, mouseEvent);
break;
case MouseEvent.BUTTON3:
// Pop up a menu, if provided by subclass.
final InspectorPopupMenu popupMenu = getPopupMenu(row, modelCol, mouseEvent);
if (popupMenu != null) {
popupMenu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
}
break;
}
// Locate the renderer under the event location and pass along the event.
final TableCellRenderer tableCellRenderer = getCellRenderer(row, col);
final Object cellValue = getValueAt(row, col);
final Component component = tableCellRenderer.getTableCellRendererComponent(InspectorTable.this, cellValue, false, true, row, col);
if (component != null) {
component.dispatchEvent(mouseEvent);
}
}
}
}
private final Inspection inspection;
private final String tracePrefix;
private Set<ColumnChangeListener> columnChangeListeners = CiUtil.newIdentityHashSet();
private MaxVMState lastRefreshedState;
/**
* Creates a new {@link JTable} for use in the {@link Inspection}.
* <br>
* Used only at this time by the two code viewers, all of which is
* subject to further refactoring.
*
* @param inspectorTableModel a model for the table
* @param inspectorTableColumnModel a column model for the table
*/
protected InspectorTable(Inspection inspection, InspectorTableModel inspectorTableModel, InspectorTableColumnModel inspectorTableColumnModel) {
super(inspectorTableModel, inspectorTableColumnModel);
this.inspection = inspection;
this.tracePrefix = "[" + getClass().getSimpleName() + "] ";
getTableHeader().setFont(inspection.preference().style().defaultFont());
addMouseListener(new InspectorTableMouseListener());
setDragEnabled(true);
setTransferHandler(new InspectorTableTransferHandler());
}
/**
* Creates a new {@link JTable} for use in the {@link Inspection}.
*/
protected InspectorTable(Inspection inspection) {
this.inspection = inspection;
this.tracePrefix = "[" + getClass().getSimpleName() + "] ";
getTableHeader().setFont(preference().style().defaultFont());
addMouseListener(new InspectorTableMouseListener());
setDragEnabled(true);
setTransferHandler(new InspectorTableTransferHandler());
}
public final Inspection inspection() {
return inspection;
}
public final MaxVM vm() {
return inspection.vm();
}
public final InspectorGUI gui() {
return inspection.gui();
}
public final InspectionFocus focus() {
return inspection.focus();
}
public final InspectionViews views() {
return inspection.views();
}
public final InspectionActions actions() {
return inspection.actions();
}
public final InspectionPreferences preference() {
return inspection.preference();
}
@Override
public final void paintChildren(Graphics g) {
super.paintChildren(g);
if (getRowSelectionAllowed()) {
// Draw a box around the selected row in the table
final int row = getSelectedRow();
if (row >= 0) {
g.setColor(preference().style().memorySelectedAddressBorderColor());
g.drawRect(0, row * getRowHeight(row), getWidth() - 1, getRowHeight(row) - 1);
}
}
}
public final void refresh(boolean force) {
MaxVMState maxVMState = vm().state();
if (maxVMState.newerThan(lastRefreshedState) || force) {
getInspectorTableModel().refresh();
getInspectorTableColumnModel().refresh(force);
lastRefreshedState = maxVMState;
getTableHeader().setBackground(headerBackgroundColor());
invalidate();
repaint();
}
updateFocusSelection();
}
public final void redisplay() {
getInspectorTableColumnModel().redisplay();
invalidate();
repaint();
}
/**
* Adds a listener for view update when column visibility changes.
*/
public final void addColumnChangeListener(ColumnChangeListener listener) {
columnChangeListeners.add(listener);
}
/**
* Remove a listener for view update when column visibility changed.
*/
public final void removeColumnChangeListener(ColumnChangeListener listener) {
columnChangeListeners.remove(listener);
}
public final InspectorTableColumnModel getInspectorTableColumnModel() {
return (InspectorTableColumnModel) getColumnModel();
}
public final InspectorTableModel getInspectorTableModel() {
return (InspectorTableModel) getModel();
}
/**
* Scrolls the table to display the first row.
*/
public final void scrollToBeginning() {
scrollToRows(0, 0);
}
/**
* Scrolls the table to display the last row.
*/
public final void scrollToEnd() {
final int lastRow = getRowCount() - 1;
scrollToRows(lastRow, lastRow);
}
/**
* Scrolls the table to display the specified range (with a few rows before or after if possible).
* @param firstRow first row of the range that should be made visible
* @param lastRow last row of the range that should be made visible
*/
public final void scrollToRows(int firstRow, int lastRow) {
assert firstRow <= lastRow;
final int tableWidth = getWidth() - 2;
final int rowHeight = getRowHeight() - 2;
// Create a rectangle in the table view to use as a scroll target; include
// the row immediately before and the row immediately after so that the row of interest
// doesn't land at the very beginning or very end of the view, if possible.
final int rowCount = lastRow - firstRow + 1 + 2;
final Rectangle rectangle = new Rectangle(0, (firstRow - 1) * getRowHeight(), tableWidth, rowCount * rowHeight);
// System.out.println("row=" + firstRow + " rect=" + rectangle);
scrollRectToVisible(rectangle);
}
/**
* Notifies listeners that the column visibility preferences for the table have changed.
*/
public final void fireColumnPreferenceChanged() {
ColumnChangeListener[] copy = columnChangeListeners.toArray(new ColumnChangeListener[columnChangeListeners.size()]);
for (ColumnChangeListener listener : copy) {
listener.columnPreferenceChanged();
}
}
/**
* Add tool tip text to the column headers, as specified by the column model.
*/
@Override
protected final JTableHeader createDefaultTableHeader() {
return new JTableHeader(getColumnModel()) {
@Override
public String getToolTipText(java.awt.event.MouseEvent mouseEvent) {
final Point p = mouseEvent.getPoint();
final InspectorTableColumnModel inspectorTableColumnModel = getInspectorTableColumnModel();
final int index = inspectorTableColumnModel.getColumnIndexAtX(p.x);
final int modelIndex = inspectorTableColumnModel.getColumn(index).getModelIndex();
return inspectorTableColumnModel.toolTipTextForColumn(modelIndex);
}
};
}
/**
* Sets up default view configuration for tables.
*/
protected final void configureDefaultTable(InspectorTableModel inspectorTableModel, InspectorTableColumnModel inspectorTableColumnModel) {
setModel(inspectorTableModel);
setColumnModel(inspectorTableColumnModel);
final InspectorStyle style = preference().style();
setShowHorizontalLines(style.defaultTableShowHorizontalLines());
setShowVerticalLines(style.defaultTableShowVerticalLines());
setIntercellSpacing(style.defaultTableIntercellSpacing());
setRowHeight(style.defaultTableRowHeight());
setRowSelectionAllowed(true);
setColumnSelectionAllowed(false);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
refresh(true);
JTableColumnResizer.adjustColumnPreferredWidths(this, MAXIMUM_ROWS_FOR_COMPUTING_COLUMN_WIDTHS);
updateFocusSelection();
}
/**
* Sets up standard view configuration for tables used to show memory in one way or another.
*/
protected final void configureMemoryTable(InspectorTableModel inspectorTableModel, InspectorTableColumnModel inspectorTableColumnModel) {
setModel(inspectorTableModel);
setColumnModel(inspectorTableColumnModel);
setFillsViewportHeight(true);
final InspectorStyle style = preference().style();
setShowHorizontalLines(style.memoryTableShowHorizontalLines());
setShowVerticalLines(style.memoryTableShowVerticalLines());
setIntercellSpacing(style.memoryTableIntercellSpacing());
setRowHeight(style.memoryTableRowHeight());
setRowSelectionAllowed(true);
setColumnSelectionAllowed(false);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
refresh(true);
JTableColumnResizer.adjustColumnPreferredWidths(this, MAXIMUM_ROWS_FOR_COMPUTING_COLUMN_WIDTHS);
updateFocusSelection();
}
/**
* Updates table state to display a new request for row
* selection; clears table selection if -1.
*/
protected final void updateSelection(int row) {
if (row < 0) {
clearSelection();
} else if (row != getSelectedRow()) {
setRowSelectionInterval(row, row);
}
}
/**
* Notification that some focus state of interest has changed, typically
* causing the table's row selection to follow the new focus.
*/
public void updateFocusSelection() {
}
/**
* Gets an alternate background color for rendering a table cell, null for the default.
* <p>
* Note that this can get called very early in table initialization by superclasses.
*/
public Color cellBackgroundColor() {
return null;
}
@Override
public Color getBackground() {
final Color alternateBackgroundColor = cellBackgroundColor();
return alternateBackgroundColor == null ? super.getBackground() : alternateBackgroundColor;
}
/**
* Gets the appropriate foreground color for rendering a table cell, depending on the cell.
* The default is null, which will default to the toolkit' settings
*
* @param row
* @param column
* @return a color to be used for foreground
*/
public Color cellForegroundColor(int row, int column) {
return null;
}
/**
* Gets an alternate background color for rendering the table's header, null for the default.
* <p>
* Note that this can get called very early in table initialization by superclasses.
*/
public Color headerBackgroundColor() {
return null;
}
/**
* Determines if a row should be treated as a "boundary", and an extra border be
* drawn at the top of any cell rendered in that row.
*
* @param row a row in the table
* @return whether the ros should be rendered as a boundary.
*/
public boolean isBoundaryRow(int row) {
return false;
}
/**
* Gives table implementations an opportunity to respond specially to left-button clicks
* over the table. Default implementation of this method in null.
* <br>
* After this method is called, the left-button click is passed along to the renderer
* for the cell under the mouse.
* <br>
* <strong>Note:</strong> When this method is called, the tables selection
* listener has already processed the left-button click: the selection model
* has been updated and any tables listening to the selection model will have
* been notified.
*
* @param row row in the table model where the click took place
* @param col column in the column model where the click took place
* @param mouseEvent the originating event
*/
protected void mouseButton1Clicked(int row, int col, MouseEvent mouseEvent) {
}
/**
* Gives table implementations an opportunity to respond specially to middle-button clicks
* over the table. Default implementation of this method in null.
* <br>
* After this method is called, the middle-button click is passed along to the renderer
* for the cell under the mouse.
*
* @param row row in the table model where the click took place
* @param col column in the column model where the click took place
* @param mouseEvent the originating event
*/ protected void mouseButton2Clicked(int row, int col, MouseEvent mouseEvent) {
}
/**
* @param row row in the table model where the click took place
* @param col column in the column model where the click took place
* @param mouseEvent the originating event
* @return a popup menu, null if none relevant to location and circumstances.
*/
protected InspectorPopupMenu getPopupMenu(int row, int col, MouseEvent mouseEvent) {
return null;
}
/**
* Delegates the request for something that can be transferred from a table cell (via drag
* and drop - copy only) to the renderer for that cell.
*
* @param row row in the table where a drag is requested
* @param col column in the table where a drag is requested
* @return something that can be transferred (via copying) from
* the specified cell; null if nothing can be transferred.
*/
protected Transferable getTransferable(int row, int col) {
final TableCellRenderer cellRenderer = getColumnModel().getColumn(col).getCellRenderer();
Object value = getValueAt(row, col);
if (cellRenderer != null) {
final Component renderer = cellRenderer.getTableCellRendererComponent(this, value, false, false, row, col);
if (renderer instanceof InspectorLabel) {
final InspectorLabel inspectorLabel = (InspectorLabel) renderer;
return inspectorLabel.getTransferable();
}
}
return null;
}
/**
* @return default prefix text for trace messages; identifies the class being traced.
*/
protected String tracePrefix() {
return tracePrefix;
}
public List<InspectorAction> extraViewMenuActions() {
return Collections.emptyList();
}
/**
* @return whether the current display mode is hiding some rows of the table
*/
public boolean isElided() {
return false;
}
protected InspectorAction scrollToBeginningAction() {
return new ShowFirstRowAction(inspection());
}
protected InspectorAction scrollToEndAction() {
return new ShowLastRowAction(inspection());
}
private final class ShowFirstRowAction extends InspectorAction {
public ShowFirstRowAction(Inspection inspection) {
super(inspection, "Show first row");
}
@Override
protected void procedure() {
scrollToRows(0, 0);
}
}
private final class ShowLastRowAction extends InspectorAction {
public ShowLastRowAction(Inspection inspection) {
super(inspection, "Show last row");
}
@Override
protected void procedure() {
final int lastRow = getInspectorTableModel().getRowCount() - 1;
scrollToRows(lastRow, lastRow);
}
}
}