GP-2067: Implement header renderer override and add cursor to ModelProvider's Plot columns

This commit is contained in:
Dan 2022-09-21 11:28:12 -04:00
parent 45165ea167
commit cdd6f3d72e
16 changed files with 250 additions and 83 deletions

View file

@ -21,6 +21,7 @@ import java.util.stream.Stream;
import com.google.common.collect.Range;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import docking.widgets.table.threaded.ThreadedTableModel;
import ghidra.framework.plugintool.Plugin;
import ghidra.trace.database.DBTraceUtils;
@ -309,4 +310,6 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
public abstract void setDiffColorSel(Color diffColorSel);
public abstract T findTraceObject(TraceObject object);
public abstract void addSeekListener(SeekListener listener);
}

View file

@ -27,6 +27,7 @@ import javax.swing.event.ListSelectionListener;
import com.google.common.collect.Range;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.framework.plugintool.Plugin;
import ghidra.trace.model.target.TraceObject;
@ -146,6 +147,10 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
table.removeKeyListener(l);
}
public void addSeekListener(SeekListener listener) {
tableModel.addSeekListener(listener);
}
public void setSelectionMode(int selectionMode) {
table.setSelectionMode(selectionMode);
}

View file

@ -27,6 +27,7 @@ import javax.swing.*;
import docking.*;
import docking.action.DockingAction;
import docking.action.ToggleDockingAction;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
@ -111,6 +112,14 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable
DebuggerObjectActionContext myActionContext;
private SeekListener seekListener = pos -> {
long snap = Math.round(pos);
if (current.getTrace() == null || snap < 0) {
snap = 0;
}
traceManager.activateSnap(snap);
};
public DebuggerModelProvider(DebuggerModelPlugin plugin, boolean isClone) {
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_MODEL, plugin.getName());
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
@ -349,6 +358,9 @@ public class DebuggerModelProvider extends ComponentProvider implements Saveable
e.consume();
}
});
elementsTablePanel.addSeekListener(seekListener);
attributesTablePanel.addSeekListener(seekListener);
}
@Override

View file

@ -23,6 +23,7 @@ import java.util.stream.Stream;
import com.google.common.collect.*;
import docking.widgets.table.DynamicTableColumn;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import docking.widgets.table.TableColumnDescriptor;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.app.plugin.core.debug.gui.model.columns.*;
@ -419,4 +420,15 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
column.setDiffColorSel(diffColorSel);
}
}
@Override
protected void snapChanged() {
super.snapChanged();
lifePlotColumn.setSnap(getSnap());
}
@Override
public void addSeekListener(SeekListener listener) {
lifePlotColumn.addSeekListener(listener);
}
}

View file

@ -22,6 +22,7 @@ import java.util.stream.Stream;
import com.google.common.collect.Range;
import docking.widgets.table.TableColumnDescriptor;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.app.plugin.core.debug.gui.model.columns.*;
import ghidra.framework.plugintool.Plugin;
@ -168,4 +169,15 @@ public class PathTableModel extends AbstractQueryTableModel<PathRow> {
public void setDiffColorSel(Color diffColorSel) {
valueColumn.setDiffColorSel(diffColorSel);
}
@Override
public void snapChanged() {
super.snapChanged();
lifespanPlotColumn.setSnap(getSnap());
}
@Override
public void addSeekListener(SeekListener listener) {
lifespanPlotColumn.addSeekListener(listener);
}
}

View file

@ -17,8 +17,8 @@ package ghidra.app.plugin.core.debug.gui.model.columns;
import com.google.common.collect.Range;
import docking.widgets.table.AbstractDynamicTableColumn;
import docking.widgets.table.RangeTableCellRenderer;
import docking.widgets.table.*;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
@ -30,6 +30,8 @@ public class TracePathLastLifespanPlotColumn
extends AbstractDynamicTableColumn<PathRow, Range<Long>, Trace> {
private final RangeTableCellRenderer<Long> cellRenderer = new RangeTableCellRenderer<>();
private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
new RangeCursorTableHeaderRenderer<>();
@Override
public String getColumnName() {
@ -51,10 +53,21 @@ public class TracePathLastLifespanPlotColumn
return cellRenderer;
}
// TODO: header renderer
@Override
public GTableHeaderRenderer getHeaderRenderer() {
return headerRenderer;
}
public void setFullRange(Range<Long> fullRange) {
cellRenderer.setFullRange(fullRange);
// TODO: header, too
headerRenderer.setFullRange(fullRange);
}
public void setSnap(long snap) {
headerRenderer.setCursorPosition(snap);
}
public void addSeekListener(SeekListener listener) {
headerRenderer.addSeekListener(listener);
}
}

