mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-23 13:16:48 +08:00
GP-931: Converted Thread Timeline to a fancy table column.
This commit is contained in:
@@ -117,7 +117,6 @@ src/main/help/help/topics/DebuggerTraceManagerServicePlugin/DebuggerTraceManager
|
||||
src/main/help/help/topics/DebuggerWatchesPlugin/DebuggerWatchesPlugin.html||GHIDRA||||END|
|
||||
src/main/help/help/topics/DebuggerWatchesPlugin/images/DebuggerWatchesPlugin.png||GHIDRA||||END|
|
||||
src/main/resources/defaultTools/Debugger.tool||GHIDRA||||END|
|
||||
src/main/resources/define_info_proc_mappings||GHIDRA||||END|
|
||||
src/main/resources/images/add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||
src/main/resources/images/attach.png||GHIDRA||||END|
|
||||
src/main/resources/images/autoread.png||GHIDRA||||END|
|
||||
|
||||
+8
-5
@@ -26,7 +26,7 @@
|
||||
each thread carries its own execution context, so this window provides a means of navigating
|
||||
those contexts. Furthermore, this window provides a means of navigating and managing open
|
||||
traces, much like the static listing provides a means of navigating open programs. The window
|
||||
also provides a timeline showing thread lifespans, and displays a caret which can be used to
|
||||
also plots a timeline showing thread lifespans, and displays a caret which can be used to
|
||||
navigate the current point in time. This window, the <A href=
|
||||
"help/topics/DebuggerStackPlugin/DebuggerStackPlugin.html">Stack</A> window, and the <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Dynamic Listing</A> window
|
||||
@@ -50,13 +50,13 @@
|
||||
|
||||
<H2>Navigating Threads</H2>
|
||||
|
||||
<P>Selecting a thread in the timeline or the table will navigate to (or "activate" or "focus")
|
||||
<P>Selecting a thread in the table will navigate to (or "activate" or "focus")
|
||||
that thread. Windows which are sensitive to the current thread will update. Notably, the <A
|
||||
href="help/topics/DebuggerRegistersPlugin/DebuggerRegistersPlugin.html">Registers</A> window
|
||||
will display the activated thread's register values. <A href=
|
||||
"help/topics/DebuggerListingPlugin/DebuggerListingPlugin.html">Listing</A> windows with
|
||||
configured location tracking will re-compute that location with the thread's context and
|
||||
navigate to it. The thread timeline displays all recorded threads. Threads which are alive will
|
||||
navigate to it. The thread timeline plots all recorded threads. Threads which are alive will
|
||||
appear to extend "to the end of time." The threads table displays more detailed information in
|
||||
the following columns:</P>
|
||||
|
||||
@@ -77,12 +77,15 @@
|
||||
The state always reflects the present, or latest known, state.</LI>
|
||||
|
||||
<LI>Comment - a user-modifiable comment about the thread.</LI>
|
||||
|
||||
<LI>Plot - a graphical representation of the thread's lifespan. Unlike other column headers, clicking and dragging
|
||||
in this one will navigate through time. To rearrange this column, hold SHIFT while dragging.</LI>
|
||||
</UL>
|
||||
|
||||
<H2>Navigating Time</H2>
|
||||
|
||||
<P>The user can navigate through time within the current trace by using the caret above the
|
||||
threads timeline. There are also actions for "stepping the trace" forward and backward. See the
|
||||
<P>The user can navigate through time within the current trace by using the caret in the plot
|
||||
column header. There are also actions for "stepping the trace" forward and backward. See the
|
||||
<A href="help/topics/DebuggerTimePlugin/DebuggerTimePlugin.html">Time</A> window for a way to
|
||||
display and navigate to specific events in the trace's timeline. Note that stepping away from
|
||||
the present will prevent most windows from interacting with the live target. While some
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
+45
-66
@@ -23,22 +23,22 @@ import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.table.TableColumn;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.WindowPosition;
|
||||
import docking.action.*;
|
||||
import docking.widgets.EventTrigger;
|
||||
import docking.widgets.HorizontalTabPanel;
|
||||
import docking.widgets.HorizontalTabPanel.TabListCellRenderer;
|
||||
import docking.widgets.table.GTable;
|
||||
import docking.widgets.table.RowWrappedEnumeratedColumnTableModel;
|
||||
import docking.widgets.timeline.TimelineListener;
|
||||
import docking.widgets.table.*;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext;
|
||||
import ghidra.app.plugin.core.debug.gui.thread.DebuggerThreadsTimelinePanel.VetoableSnapRequestListener;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.app.services.DebuggerTraceManagerService.BooleanChangeAdapter;
|
||||
import ghidra.dbg.DebugModelConventions;
|
||||
@@ -333,30 +333,24 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
private final SuppressableCallback<Void> cbCoordinateActivation = new SuppressableCallback<>();
|
||||
|
||||
private final ThreadsListener threadsListener = new ThreadsListener();
|
||||
private final VetoableSnapRequestListener snapListener = this::snapRequested;
|
||||
private final CollectionChangeListener<TraceRecorder> recordersListener =
|
||||
new RecordersChangeListener();
|
||||
/* package access for testing */
|
||||
final RangeTableCellRenderer<Long> rangeRenderer = new RangeTableCellRenderer<>();
|
||||
final RangeCursorTableHeaderRenderer<Long> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
|
||||
protected final ThreadTableModel threadTableModel = new ThreadTableModel(this);
|
||||
|
||||
private JPanel mainPanel;
|
||||
private JSplitPane splitPane;
|
||||
|
||||
HorizontalTabPanel<Trace> traceTabs;
|
||||
GTable threadTable;
|
||||
GhidraTableFilterPanel<ThreadRow> threadFilterPanel;
|
||||
DebuggerThreadsTimelinePanel threadTimeline;
|
||||
JPopupMenu traceTabPopupMenu;
|
||||
|
||||
private ActionContext myActionContext;
|
||||
|
||||
private final TimelineListener timelineListener = new TimelineListener() {
|
||||
@Override
|
||||
public void itemActivated(int index) {
|
||||
timelineItemActivated(index);
|
||||
}
|
||||
};
|
||||
|
||||
DockingAction actionSaveTrace;
|
||||
StepSnapBackwardAction actionStepSnapBackward;
|
||||
StepTickBackwardAction actionStepTickBackward;
|
||||
@@ -464,10 +458,8 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
|
||||
private void doSetSnap(long snap) {
|
||||
if (threadTimeline.getSnap() == snap) {
|
||||
return;
|
||||
}
|
||||
threadTimeline.setSnap(snap);
|
||||
headerRenderer.setCursorPosition(snap);
|
||||
threadTable.getTableHeader().repaint();
|
||||
}
|
||||
|
||||
public void traceOpened(Trace trace) {
|
||||
@@ -508,7 +500,11 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
|
||||
protected void updateTimelineMax() {
|
||||
threadTimeline.setMaxSnapAtLeast(orZero(current.getTrace().getTimeManager().getMaxSnap()));
|
||||
long max = orZero(current.getTrace().getTimeManager().getMaxSnap());
|
||||
Range<Long> fullRange = Range.closed(0L, max + 1);
|
||||
rangeRenderer.setFullRange(fullRange);
|
||||
headerRenderer.setFullRange(fullRange);
|
||||
threadTable.getTableHeader().repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -524,35 +520,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
return myActionContext;
|
||||
}
|
||||
|
||||
private void snapRequested(long req, EventTrigger trigger) {
|
||||
long snap = req;
|
||||
if (snap < 0) {
|
||||
snap = 0;
|
||||
}
|
||||
if (current.getTrace() == null) {
|
||||
snap = 0;
|
||||
}
|
||||
/*else {
|
||||
Long maxSnap = currentTrace.getTimeManager().getMaxSnap();
|
||||
if (maxSnap == null) {
|
||||
maxSnap = 0L;
|
||||
}
|
||||
if (snap > maxSnap) {
|
||||
snap = maxSnap;
|
||||
}
|
||||
}*/
|
||||
if (trigger == EventTrigger.GUI_ACTION) {
|
||||
traceManager.activateSnap(snap);
|
||||
}
|
||||
myActionContext = new DebuggerSnapActionContext(snap);
|
||||
contextChanged();
|
||||
}
|
||||
|
||||
private void timelineItemActivated(int index) {
|
||||
ThreadRow row = threadTableModel.getRowObject(index);
|
||||
rowActivated(row);
|
||||
}
|
||||
|
||||
private void rowActivated(ThreadRow row) {
|
||||
TraceThread thread = row.getThread();
|
||||
Trace trace = thread.getTrace();
|
||||
@@ -569,27 +536,15 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
traceTabPopupMenu = new JPopupMenu("Trace");
|
||||
|
||||
mainPanel = new JPanel(new BorderLayout());
|
||||
splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
|
||||
splitPane.setContinuousLayout(true);
|
||||
|
||||
JPanel tablePanel = new JPanel(new BorderLayout());
|
||||
threadTable = new GhidraTable(threadTableModel);
|
||||
threadTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
tablePanel.add(new JScrollPane(threadTable));
|
||||
mainPanel.add(new JScrollPane(threadTable));
|
||||
|
||||
threadFilterPanel = new GhidraTableFilterPanel<>(threadTable, threadTableModel);
|
||||
tablePanel.add(threadFilterPanel, BorderLayout.SOUTH);
|
||||
splitPane.setLeftComponent(tablePanel);
|
||||
|
||||
threadTimeline = new DebuggerThreadsTimelinePanel(threadFilterPanel.getTableFilterModel());
|
||||
splitPane.setRightComponent(threadTimeline);
|
||||
|
||||
splitPane.setResizeWeight(0.4);
|
||||
mainPanel.add(threadFilterPanel, BorderLayout.SOUTH);
|
||||
|
||||
threadTable.getSelectionModel().addListSelectionListener(this::threadRowSelected);
|
||||
threadTimeline.setSelectionModel(threadTable.getSelectionModel());
|
||||
threadTimeline.addSnapRequestedListener(snapListener);
|
||||
threadTimeline.addTimelineListener(timelineListener);
|
||||
|
||||
threadTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
@@ -599,8 +554,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
}
|
||||
});
|
||||
|
||||
mainPanel.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
traceTabs = new HorizontalTabPanel<>();
|
||||
traceTabs.getList().setCellRenderer(new TabListCellRenderer<>() {
|
||||
protected String getText(Trace value) {
|
||||
@@ -633,6 +586,32 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
|
||||
});
|
||||
// TODO: The popup key? Only seems to have rawCode=0x93 (147) in Swing
|
||||
mainPanel.add(traceTabs, BorderLayout.NORTH);
|
||||
|
||||
TableColumnModel columnModel = threadTable.getColumnModel();
|
||||
TableColumn colName = columnModel.getColumn(ThreadTableColumns.NAME.ordinal());
|
||||
colName.setPreferredWidth(100);
|
||||
TableColumn colCreated = columnModel.getColumn(ThreadTableColumns.CREATED.ordinal());
|
||||
colCreated.setPreferredWidth(10);
|
||||
TableColumn colDestroyed = columnModel.getColumn(ThreadTableColumns.DESTROYED.ordinal());
|
||||
colDestroyed.setPreferredWidth(10);
|
||||
TableColumn colState = columnModel.getColumn(ThreadTableColumns.STATE.ordinal());
|
||||
colState.setPreferredWidth(20);
|
||||
TableColumn colComment = columnModel.getColumn(ThreadTableColumns.COMMENT.ordinal());
|
||||
colComment.setPreferredWidth(100);
|
||||
TableColumn colPlot = columnModel.getColumn(ThreadTableColumns.PLOT.ordinal());
|
||||
colPlot.setPreferredWidth(200);
|
||||
colPlot.setCellRenderer(rangeRenderer);
|
||||
colPlot.setHeaderRenderer(headerRenderer);
|
||||
|
||||
headerRenderer.addSeekListener(threadTable, ThreadTableColumns.PLOT.ordinal(), pos -> {
|
||||
long snap = Math.round(pos);
|
||||
if (current.getTrace() == null || snap < 0) {
|
||||
snap = 0;
|
||||
}
|
||||
traceManager.activateSnap(snap);
|
||||
myActionContext = new DebuggerSnapActionContext(snap);
|
||||
contextChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void checkTraceTabPopupViaMouse(MouseEvent e) {
|
||||
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.thread;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Rectangle;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.ListSelectionModel;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.widgets.*;
|
||||
import docking.widgets.RangeCursorPanel.Direction;
|
||||
import docking.widgets.table.RowObjectTableModel;
|
||||
import docking.widgets.timeline.TimelineListener;
|
||||
import docking.widgets.timeline.TimelinePanel;
|
||||
import ghidra.app.services.DebuggerModelService;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
||||
public class DebuggerThreadsTimelinePanel extends JScrollPane {
|
||||
static class SnapRequestVetoedException extends Exception {
|
||||
private final long requestedSnap;
|
||||
private final long newSnap;
|
||||
|
||||
public SnapRequestVetoedException(long requestedSnap, long newSnap) {
|
||||
this.requestedSnap = requestedSnap;
|
||||
this.newSnap = newSnap;
|
||||
}
|
||||
|
||||
public long getRequestedSnap() {
|
||||
return requestedSnap;
|
||||
}
|
||||
|
||||
public long getNewSnap() {
|
||||
return newSnap;
|
||||
}
|
||||
}
|
||||
|
||||
public interface VetoableSnapRequestListener {
|
||||
void snapRequested(long snap, EventTrigger trigger) throws SnapRequestVetoedException;
|
||||
}
|
||||
|
||||
protected class ThreadTimelinePanel extends TimelinePanel<ThreadRow, Long> {
|
||||
protected final RowObjectTableModel<ThreadRow> model;
|
||||
|
||||
public ThreadTimelinePanel(RowObjectTableModel<ThreadRow> model) {
|
||||
super(model, ThreadRow::getLifespan);
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
protected class ThreadRangeCursorPanel extends RangeCursorPanel {
|
||||
public ThreadRangeCursorPanel(Direction direction) {
|
||||
super(direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double adjustRequestedValue(double requested) {
|
||||
double rounded = Math.round(requested);
|
||||
// TODO: Also remove 1-snap view buffer?
|
||||
// Until I figure out the event processing order, leaving the buffer
|
||||
// prevents some stepping glitches.
|
||||
if (range.hasLowerBound() && rounded < range.lowerEndpoint() /*+1*/) {
|
||||
return range.lowerEndpoint(); // +1
|
||||
}
|
||||
if (range.hasUpperBound() && rounded > range.upperEndpoint() /*-1*/) {
|
||||
return range.upperEndpoint(); // -1
|
||||
}
|
||||
return rounded;
|
||||
}
|
||||
}
|
||||
|
||||
protected final ThreadTimelinePanel timeline;
|
||||
protected final RangeCursorPanel topCursor = new ThreadRangeCursorPanel(Direction.SOUTH) {
|
||||
Dimension preferredSize = new Dimension(super.getPreferredSize());
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
preferredSize.width = timeline.getPreferredSize().width;
|
||||
return preferredSize;
|
||||
}
|
||||
};
|
||||
protected final ListenerSet<VetoableSnapRequestListener> listeners =
|
||||
new ListenerSet<>(VetoableSnapRequestListener.class);
|
||||
protected final RangeCursorValueListener valueListener = this::cursorValueChanged;
|
||||
protected final TimelineListener timelineListener = new TimelineListener() {
|
||||
@Override
|
||||
public void viewRangeChanged(Range<Double> range) {
|
||||
timelineViewRangeChanged(range);
|
||||
}
|
||||
};
|
||||
private DebuggerModelService modelService;
|
||||
private RowObjectTableModel<ThreadRow> model;
|
||||
|
||||
public DebuggerThreadsTimelinePanel(RowObjectTableModel<ThreadRow> model) {
|
||||
this.model = model;
|
||||
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
|
||||
|
||||
timeline = new ThreadTimelinePanel(model);
|
||||
setViewportView(timeline);
|
||||
setColumnHeaderView(topCursor);
|
||||
|
||||
topCursor.addValueListener(valueListener);
|
||||
timeline.addTimelineListener(timelineListener);
|
||||
}
|
||||
|
||||
public void addSnapRequestedListener(VetoableSnapRequestListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void cursorValueChanged(double value, EventTrigger trigger) {
|
||||
try {
|
||||
listeners.fire.snapRequested(Math.round(value), trigger);
|
||||
}
|
||||
catch (SnapRequestVetoedException e) {
|
||||
value = e.getNewSnap();
|
||||
}
|
||||
}
|
||||
|
||||
private void timelineViewRangeChanged(Range<Double> range) {
|
||||
topCursor.setRange(range);
|
||||
}
|
||||
|
||||
public void setSelectionModel(ListSelectionModel selectionModel) {
|
||||
timeline.setSelectionModel(selectionModel);
|
||||
}
|
||||
|
||||
public void setSnap(long snap) {
|
||||
topCursor.requestValue(snap);
|
||||
}
|
||||
|
||||
public long getSnap() {
|
||||
// TODO: If there are enough snaps, we may not have the required precision
|
||||
// Consider BigDecimal in cursor? Eww.
|
||||
return (long) topCursor.getValue();
|
||||
}
|
||||
|
||||
public void setMaxSnapAtLeast(long maxSnapAtLeast) {
|
||||
timeline.setMaxAtLeast(maxSnapAtLeast);
|
||||
}
|
||||
|
||||
public long getMaxSnapAtLeast() {
|
||||
return (long) timeline.getMaxAtLeast();
|
||||
}
|
||||
|
||||
public ThreadRow findRow(TraceThread thread) {
|
||||
RowObjectTableModel<ThreadRow> model = timeline.getTableModel();
|
||||
List<ThreadRow> data = model.getModelData();
|
||||
for (ThreadRow row : data) {
|
||||
if (row.getThread() == thread) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Rectangle getCellBounds(TraceThread thread) {
|
||||
ThreadRow row = findRow(thread);
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
return timeline.getCellBounds(row);
|
||||
}
|
||||
|
||||
public void addTimelineListener(TimelineListener listener) {
|
||||
timeline.addTimelineListener(listener);
|
||||
}
|
||||
}
|
||||
+19
-8
@@ -18,31 +18,37 @@ package ghidra.app.plugin.core.debug.gui.thread;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
|
||||
|
||||
public enum ThreadTableColumns implements EnumeratedTableColumn<ThreadTableColumns, ThreadRow> {
|
||||
NAME("Name", String.class, ThreadRow::getName, ThreadRow::setName),
|
||||
CREATED("Created", Long.class, ThreadRow::getCreationSnap),
|
||||
DESTROYED("Destroyed", String.class, ThreadRow::getDestructionSnap),
|
||||
STATE("State", ThreadState.class, ThreadRow::getState),
|
||||
COMMENT("Comment", String.class, ThreadRow::getComment, ThreadRow::setComment);
|
||||
NAME("Name", String.class, ThreadRow::getName, ThreadRow::setName, true),
|
||||
CREATED("Created", Long.class, ThreadRow::getCreationSnap, true),
|
||||
DESTROYED("Destroyed", String.class, ThreadRow::getDestructionSnap, true),
|
||||
STATE("State", ThreadState.class, ThreadRow::getState, true),
|
||||
COMMENT("Comment", String.class, ThreadRow::getComment, ThreadRow::setComment, true),
|
||||
PLOT("Plot", Range.class, ThreadRow::getLifespan, false);
|
||||
|
||||
private final String header;
|
||||
private final Function<ThreadRow, ?> getter;
|
||||
private final BiConsumer<ThreadRow, Object> setter;
|
||||
private final boolean sortable;
|
||||
private final Class<?> cls;
|
||||
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter) {
|
||||
this(header, cls, getter, null);
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
boolean sortable) {
|
||||
this(header, cls, getter, null, sortable);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> ThreadTableColumns(String header, Class<T> cls, Function<ThreadRow, T> getter,
|
||||
BiConsumer<ThreadRow, T> setter) {
|
||||
BiConsumer<ThreadRow, T> setter, boolean sortable) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
this.setter = (BiConsumer<ThreadRow, Object>) setter;
|
||||
this.sortable = sortable;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,6 +71,11 @@ public enum ThreadTableColumns implements EnumeratedTableColumn<ThreadTableColum
|
||||
return setter != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSortable() {
|
||||
return sortable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueOf(ThreadRow row, Object value) {
|
||||
setter.accept(row, value);
|
||||
|
||||
+9
-57
@@ -17,17 +17,15 @@ package ghidra.app.plugin.core.debug.gui.thread;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.widgets.EventTrigger;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
|
||||
import ghidra.app.services.TraceRecorder;
|
||||
import ghidra.trace.model.Trace;
|
||||
@@ -264,14 +262,14 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(0, threadsProvider.threadTimeline.getMaxSnapAtLeast());
|
||||
assertEquals(1, threadsProvider.rangeRenderer.getFullRange().upperEndpoint().longValue());
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
manager.getSnapshot(10, true);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(10, threadsProvider.threadTimeline.getMaxSnapAtLeast());
|
||||
assertEquals(11, threadsProvider.rangeRenderer.getFullRange().upperEndpoint().longValue());
|
||||
}
|
||||
|
||||
// NOTE: Do not test delete updates timeline max, as maxSnap does not reflect deletion
|
||||
@@ -290,8 +288,7 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
|
||||
assertEquals("15",
|
||||
threadsProvider.threadTableModel.getModelData().get(0).getDestructionSnap());
|
||||
assertEquals(Range.closed(-1d, 16d),
|
||||
threadsProvider.threadTimeline.timeline.getViewRange());
|
||||
// NOTE: Plot max is based on time table, never thread destruction
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -301,8 +298,7 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(Range.closed(-1d, 11d),
|
||||
threadsProvider.threadTimeline.timeline.getViewRange());
|
||||
assertEquals(2, threadsProvider.threadTableModel.getModelData().size());
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
thread2.delete();
|
||||
@@ -310,7 +306,7 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(1, threadsProvider.threadTableModel.getModelData().size());
|
||||
assertEquals(Range.closed(-1d, 1d), threadsProvider.threadTimeline.timeline.getViewRange());
|
||||
// NOTE: Plot max is based on time table, never thread destruction
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -381,33 +377,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("TODO") // Not sure why this fails under Gradle but not my IDE
|
||||
public void testSelectThreadInTimelineActivatesThread() throws Exception {
|
||||
createAndOpenTrace();
|
||||
addThreads();
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForDomainObject(tb.trace);
|
||||
|
||||
assertThreadsPopulated();
|
||||
assertThreadSelected(thread1);
|
||||
|
||||
// Otherwise, this test fails unpredictably
|
||||
waitForPass(noExc(() -> {
|
||||
Rectangle b = threadsProvider.threadTimeline.getCellBounds(thread2);
|
||||
threadsProvider.threadTimeline.scrollRectToVisible(b);
|
||||
Point tsl = threadsProvider.threadTimeline.getLocationOnScreen();
|
||||
Point vp = threadsProvider.threadTimeline.getViewport().getViewPosition();
|
||||
Point m =
|
||||
new Point(tsl.x + b.x + b.width / 2 - vp.x, tsl.y + b.y + b.height / 2 - vp.y);
|
||||
clickMouse(MouseEvent.BUTTON1, m);
|
||||
waitForSwing();
|
||||
|
||||
assertThreadSelected(thread2);
|
||||
assertEquals(thread2, traceManager.getCurrentThread());
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActivateSnapUpdatesTimelineCursor() throws Exception {
|
||||
createAndOpenTrace();
|
||||
@@ -417,29 +386,12 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerGUI
|
||||
|
||||
assertThreadsPopulated();
|
||||
assertEquals(0, traceManager.getCurrentSnap());
|
||||
assertEquals(0, (int) threadsProvider.threadTimeline.topCursor.getValue());
|
||||
assertEquals(0, threadsProvider.headerRenderer.getCursorPosition().longValue());
|
||||
|
||||
traceManager.activateSnap(6);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(6, (int) threadsProvider.threadTimeline.topCursor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSeekTimelineActivatesSnap() throws Exception {
|
||||
createAndOpenTrace();
|
||||
addThreads();
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
|
||||
assertThreadsPopulated();
|
||||
assertEquals(0, traceManager.getCurrentSnap());
|
||||
assertEquals(0, (int) threadsProvider.threadTimeline.topCursor.getValue());
|
||||
|
||||
threadsProvider.threadTimeline.topCursor.requestValue(6, EventTrigger.GUI_ACTION);
|
||||
waitForSwing();
|
||||
|
||||
assertEquals(6, traceManager.getCurrentSnap());
|
||||
assertEquals(6, threadsProvider.headerRenderer.getCursorPosition().longValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.util.datastruct.ListenerSet;
|
||||
|
||||
public class RangeCursorPanel extends JPanel {
|
||||
protected final static int MIN_SIZE = 16;
|
||||
protected final static Dimension MIN_BOX = new Dimension(MIN_SIZE, MIN_SIZE);
|
||||
|
||||
protected final static Polygon ARROW =
|
||||
new Polygon(new int[] { 0, -MIN_SIZE, -MIN_SIZE }, new int[] { 0, MIN_SIZE, -MIN_SIZE }, 3);
|
||||
|
||||
protected static double clamp(Range<Double> range, double value) {
|
||||
return Math.max(range.lowerEndpoint(), Math.min(value, range.upperEndpoint()));
|
||||
}
|
||||
|
||||
protected enum Orientation {
|
||||
HORIZONTAL {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g, Range<Double> range, double value) {
|
||||
int offset = valueToOffset(component.getWidth(), range, value);
|
||||
g.translate(offset, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
double getValue(Component component, MouseEvent e, Range<Double> range) {
|
||||
return offsetToValue(component.getWidth(), range, e.getX());
|
||||
}
|
||||
},
|
||||
VERTICAL {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g, Range<Double> range, double value) {
|
||||
g.translate(0, valueToOffset(component.getHeight(), range, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
double getValue(Component component, MouseEvent e, Range<Double> range) {
|
||||
return offsetToValue(component.getHeight(), range, e.getY());
|
||||
}
|
||||
};
|
||||
|
||||
protected static int valueToOffset(int size, Range<Double> range, double value) {
|
||||
double lower = range.lowerEndpoint();
|
||||
double length = range.upperEndpoint() - lower;
|
||||
double diff = value - lower;
|
||||
return (int) (diff * size / length);
|
||||
}
|
||||
|
||||
protected static double offsetToValue(int size, Range<Double> range, int offset) {
|
||||
double lower = range.lowerEndpoint();
|
||||
double length = range.upperEndpoint() - lower;
|
||||
double diff = length * offset / size;
|
||||
return lower + diff;
|
||||
}
|
||||
|
||||
abstract void transform(Component component, Graphics2D g, Range<Double> range,
|
||||
double value);
|
||||
|
||||
abstract double getValue(Component component, MouseEvent e, Range<Double> range);
|
||||
}
|
||||
|
||||
public enum Direction {
|
||||
EAST(Orientation.VERTICAL) {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g) {
|
||||
g.translate(component.getWidth(), 0);
|
||||
}
|
||||
},
|
||||
NORTH(Orientation.HORIZONTAL) {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g) {
|
||||
g.rotate(-Math.PI / 2);
|
||||
}
|
||||
},
|
||||
WEST(Orientation.VERTICAL) {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g) {
|
||||
g.rotate(Math.PI);
|
||||
}
|
||||
},
|
||||
SOUTH(Orientation.HORIZONTAL) {
|
||||
@Override
|
||||
void transform(Component component, Graphics2D g) {
|
||||
g.translate(0, component.getHeight());
|
||||
g.rotate(Math.PI / 2);
|
||||
}
|
||||
};
|
||||
|
||||
protected final Orientation orientation;
|
||||
|
||||
Direction(Orientation orientation) {
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
abstract void transform(Component component, Graphics2D g);
|
||||
}
|
||||
|
||||
protected final ListenerSet<RangeCursorValueListener> listeners =
|
||||
new ListenerSet<>(RangeCursorValueListener.class);
|
||||
|
||||
protected final MouseListener mouseListener = new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getButton() != MouseEvent.BUTTON1) {
|
||||
return;
|
||||
}
|
||||
doSeek(e);
|
||||
}
|
||||
};
|
||||
|
||||
protected final MouseMotionListener mouseMotionListener = new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0) {
|
||||
return;
|
||||
}
|
||||
doSeek(e);
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
addMouseListener(mouseListener);
|
||||
addMouseMotionListener(mouseMotionListener);
|
||||
}
|
||||
|
||||
protected Direction direction;
|
||||
protected Range<Double> range = Range.closed(-1.0, 1.0);
|
||||
protected double value;
|
||||
|
||||
public RangeCursorPanel(Direction direction) {
|
||||
this.direction = direction;
|
||||
this.setFocusable(true);
|
||||
}
|
||||
|
||||
protected void doSeek(MouseEvent e) {
|
||||
double requested = direction.orientation.getValue(RangeCursorPanel.this, e, range);
|
||||
requestValue(requested, EventTrigger.GUI_ACTION);
|
||||
}
|
||||
|
||||
public void addValueListener(RangeCursorValueListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeValueListener(RangeCursorValueListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void setDirection(Direction direction) {
|
||||
this.direction = direction;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setRange(Range<Double> range) {
|
||||
this.range = range;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void requestValue(double requested) {
|
||||
requestValue(requested, EventTrigger.API_CALL);
|
||||
}
|
||||
|
||||
public void requestValue(double requested, EventTrigger trigger) {
|
||||
double val = adjustRequestedValue(requested);
|
||||
if (this.value == val) {
|
||||
return;
|
||||
}
|
||||
this.value = val;
|
||||
listeners.fire.valueChanged(val, trigger);
|
||||
repaint();
|
||||
}
|
||||
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
//g2.setColor(Color.GREEN);
|
||||
//g2.fillPolygon(ARROW);
|
||||
direction.orientation.transform(this, g2, range, value);
|
||||
//g2.setColor(Color.RED);
|
||||
//g2.fillPolygon(ARROW);
|
||||
direction.transform(this, g2);
|
||||
g2.setColor(getForeground());
|
||||
g2.fillPolygon(ARROW);
|
||||
}
|
||||
|
||||
protected double adjustRequestedValue(double requested) {
|
||||
// Extension point
|
||||
return clamp(range, requested);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
return MIN_BOX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMinimumSize() {
|
||||
return MIN_BOX;
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets;
|
||||
|
||||
public interface RangeCursorValueListener {
|
||||
void valueChanged(double position, EventTrigger trigger);
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.table.*;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
extends GTableHeaderRenderer {
|
||||
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> fullRange = Range.closed(0d, 1d);
|
||||
protected double span = 1;
|
||||
|
||||
protected N pos;
|
||||
protected double doublePos;
|
||||
|
||||
public void setFullRange(Range<N> fullRange) {
|
||||
this.fullRange = RangeTableCellRenderer.validateViewRange(fullRange);
|
||||
this.span = this.fullRange.upperEndpoint() - this.fullRange.lowerEndpoint();
|
||||
}
|
||||
|
||||
public void setCursorPosition(N pos) {
|
||||
this.pos = pos;
|
||||
this.doublePos = pos.doubleValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintChildren(Graphics g) {
|
||||
super.paintChildren(g);
|
||||
paintCursor(g);
|
||||
}
|
||||
|
||||
protected void paintCursor(Graphics parentG) {
|
||||
Graphics2D g = (Graphics2D) parentG.create();
|
||||
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
double x = (doublePos - fullRange.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) {
|
||||
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() + fullRange.lowerEndpoint();
|
||||
listener.accept(pos);
|
||||
}
|
||||
};
|
||||
header.addMouseListener(l);
|
||||
header.addMouseMotionListener(l);
|
||||
}
|
||||
|
||||
public N getCursorPosition() {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Graphics;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||
|
||||
public class RangeTableCellRenderer<N extends Number & Comparable<N>>
|
||||
extends AbstractGColumnRenderer<Range<N>> {
|
||||
|
||||
protected Range<Double> doubleFullRange = Range.closed(0d, 1d);
|
||||
protected double span = 1;
|
||||
|
||||
protected Range<N> fullRange;
|
||||
protected Range<N> dataRange;
|
||||
|
||||
public static Range<Double> validateViewRange(Range<? extends Number> fullRange) {
|
||||
if (!fullRange.hasLowerBound() || !fullRange.hasUpperBound()) {
|
||||
throw new IllegalArgumentException("Cannot have unbounded full range");
|
||||
}
|
||||
// I don't care to preserve open/closed, since it just specifies the view bounds
|
||||
return Range.closed(fullRange.lowerEndpoint().doubleValue(),
|
||||
fullRange.upperEndpoint().doubleValue());
|
||||
}
|
||||
|
||||
public void setFullRange(Range<N> fullRange) {
|
||||
this.fullRange = fullRange;
|
||||
this.doubleFullRange = validateViewRange(fullRange);
|
||||
this.span = this.doubleFullRange.upperEndpoint() - this.doubleFullRange.lowerEndpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterString(Range<N> t, Settings settings) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
|
||||
this.dataRange = (Range<N>) data.getValue();
|
||||
super.getTableCellRendererComponent(data);
|
||||
setText("");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics parentG) {
|
||||
super.paintComponent(parentG);
|
||||
if (dataRange == null) {
|
||||
return;
|
||||
}
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
int x1 = dataRange.hasLowerBound()
|
||||
? interpolate(width, dataRange.lowerEndpoint().doubleValue())
|
||||
: 0;
|
||||
int x2 = dataRange.hasUpperBound()
|
||||
? interpolate(width, dataRange.upperEndpoint().doubleValue())
|
||||
: width;
|
||||
|
||||
int y1 = height > 2 ? 1 : 0;
|
||||
int y2 = height > 2 ? height - 1 : height;
|
||||
|
||||
Graphics g = parentG.create();
|
||||
g.setColor(getForeground());
|
||||
|
||||
g.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
protected int interpolate(int w, double val) {
|
||||
double lower = doubleFullRange.lowerEndpoint();
|
||||
if (val <= lower) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= doubleFullRange.upperEndpoint()) {
|
||||
return w;
|
||||
}
|
||||
double dif = val - lower;
|
||||
return (int) (dif / span * w);
|
||||
}
|
||||
|
||||
public Range<N> getFullRange() {
|
||||
return fullRange;
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import static org.junit.Assume.assumeFalse;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.event.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.table.TableColumn;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.table.GhidraTable;
|
||||
|
||||
public class DemoRangeCellRendererTest {
|
||||
@Before
|
||||
public void checkNotBatch() {
|
||||
assumeFalse(SystemUtilities.isInTestingBatchMode());
|
||||
}
|
||||
|
||||
protected static class MyRow {
|
||||
private final String name;
|
||||
private Range<Integer> lifespan;
|
||||
|
||||
public MyRow(String name, Range<Integer> lifespan) {
|
||||
this.name = name;
|
||||
this.lifespan = lifespan;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Range<Integer> getLifespan() {
|
||||
return lifespan;
|
||||
}
|
||||
|
||||
public void setLifespan(Range<Integer> lifespan) {
|
||||
this.lifespan = lifespan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
protected enum MyColumns implements EnumeratedTableColumn<MyColumns, MyRow> {
|
||||
NAME("Name", String.class, MyRow::getName),
|
||||
LIFESPAN("Lifespan", Range.class, MyRow::getLifespan);
|
||||
|
||||
private String header;
|
||||
private Class<?> cls;
|
||||
private Function<MyRow, ?> getter;
|
||||
|
||||
private <T> MyColumns(String header, Class<T> cls, Function<MyRow, T> getter) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getValueClass() {
|
||||
return cls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueOf(MyRow row) {
|
||||
return getter.apply(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSortable() {
|
||||
return this != LIFESPAN;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDemoRangeCellRenderer() throws Throwable {
|
||||
JFrame window = new JFrame();
|
||||
window.setLayout(new BorderLayout());
|
||||
|
||||
DefaultEnumeratedColumnTableModel<MyColumns, MyRow> model =
|
||||
new DefaultEnumeratedColumnTableModel<>("People", MyColumns.class);
|
||||
GhidraTable table = new GhidraTable(model);
|
||||
GTableFilterPanel<MyRow> filterPanel = new GTableFilterPanel<>(table, model);
|
||||
|
||||
TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal());
|
||||
RangeTableCellRenderer<Integer> rangeRenderer = new RangeTableCellRenderer<>();
|
||||
RangeCursorTableHeaderRenderer<Integer> headerRenderer =
|
||||
new RangeCursorTableHeaderRenderer<>();
|
||||
column.setCellRenderer(rangeRenderer);
|
||||
column.setHeaderRenderer(headerRenderer);
|
||||
|
||||
rangeRenderer.setFullRange(Range.closed(1800, 2000));
|
||||
headerRenderer.setFullRange(Range.closed(1800, 2000));
|
||||
headerRenderer.setCursorPosition(1940);
|
||||
|
||||
model.add(new MyRow("Albert", Range.closed(1879, 1955)));
|
||||
model.add(new MyRow("Bob", Range.atLeast(1956)));
|
||||
model.add(new MyRow("Elvis", Range.closed(1935, 1977)));
|
||||
|
||||
headerRenderer.addSeekListener(table, MyColumns.LIFESPAN.ordinal(), pos -> {
|
||||
System.out.println("pos: " + pos);
|
||||
headerRenderer.setCursorPosition(pos.intValue());
|
||||
table.getTableHeader().repaint();
|
||||
});
|
||||
|
||||
window.add(new JScrollPane(table));
|
||||
window.add(filterPanel, BorderLayout.SOUTH);
|
||||
|
||||
window.setBounds(0, 0, 1000, 200);
|
||||
CompletableFuture<Void> windowClosed = new CompletableFuture<>();
|
||||
WindowListener listener = new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
windowClosed.complete(null);
|
||||
}
|
||||
};
|
||||
window.addWindowListener(listener);
|
||||
window.setVisible(true);
|
||||
windowClosed.get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user