From cdd6f3d72e8d08b0f3982e7fc74f1649c5cc6c56 Mon Sep 17 00:00:00 2001 From: Dan <46821332+nsadeveloper789@users.noreply.github.com> Date: Wed, 21 Sep 2022 11:28:12 -0400 Subject: [PATCH] GP-2067: Implement header renderer override and add cursor to ModelProvider's Plot columns --- .../gui/model/AbstractQueryTableModel.java | 3 + .../gui/model/AbstractQueryTablePanel.java | 5 + .../gui/model/DebuggerModelProvider.java | 12 ++ .../debug/gui/model/ObjectTableModel.java | 12 ++ .../core/debug/gui/model/PathTableModel.java | 12 ++ .../TracePathLastLifespanPlotColumn.java | 21 ++- .../columns/TraceValueLifePlotColumn.java | 21 ++- .../gui/thread/DebuggerThreadsProvider.java | 21 +-- .../table/RangeCursorTableHeaderRenderer.java | 151 +++++++++++------- .../table/DemoRangeCellRendererTest.java | 7 +- .../table/AbstractDynamicTableColumn.java | 5 + .../table/ConfigurableColumnTableModel.java | 8 + .../widgets/table/DynamicTableColumn.java | 10 ++ .../table/GDynamicColumnTableModel.java | 13 ++ .../java/docking/widgets/table/GTable.java | 27 ++++ .../widgets/table/MappedTableColumn.java | 5 + 16 files changed, 250 insertions(+), 83 deletions(-) diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java index d66c0a2ddc..15d2745c1b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java @@ -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 extends ThreadedTableModel extends JPanel { table.removeKeyListener(l); } + public void addSeekListener(SeekListener listener) { + tableModel.addSeekListener(listener); + } + public void setSelectionMode(int selectionMode) { table.setSelectionMode(selectionMode); } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java index 05f45deb13..7177cb14cd 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java @@ -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 diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java index 6a4ec73473..654946b8d0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java @@ -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 { column.setDiffColorSel(diffColorSel); } } + + @Override + protected void snapChanged() { + super.snapChanged(); + lifePlotColumn.setSnap(getSnap()); + } + + @Override + public void addSeekListener(SeekListener listener) { + lifePlotColumn.addSeekListener(listener); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java index 3ce61438b1..69001e1ef2 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java @@ -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 { 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); + } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java index 1be1443188..94a6b5e9d0 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java @@ -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, Trace> { private final RangeTableCellRenderer cellRenderer = new RangeTableCellRenderer<>(); + private final RangeCursorTableHeaderRenderer 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 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); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java index c7d408f18b..eb8d80b62e 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java @@ -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, Trace> { private final RangeSetTableCellRenderer cellRenderer = new RangeSetTableCellRenderer<>(); + private final RangeCursorTableHeaderRenderer 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 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); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 9c771f93ac..6759e44b4a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -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 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 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) diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java index 946f923eba..90e4292679 100644 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java @@ -25,8 +25,70 @@ import javax.swing.table.*; import com.google.common.collect.Range; +import ghidra.util.datastruct.ListenerSet; + public class RangeCursorTableHeaderRenderer> extends GTableHeaderRenderer implements RangedRenderer { + + public interface SeekListener extends Consumer { + } + + protected class ForSeekMouseListener extends MouseAdapter { + + @Override + public void mouseClicked(MouseEvent e) { + if ((e.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) != 0) { + return; + } + if ((e.getButton() != MouseEvent.BUTTON1)) { + return; + } + doSeek(e); + e.consume(); + } + + @Override + public void mouseDragged(MouseEvent e) { + int onmask = MouseEvent.BUTTON1_DOWN_MASK; + int offmask = MouseEvent.SHIFT_DOWN_MASK; + if ((e.getModifiersEx() & (onmask | offmask)) != onmask) { + return; + } + doSeek(e); + e.consume(); + } + + protected void doSeek(MouseEvent e) { + TableColumnModel colModel = savedTable.getColumnModel(); + JTableHeader header = savedTable.getTableHeader(); + TableColumn myViewCol = colModel.getColumn(savedViewColumn); + if (header.getResizingColumn() != null) { + return; + } + int clickedViewColIdx = colModel.getColumnIndexAtX(e.getX()); + if (clickedViewColIdx != savedViewColumn) { + return; + } + + TableColumn draggedViewCol = header.getDraggedColumn(); + if (draggedViewCol == myViewCol) { + header.setDraggedColumn(null); + } + else if (draggedViewCol != null) { + return; + } + + int colX = 0; + for (int i = 0; i < clickedViewColIdx; i++) { + colX += colModel.getColumn(i).getWidth(); + } + + double pos = + span * (e.getX() - colX) / myViewCol.getWidth() + fullRangeDouble.lowerEndpoint(); + listeners.fire.accept(pos); + } + } + protected final static int ARROW_SIZE = 10; protected final static Polygon ARROW = new Polygon( new int[] { 0, -ARROW_SIZE, -ARROW_SIZE }, @@ -40,6 +102,12 @@ public class RangeCursorTableHeaderRenderer> protected N pos; protected double doublePos; + private JTable savedTable; + private int savedViewColumn; + + private final ForSeekMouseListener forSeekMouseListener = new ForSeekMouseListener(); + private final ListenerSet listeners = new ListenerSet<>(SeekListener.class); + @Override public void setFullRange(Range fullRange) { this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange); @@ -51,6 +119,28 @@ public class RangeCursorTableHeaderRenderer> 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); @@ -69,65 +159,8 @@ public class RangeCursorTableHeaderRenderer> g.fillPolygon(ARROW); } - public void addSeekListener(JTable table, int modelColumn, Consumer 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) { - return; - } - if ((e.getButton() != MouseEvent.BUTTON1)) { - return; - } - doSeek(e); - e.consume(); - } - - @Override - public void mouseDragged(MouseEvent e) { - int onmask = MouseEvent.BUTTON1_DOWN_MASK; - int offmask = MouseEvent.SHIFT_DOWN_MASK; - if ((e.getModifiersEx() & (onmask | offmask)) != onmask) { - return; - } - doSeek(e); - e.consume(); - } - - protected void doSeek(MouseEvent e) { - if (header.getResizingColumn() != null) { - return; - } - int viewColIdx = colModel.getColumnIndexAtX(e.getX()); - int modelColIdx = table.convertColumnIndexToModel(viewColIdx); - if (modelColIdx != modelColumn) { - return; - } - - TableColumn draggedCol = header.getDraggedColumn(); - if (draggedCol == col) { - header.setDraggedColumn(null); - } - else if (draggedCol != null) { - return; - } - - int colX = 0; - for (int i = 0; i < viewColIdx; i++) { - colX += colModel.getColumn(i).getWidth(); - } - TableColumn col = colModel.getColumn(viewColIdx); - - double pos = - span * (e.getX() - colX) / col.getWidth() + fullRangeDouble.lowerEndpoint(); - listener.accept(pos); - } - }; - header.addMouseListener(l); - header.addMouseMotionListener(l); + public void addSeekListener(SeekListener listener) { + listeners.add(listener); } public N getCursorPosition() { diff --git a/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoRangeCellRendererTest.java b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoRangeCellRendererTest.java index 406ed45f25..05f7839179 100644 --- a/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoRangeCellRendererTest.java +++ b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoRangeCellRendererTest.java @@ -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); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java index 47a4f3c9ac..963261e502 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/AbstractDynamicTableColumn.java @@ -120,6 +120,11 @@ public abstract class AbstractDynamicTableColumn columnClass = getColumnClass(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ConfigurableColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ConfigurableColumnTableModel.java index aa78a8b915..a678f30935 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ConfigurableColumnTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/ConfigurableColumnTableModel.java @@ -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); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java index 512f2945cf..b98876d8cc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/DynamicTableColumn.java @@ -97,6 +97,16 @@ public interface DynamicTableColumn { */ public GColumnRenderer getColumnRenderer(); + /** + * Returns the optional header renderer for this column; null if no renderer is used. + * + *

+ * This method allows columns to define custom header rendering. + * + * @return the renderer + */ + public GTableHeaderRenderer getHeaderRenderer(); + /** * Returns a list of settings definitions for this field. * diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java index c9dadd14d1..8f3066997e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java @@ -540,6 +540,19 @@ public abstract class GDynamicColumnTableModel 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. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java index ec692f0e57..a8546e0348 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTable.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java index 1983fca094..41286fa6dd 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/MappedTableColumn.java @@ -99,6 +99,11 @@ public class MappedTableColumn