View file

@ -18,8 +18,8 @@ package ghidra.app.plugin.core.debug.gui.model.columns;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import docking.widgets.table.AbstractDynamicTableColumn;
import docking.widgets.table.RangeSetTableCellRenderer;
import docking.widgets.table.*;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
@ -30,6 +30,8 @@ public class TraceValueLifePlotColumn
extends AbstractDynamicTableColumn<ValueRow, RangeSet<Long>, Trace> {
private final RangeSetTableCellRenderer<Long> cellRenderer = new RangeSetTableCellRenderer<>();
private final RangeCursorTableHeaderRenderer<Long> headerRenderer =
new RangeCursorTableHeaderRenderer<>();
@Override
public String getColumnName() {
@ -47,10 +49,21 @@ public class TraceValueLifePlotColumn
return cellRenderer;
}
// TODO: The header renderer
@Override
public GTableHeaderRenderer getHeaderRenderer() {
return headerRenderer;
}
public void setFullRange(Range<Long> fullRange) {
cellRenderer.setFullRange(fullRange);
// TODO: set header's full range, too
headerRenderer.setFullRange(fullRange);
}
public void setSnap(long snap) {
headerRenderer.setCursorPosition(snap);
}
public void addSeekListener(SeekListener listener) {
headerRenderer.addSeekListener(listener);
}
}

View file

