diff --git a/Ghidra/Debug/Debugger/certification.manifest b/Ghidra/Debug/Debugger/certification.manifest index 6ece02c2da..3ac7ff87b8 100644 --- a/Ghidra/Debug/Debugger/certification.manifest +++ b/Ghidra/Debug/Debugger/certification.manifest @@ -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| diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html index 3382768817..92e6c1f23b 100644 --- a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html +++ b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/DebuggerThreadsPlugin.html @@ -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 Stack window, and the Dynamic Listing window @@ -50,13 +50,13 @@

Navigating Threads

-

Selecting a thread in the timeline or the table will navigate to (or "activate" or "focus") +

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 Registers window will display the activated thread's register values. Listing 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:

@@ -77,12 +77,15 @@ The state always reflects the present, or latest known, state.
  • Comment - a user-modifiable comment about the thread.
  • + +
  • 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.
  • Navigating Time

    -

    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 +

    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 Time 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 diff --git a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png index 2ad4a4bb74..3810e81bff 100644 Binary files a/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png and b/Ghidra/Debug/Debugger/src/main/help/help/topics/DebuggerThreadsPlugin/images/DebuggerThreadsPlugin.png differ 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 e05468951e..8bb047e2f1 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 @@ -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 cbCoordinateActivation = new SuppressableCallback<>(); private final ThreadsListener threadsListener = new ThreadsListener(); - private final VetoableSnapRequestListener snapListener = this::snapRequested; private final CollectionChangeListener recordersListener = new RecordersChangeListener(); + /* package access for testing */ + final RangeTableCellRenderer rangeRenderer = new RangeTableCellRenderer<>(); + final RangeCursorTableHeaderRenderer headerRenderer = + new RangeCursorTableHeaderRenderer<>(); protected final ThreadTableModel threadTableModel = new ThreadTableModel(this); private JPanel mainPanel; - private JSplitPane splitPane; HorizontalTabPanel traceTabs; GTable threadTable; GhidraTableFilterPanel 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 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) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsTimelinePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsTimelinePanel.java deleted file mode 100644 index dacb0ada5c..0000000000 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsTimelinePanel.java +++ /dev/null @@ -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 { - protected final RowObjectTableModel model; - - public ThreadTimelinePanel(RowObjectTableModel 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 listeners = - new ListenerSet<>(VetoableSnapRequestListener.class); - protected final RangeCursorValueListener valueListener = this::cursorValueChanged; - protected final TimelineListener timelineListener = new TimelineListener() { - @Override - public void viewRangeChanged(Range range) { - timelineViewRangeChanged(range); - } - }; - private DebuggerModelService modelService; - private RowObjectTableModel model; - - public DebuggerThreadsTimelinePanel(RowObjectTableModel 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 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 model = timeline.getTableModel(); - List 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); - } -} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java index 4c4c7d08d5..ba42286dec 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/ThreadTableColumns.java @@ -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 { - 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 getter; private final BiConsumer setter; + private final boolean sortable; private final Class cls; - ThreadTableColumns(String header, Class cls, Function getter) { - this(header, cls, getter, null); + ThreadTableColumns(String header, Class cls, Function getter, + boolean sortable) { + this(header, cls, getter, null, sortable); } @SuppressWarnings("unchecked") ThreadTableColumns(String header, Class cls, Function getter, - BiConsumer setter) { + BiConsumer setter, boolean sortable) { this.header = header; this.cls = cls; this.getter = getter; this.setter = (BiConsumer) setter; + this.sortable = sortable; } @Override @@ -65,6 +71,11 @@ public enum ThreadTableColumns implements EnumeratedTableColumn { - 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 diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorPanel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorPanel.java deleted file mode 100644 index de3ac910e8..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorPanel.java +++ /dev/null @@ -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 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 range, double value) { - int offset = valueToOffset(component.getWidth(), range, value); - g.translate(offset, 0); - } - - @Override - double getValue(Component component, MouseEvent e, Range range) { - return offsetToValue(component.getWidth(), range, e.getX()); - } - }, - VERTICAL { - @Override - void transform(Component component, Graphics2D g, Range range, double value) { - g.translate(0, valueToOffset(component.getHeight(), range, value)); - } - - @Override - double getValue(Component component, MouseEvent e, Range range) { - return offsetToValue(component.getHeight(), range, e.getY()); - } - }; - - protected static int valueToOffset(int size, Range 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 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 range, - double value); - - abstract double getValue(Component component, MouseEvent e, Range 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 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 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 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; - } -} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorValueListener.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorValueListener.java deleted file mode 100644 index cafbb32c6b..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/RangeCursorValueListener.java +++ /dev/null @@ -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); -} 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 new file mode 100644 index 0000000000..ff7b84722a --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeCursorTableHeaderRenderer.java @@ -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> + 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 fullRange = Range.closed(0d, 1d); + protected double span = 1; + + protected N pos; + protected double doublePos; + + public void setFullRange(Range 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 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; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java new file mode 100644 index 0000000000..93a3ca207d --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/table/RangeTableCellRenderer.java @@ -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> + extends AbstractGColumnRenderer> { + + protected Range doubleFullRange = Range.closed(0d, 1d); + protected double span = 1; + + protected Range fullRange; + protected Range dataRange; + + public static Range validateViewRange(Range 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 fullRange) { + this.fullRange = fullRange; + this.doubleFullRange = validateViewRange(fullRange); + this.span = this.doubleFullRange.upperEndpoint() - this.doubleFullRange.lowerEndpoint(); + } + + @Override + public String getFilterString(Range t, Settings settings) { + return ""; + } + + @Override + @SuppressWarnings("unchecked") + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + this.dataRange = (Range) 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 getFullRange() { + return fullRange; + } +} 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 new file mode 100644 index 0000000000..addc7c54a0 --- /dev/null +++ b/Ghidra/Debug/ProposedUtils/src/test/java/docking/widgets/table/DemoRangeCellRendererTest.java @@ -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 lifespan; + + public MyRow(String name, Range lifespan) { + this.name = name; + this.lifespan = lifespan; + } + + public String getName() { + return name; + } + + public Range getLifespan() { + return lifespan; + } + + public void setLifespan(Range lifespan) { + this.lifespan = lifespan; + } + + @Override + public String toString() { + return name; + } + } + + protected enum MyColumns implements EnumeratedTableColumn { + NAME("Name", String.class, MyRow::getName), + LIFESPAN("Lifespan", Range.class, MyRow::getLifespan); + + private String header; + private Class cls; + private Function getter; + + private MyColumns(String header, Class cls, Function 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 model = + new DefaultEnumeratedColumnTableModel<>("People", MyColumns.class); + GhidraTable table = new GhidraTable(model); + GTableFilterPanel filterPanel = new GTableFilterPanel<>(table, model); + + TableColumn column = table.getColumnModel().getColumn(MyColumns.LIFESPAN.ordinal()); + RangeTableCellRenderer rangeRenderer = new RangeTableCellRenderer<>(); + RangeCursorTableHeaderRenderer 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 windowClosed = new CompletableFuture<>(); + WindowListener listener = new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + windowClosed.complete(null); + } + }; + window.addWindowListener(listener); + window.setVisible(true); + windowClosed.get(); + } +}