@ -19,7 +19,7 @@ import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.Objects;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
@ -35,6 +35,7 @@ import docking.widgets.HorizontalTabPanel;
import docking.widgets.HorizontalTabPanel.TabListCellRenderer;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.table.*;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
@ -206,7 +207,9 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
DockingAction actionCloseDeadTraces;
DockingAction actionCloseAllTraces;
Set<Object> strongRefs = new HashSet<>(); // Eww
// strong refs
ToToggleSelectionListener toToggleSelectionListener;
SeekListener seekListener;
public DebuggerThreadsProvider(final DebuggerThreadsPlugin plugin) {
super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_THREADS, plugin.getName());
@ -220,8 +223,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
buildMainPanel();
// TODO: Consider a custom cell renderer in the table instead of a timeline widget?
// TODO: Should I receive clicks on that renderer to seek to a given snap?
setDefaultWindowPosition(WindowPosition.BOTTOM);
myActionContext = new DebuggerSnapActionContext(current.getTrace(), current.getViewSnap());
@ -231,11 +232,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
setVisible(true);
}
private <T> T strongRef(T t) {
strongRefs.add(t);
return t;
}
@AutoServiceConsumed
public void setModelService(DebuggerModelService modelService) {
if (this.modelService != null) {
@ -471,7 +467,7 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
colPlot.setCellRenderer(rangeRenderer);
colPlot.setHeaderRenderer(headerRenderer);
headerRenderer.addSeekListener(threadTable, ThreadTableColumns.PLOT.ordinal(), pos -> {
headerRenderer.addSeekListener(seekListener = pos -> {
long snap = Math.round(pos);
if (current.getTrace() == null || snap < 0) {
snap = 0;
@ -483,7 +479,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
}
protected void createActions() {
// TODO: Make other actions use builder?
actionStepSnapBackward = StepSnapBackwardAction.builder(plugin)
.enabledWhen(this::isStepSnapBackwardEnabled)
.enabled(false)
@ -521,8 +516,8 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
.enabledWhen(c -> current.getTrace() != null)
.onAction(c -> activatedGoToTime())
.buildAndInstallLocal(this);
traceManager.addSynchronizeFocusChangeListener(
strongRef(new ToToggleSelectionListener(actionSyncFocus)));
traceManager.addSynchronizeFocusChangeListener(toToggleSelectionListener =
new ToToggleSelectionListener(actionSyncFocus));
actionCloseTrace = CloseTraceAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class)

View file

@ -25,55 +25,16 @@ import javax.swing.table.*;
import com.google.common.collect.Range;
import ghidra.util.datastruct.ListenerSet;
public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
extends GTableHeaderRenderer implements RangedRenderer<N> {
protected final static int ARROW_SIZE = 10;
protected final static Polygon ARROW = new Polygon(
new int[] { 0, -ARROW_SIZE, -ARROW_SIZE },
new int[] { 0, ARROW_SIZE, -ARROW_SIZE }, 3);
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
protected double span = 1;
protected Range<N> fullRange;
protected N pos;
protected double doublePos;
@Override
public void setFullRange(Range<N> fullRange) {
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
public interface SeekListener extends Consumer<Double> {
}
public void setCursorPosition(N pos) {
this.pos = pos;
this.doublePos = pos.doubleValue();
}
protected class ForSeekMouseListener extends MouseAdapter {
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
// The cursor should occlude the children
paintCursor(g);
}
protected void paintCursor(Graphics parentG) {
Graphics2D g = (Graphics2D) parentG.create();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double x = (doublePos - fullRangeDouble.lowerEndpoint()) / span * getWidth();
g.translate(x, getHeight());
g.rotate(Math.PI / 2);
g.setColor(getForeground());
g.fillPolygon(ARROW);
}
public void addSeekListener(JTable table, int modelColumn, Consumer<Double> listener) {
TableColumnModel colModel = table.getColumnModel();
JTableHeader header = table.getTableHeader();
TableColumn col = colModel.getColumn(modelColumn);
MouseAdapter l = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if ((e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0) {
@ -98,36 +59,108 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
}
protected void doSeek(MouseEvent e) {
TableColumnModel colModel = savedTable.getColumnModel();
JTableHeader header = savedTable.getTableHeader();
TableColumn myViewCol = colModel.getColumn(savedViewColumn);
if (header.getResizingColumn() != null) {
return;
}
int viewColIdx = colModel.getColumnIndexAtX(e.getX());
int modelColIdx = table.convertColumnIndexToModel(viewColIdx);
if (modelColIdx != modelColumn) {
int clickedViewColIdx = colModel.getColumnIndexAtX(e.getX());
if (clickedViewColIdx != savedViewColumn) {
return;
}
TableColumn draggedCol = header.getDraggedColumn();
if (draggedCol == col) {
TableColumn draggedViewCol = header.getDraggedColumn();
if (draggedViewCol == myViewCol) {
header.setDraggedColumn(null);
}
else if (draggedCol != null) {
else if (draggedViewCol != null) {
return;
}
int colX = 0;
for (int i = 0; i < viewColIdx; i++) {
for (int i = 0; i < clickedViewColIdx; i++) {
colX += colModel.getColumn(i).getWidth();
}
TableColumn col = colModel.getColumn(viewColIdx);
double pos =
span * (e.getX() - colX) / col.getWidth() + fullRangeDouble.lowerEndpoint();
listener.accept(pos);
span * (e.getX() - colX) / myViewCol.getWidth() + fullRangeDouble.lowerEndpoint();
listeners.fire.accept(pos);
}
};
header.addMouseListener(l);
header.addMouseMotionListener(l);
}
protected final static int ARROW_SIZE = 10;
protected final static Polygon ARROW = new Polygon(
new int[] { 0, -ARROW_SIZE, -ARROW_SIZE },
new int[] { 0, ARROW_SIZE, -ARROW_SIZE }, 3);
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
protected double span = 1;
protected Range<N> fullRange;
protected N pos;
protected double doublePos;
private JTable savedTable;
private int savedViewColumn;
private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener();
private final ListenerSet<SeekListener> listeners = new ListenerSet<>(SeekListener.class);
@Override
public void setFullRange(Range<N> fullRange) {
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
}
public void setCursorPosition(N pos) {
this.pos = pos;
this.doublePos = pos.doubleValue();
}
protected void setSavedTable(JTable table) {
if (savedTable != null) {
JTableHeader header = savedTable.getTableHeader();
header.removeMouseListener(forSeekMouseListener);
header.removeMouseMotionListener(forSeekMouseListener);
}
savedTable = table;
if (savedTable != null) {
JTableHeader header = savedTable.getTableHeader();
header.addMouseListener(forSeekMouseListener);
header.addMouseMotionListener(forSeekMouseListener);
}
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
setSavedTable(table);
savedViewColumn = column;
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
// The cursor should occlude the children
paintCursor(g);
}
protected void paintCursor(Graphics parentG) {
Graphics2D g = (Graphics2D) parentG.create();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double x = (doublePos - fullRangeDouble.lowerEndpoint()) / span * getWidth();
g.translate(x, getHeight());
g.rotate(Math.PI / 2);
g.setColor(getForeground());
g.fillPolygon(ARROW);
}
public void addSeekListener(SeekListener listener) {
listeners.add(listener);
}
public N getCursorPosition() {

View file

@ -32,6 +32,7 @@ import org.junit.*;
import com.google.common.collect.Range;
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.SystemUtilities;
@ -117,7 +118,6 @@ public class DemoRangeCellRendererTest extends AbstractGhidraHeadedIntegrationTe
@Test
public void testDemoRangeCellRenderer() throws Throwable {
new TestEnv().getTool();
JFrame window = new JFrame();
window.setLayout(new BorderLayout());
@ -141,11 +141,12 @@ public class DemoRangeCellRendererTest extends AbstractGhidraHeadedIntegrationTe
model.add(new MyRow("Bob", Range.atLeast(1956)));
model.add(new MyRow("Elvis", Range.closed(1935, 1977)));
headerRenderer.addSeekListener(table, MyColumns.LIFESPAN.ordinal(), pos -> {
SeekListener seekListener = pos -> {
System.out.println("pos: " + pos);
headerRenderer.setCursorPosition(pos.intValue());
table.getTableHeader().repaint();
});
};
headerRenderer.addSeekListener(seekListener);
window.add(new JScrollPane(table));
window.add(filterPanel, BorderLayout.SOUTH);

View file

@ -120,6 +120,11 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
return null;
}
@Override
public GTableHeaderRenderer getHeaderRenderer() {
return null;
}
private void configureDefaultSettings() {
defaultSettingsDefinitions = NO_SETTINGS_DEFINITIONS;
Class<COLUMN_TYPE> columnClass = getColumnClass();

View file

@ -66,4 +66,12 @@ public interface ConfigurableColumnTableModel extends TableModel {
* @return the renderer
*/
public TableCellRenderer getRenderer(int columnIndex);
/**
* Returns the header cell renderer for the given column
* @param columnIndex the index of the column
* @return the renderer
* @return
*/
public TableCellRenderer getHeaderRenderer(int columnIndex);
}

View file

@ -97,6 +97,16 @@ public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
*/
public GColumnRenderer<COLUMN_TYPE> getColumnRenderer();
/**
* Returns the optional header renderer for this column; null if no renderer is used.
*
* <P>
* This method allows columns to define custom header rendering.
*
* @return the renderer
*/
public GTableHeaderRenderer getHeaderRenderer();
/**
* Returns a list of settings definitions for this field.
*

View file

@ -540,6 +540,19 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
return tableColumns.get(index).getColumnRenderer();
}
/**
* Gets the special header cell renderer for the specified table field column. A null value
* indicates that this column uses a default header renderer.
*
* @param index the model column index
* @return a table cell renderer for this field's header. Otherwise, null if a default renderer
* should be used.
*/
@Override
public TableCellRenderer getHeaderRenderer(int index) {
return tableColumns.get(index).getHeaderRenderer();
}
/**
* Gets the maximum number of text display lines needed for any given cell within the specified
* column.

View file

@ -692,6 +692,13 @@ public class GTable extends JTable {
addColumn(newColumn);
}
for (int i = 0; i < columnCount; i++ ) {
TableCellRenderer headerRenderer = getHeaderRendererOverride(i);
if (headerRenderer != null) {
tableColumnModel.getColumn(i).setHeaderRenderer(headerRenderer);
}
}
tableColumnModel.setEventsEnabled(wasEnabled);
}
@ -890,6 +897,26 @@ public class GTable extends JTable {
return super.getCellRenderer(row, col);
}
/**
* Performs custom work to locate header renderers for special table model types. The headers
* are located and installed at the time the table's model is set.
*
* @param row the row
* @param col the column
* @return the header cell renderer
*/
public final TableCellRenderer getHeaderRendererOverride(int col) {
ConfigurableColumnTableModel configurableModel = getConfigurableColumnTableModel();
if (configurableModel != null) {
int modelIndex = convertColumnIndexToModel(col);
TableCellRenderer renderer = configurableModel.getHeaderRenderer(modelIndex);
if (renderer != null) {
return renderer;
}
}
return null;
}
/**
* If you just begin typing into an editable cell in a JTable, then the cell editor will be
* displayed. However, the editor component will not have a focus. This method has been

View file

@ -99,6 +99,11 @@ public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SO
return tableColumn.getColumnRenderer();
}
@Override
public GTableHeaderRenderer getHeaderRenderer() {
return tableColumn.getHeaderRenderer();
}
@Override
public SettingsDefinition[] getSettingsDefinitions() {
return tableColumn.getSettingsDefinitions();