getDisconnectedProviders();
+
+ protected abstract P createDisconnectedProvider();
+
+ protected abstract void removeDisconnectedProvider(P p);
+
+ protected void doWrite(SaveState saveState, BiConsumer super P, ? super SaveState> writer) {
+ P cp = getConnectedProvider();
+ SaveState cpState = new SaveState();
+ writer.accept(cp, cpState);
+ saveState.putXmlElement(KEY_CONNECTED_PROVIDER, cpState.saveToXml());
+
+ List
disconnectedProviders = getDisconnectedProviders();
+ List
disconnected;
+ synchronized (disconnectedProviders) {
+ disconnected = List.copyOf(disconnectedProviders);
+ }
+ saveState.putInt(KEY_DISCONNECTED_COUNT, disconnected.size());
+ for (int i = 0; i < disconnected.size(); i++) {
+ P dp = disconnected.get(i);
+ String stateName = PREFIX_DISCONNECTED_PROVIDER + i;
+ SaveState dpState = new SaveState();
+ writer.accept(dp, dpState);
+ saveState.putXmlElement(stateName, dpState.saveToXml());
+ }
+ }
+
+ protected void doRead(SaveState saveState, BiConsumer super P, ? super SaveState> reader,
+ boolean matchCount) {
+ Element cpElement = saveState.getXmlElement(KEY_CONNECTED_PROVIDER);
+ if (cpElement != null) {
+ P cp = getConnectedProvider();
+ SaveState cpState = new SaveState(cpElement);
+ reader.accept(cp, cpState);
+ }
+
+ int disconnectedCount = saveState.getInt(KEY_DISCONNECTED_COUNT, 0);
+ List
disconnectedProviders = getDisconnectedProviders();
+ while (matchCount && disconnectedProviders.size() < disconnectedCount) {
+ createDisconnectedProvider();
+ }
+ while (matchCount && disconnectedProviders.size() > disconnectedCount) {
+ removeDisconnectedProvider(disconnectedProviders.get(disconnectedProviders.size() - 1));
+ }
+
+ int count = Math.min(disconnectedCount, disconnectedProviders.size());
+ for (int i = 0; i < count; i++) {
+ String stateName = PREFIX_DISCONNECTED_PROVIDER + i;
+ Element dpElement = saveState.getXmlElement(stateName);
+ if (dpElement != null) {
+ P dp = disconnectedProviders.get(i);
+ SaveState dpState = new SaveState(dpElement);
+ reader.accept(dp, dpState);
+ }
+ }
+ }
+
+ public void writeConfigState(SaveState saveState) {
+ doWrite(saveState, P::writeConfigState);
+ }
+
+ public void readConfigState(SaveState saveState) {
+ doRead(saveState, P::readConfigState, true);
+ }
+
+ public void writeDataState(SaveState saveState) {
+ doWrite(saveState, P::writeDataState);
+ }
+
+ public void readDataState(SaveState saveState) {
+ doRead(saveState, P::readDataState, false);
+ }
+}
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
new file mode 100644
index 0000000000..80d873db9c
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTableModel.java
@@ -0,0 +1,309 @@
+/* ###
+ * 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.model;
+
+import java.awt.Color;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.table.threaded.ThreadedTableModel;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.trace.database.DBTraceUtils;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.Trace.TraceObjectChangeType;
+import ghidra.trace.model.Trace.TraceSnapshotChangeType;
+import ghidra.trace.model.TraceDomainObjectListener;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.datastruct.Accumulator;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public abstract class AbstractQueryTableModel extends ThreadedTableModel
+ implements DisplaysModified {
+
+ protected class ListenerForChanges extends TraceDomainObjectListener {
+ public ListenerForChanges() {
+ listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueCreated);
+ listenFor(TraceObjectChangeType.VALUE_DELETED, this::valueDeleted);
+ listenFor(TraceObjectChangeType.VALUE_LIFESPAN_CHANGED, this::valueLifespanChanged);
+
+ listenFor(TraceSnapshotChangeType.ADDED, this::maxSnapChanged);
+ listenFor(TraceSnapshotChangeType.DELETED, this::maxSnapChanged);
+ }
+
+ protected void valueCreated(TraceObjectValue value) {
+ if (query != null && query.includes(span, value)) {
+ reload(); // Can I be more surgical?
+ }
+ }
+
+ protected void valueDeleted(TraceObjectValue value) {
+ if (query != null && query.includes(span, value)) {
+ reload();
+ }
+ }
+
+ protected void valueLifespanChanged(TraceObjectValue value, Range oldSpan,
+ Range newSpan) {
+ if (query == null) {
+ return;
+ }
+ boolean inOld = DBTraceUtils.intersect(oldSpan, span);
+ boolean inNew = DBTraceUtils.intersect(newSpan, span);
+ boolean queryIncludes = query.includes(Range.all(), value);
+ if (queryIncludes) {
+ if (inOld != inNew) {
+ reload();
+ }
+ else if (inOld || inNew) {
+ refresh();
+ }
+ }
+ }
+
+ protected void maxSnapChanged() {
+ AbstractQueryTableModel.this.maxSnapChanged();
+ }
+ }
+
+ protected class TableDisplaysObjectValues implements DisplaysObjectValues {
+ @Override
+ public long getSnap() {
+ return snap;
+ }
+ }
+
+ protected class DiffTableDisplaysObjectValues implements DisplaysObjectValues {
+ @Override
+ public long getSnap() {
+ return diffSnap;
+ }
+ }
+
+ private Trace trace;
+ private long snap;
+ private Trace diffTrace;
+ private long diffSnap;
+ private ModelQuery query;
+ private Range span = Range.all();
+ private boolean showHidden;
+
+ private final ListenerForChanges listenerForChanges = newListenerForChanges();
+ protected final DisplaysObjectValues display = new TableDisplaysObjectValues();
+ protected final DisplaysObjectValues diffDisplay = new DiffTableDisplaysObjectValues();
+
+ protected AbstractQueryTableModel(String name, Plugin plugin) {
+ super(name, plugin.getTool(), null, true);
+ }
+
+ protected ListenerForChanges newListenerForChanges() {
+ return new ListenerForChanges();
+ }
+
+ protected void maxSnapChanged() {
+ // Extension point
+ }
+
+ private void removeOldTraceListener() {
+ if (trace != null) {
+ trace.removeListener(listenerForChanges);
+ }
+ }
+
+ private void addNewTraceListener() {
+ if (trace != null) {
+ trace.addListener(listenerForChanges);
+ }
+ }
+
+ protected void traceChanged() {
+ reload();
+ }
+
+ public void setTrace(Trace trace) {
+ if (Objects.equals(this.trace, trace)) {
+ return;
+ }
+ removeOldTraceListener();
+ this.trace = trace;
+ addNewTraceListener();
+
+ traceChanged();
+ }
+
+ @Override
+ public Trace getTrace() {
+ return trace;
+ }
+
+ protected void snapChanged() {
+ refresh();
+ }
+
+ public void setSnap(long snap) {
+ if (this.snap == snap) {
+ return;
+ }
+ this.snap = snap;
+
+ snapChanged();
+ }
+
+ @Override
+ public long getSnap() {
+ return snap;
+ }
+
+ protected void diffTraceChanged() {
+ refresh();
+ }
+
+ /**
+ * Set alternative trace to colorize values that differ
+ *
+ *
+ * The same trace can be used, but with an alternative snap, if desired. See
+ * {@link #setDiffSnap(long)}. One common use is to compare with the previous snap of the same
+ * trace. Another common use is to compare with the previous navigation.
+ *
+ * @param diffTrace the alternative trace
+ */
+ public void setDiffTrace(Trace diffTrace) {
+ if (this.diffTrace == diffTrace) {
+ return;
+ }
+ this.diffTrace = diffTrace;
+ diffTraceChanged();
+ }
+
+ @Override
+ public Trace getDiffTrace() {
+ return diffTrace;
+ }
+
+ protected void diffSnapChanged() {
+ refresh();
+ }
+
+ /**
+ * Set alternative snap to colorize values that differ
+ *
+ *
+ * The diff trace must be set, even if it's the same as the trace being displayed. See
+ * {@link #setDiffTrace(Trace)}.
+ *
+ * @param diffSnap the alternative snap
+ */
+ public void setDiffSnap(long diffSnap) {
+ if (this.diffSnap == diffSnap) {
+ return;
+ }
+ this.diffSnap = diffSnap;
+ diffSnapChanged();
+ }
+
+ @Override
+ public long getDiffSnap() {
+ return diffSnap;
+ }
+
+ protected void queryChanged() {
+ reload();
+ }
+
+ public void setQuery(ModelQuery query) {
+ if (Objects.equals(this.query, query)) {
+ return;
+ }
+ this.query = query;
+
+ queryChanged();
+ }
+
+ public ModelQuery getQuery() {
+ return query;
+ }
+
+ protected void spanChanged() {
+ reload();
+ }
+
+ public void setSpan(Range span) {
+ if (Objects.equals(this.span, span)) {
+ return;
+ }
+ this.span = span;
+
+ spanChanged();
+ }
+
+ public Range getSpan() {
+ return span;
+ }
+
+ protected void showHiddenChanged() {
+ reload();
+ }
+
+ public void setShowHidden(boolean showHidden) {
+ if (this.showHidden == showHidden) {
+ return;
+ }
+ this.showHidden = showHidden;
+
+ showHiddenChanged();
+ }
+
+ public boolean isShowHidden() {
+ return showHidden;
+ }
+
+ protected abstract Stream streamRows(Trace trace, ModelQuery query, Range span);
+
+ @Override
+ protected void doLoad(Accumulator accumulator, TaskMonitor monitor)
+ throws CancelledException {
+ if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) {
+ return;
+ }
+ for (T t : (Iterable) streamRows(trace, query, span)::iterator) {
+ accumulator.add(t);
+ monitor.checkCanceled();
+ }
+ }
+
+ @Override
+ public Trace getDataSource() {
+ return trace;
+ }
+
+ @Override
+ public boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) {
+ if (DisplaysModified.super.isEdgesDiffer(newEdge, oldEdge)) {
+ return true;
+ }
+ // Hack to incorporate _display logic to differencing.
+ // This ensures "boxed" primitives show as differing at the object level
+ return !Objects.equals(diffDisplay.getEdgeDisplay(oldEdge),
+ display.getEdgeDisplay(newEdge));
+ }
+
+ public abstract void setDiffColor(Color diffColor);
+
+ public abstract void setDiffColorSel(Color diffColorSel);
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java
new file mode 100644
index 0000000000..b4bcb7d91b
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/AbstractQueryTablePanel.java
@@ -0,0 +1,176 @@
+/* ###
+ * 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.model;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseListener;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.event.ListSelectionListener;
+
+import com.google.common.collect.Range;
+
+import ghidra.app.plugin.core.debug.DebuggerCoordinates;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.util.table.GhidraTable;
+import ghidra.util.table.GhidraTableFilterPanel;
+
+public abstract class AbstractQueryTablePanel extends JPanel {
+
+ protected final AbstractQueryTableModel tableModel;
+ protected final GhidraTable table;
+ private final GhidraTableFilterPanel filterPanel;
+
+ protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
+ protected boolean limitToSnap = false;
+ protected boolean showHidden = false;
+
+ public AbstractQueryTablePanel(Plugin plugin) {
+ super(new BorderLayout());
+ tableModel = createModel(plugin);
+ table = new GhidraTable(tableModel);
+ filterPanel = new GhidraTableFilterPanel<>(table, tableModel);
+
+ add(new JScrollPane(table), BorderLayout.CENTER);
+ add(filterPanel, BorderLayout.SOUTH);
+ }
+
+ protected abstract AbstractQueryTableModel createModel(Plugin plugin);
+
+ public void goToCoordinates(DebuggerCoordinates coords) {
+ if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) {
+ return;
+ }
+ DebuggerCoordinates previous = current;
+ this.current = coords;
+ tableModel.setDiffTrace(previous.getTrace());
+ tableModel.setTrace(current.getTrace());
+ tableModel.setDiffSnap(previous.getSnap());
+ tableModel.setSnap(current.getSnap());
+ if (limitToSnap) {
+ tableModel.setSpan(Range.singleton(current.getSnap()));
+ }
+ }
+
+ public void reload() {
+ tableModel.reload();
+ }
+
+ public void setQuery(ModelQuery query) {
+ tableModel.setQuery(query);
+ }
+
+ public ModelQuery getQuery() {
+ return tableModel.getQuery();
+ }
+
+ public void setLimitToSnap(boolean limitToSnap) {
+ if (this.limitToSnap == limitToSnap) {
+ return;
+ }
+ this.limitToSnap = limitToSnap;
+ tableModel.setSpan(limitToSnap ? Range.singleton(current.getSnap()) : Range.all());
+ }
+
+ public boolean isLimitToSnap() {
+ return limitToSnap;
+ }
+
+ public void setShowHidden(boolean showHidden) {
+ if (this.showHidden == showHidden) {
+ return;
+ }
+ this.showHidden = showHidden;
+ tableModel.setShowHidden(showHidden);
+ }
+
+ public boolean isShowHidden() {
+ return showHidden;
+ }
+
+ public void addSelectionListener(ListSelectionListener listener) {
+ table.getSelectionModel().addListSelectionListener(listener);
+ }
+
+ public void removeSelectionListener(ListSelectionListener listener) {
+ table.getSelectionModel().removeListSelectionListener(listener);
+ }
+
+ @Override
+ public synchronized void addMouseListener(MouseListener l) {
+ super.addMouseListener(l);
+ // HACK?
+ table.addMouseListener(l);
+ }
+
+ @Override
+ public synchronized void removeMouseListener(MouseListener l) {
+ super.removeMouseListener(l);
+ // HACK?
+ table.removeMouseListener(l);
+ }
+
+ @Override
+ public synchronized void addKeyListener(KeyListener l) {
+ super.addKeyListener(l);
+ // HACK?
+ table.addKeyListener(l);
+ }
+
+ @Override
+ public synchronized void removeKeyListener(KeyListener l) {
+ super.removeKeyListener(l);
+ // HACK?
+ table.removeKeyListener(l);
+ }
+
+ public void setSelectionMode(int selectionMode) {
+ table.setSelectionMode(selectionMode);
+ }
+
+ public int getSelectionMode() {
+ return table.getSelectionModel().getSelectionMode();
+ }
+
+ // TODO: setSelectedItems? Is a bit more work than expected:
+ // see filterPanel.getTableFilterModel();
+ // see table.getSelectionMode().addSelectionInterval()
+ // seems like setSelectedItems should be in filterPanel?
+
+ public void setSelectedItem(T item) {
+ filterPanel.setSelectedItem(item);
+ }
+
+ public List getSelectedItems() {
+ return filterPanel.getSelectedItems();
+ }
+
+ public T getSelectedItem() {
+ return filterPanel.getSelectedItem();
+ }
+
+ public void setDiffColor(Color diffColor) {
+ tableModel.setDiffColor(diffColor);
+ }
+
+ public void setDiffColorSel(Color diffColorSel) {
+ tableModel.setDiffColorSel(diffColorSel);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java
new file mode 100644
index 0000000000..afa961ab86
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ColorsModified.java
@@ -0,0 +1,66 @@
+/* ###
+ * 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.model;
+
+import java.awt.Color;
+
+import javax.swing.*;
+import javax.swing.tree.TreeCellRenderer;
+
+public interface ColorsModified
{
+
+ Color getDiffForeground(P p);
+
+ Color getDiffSelForeground(P p);
+
+ Color getForeground(P p);
+
+ Color getSelForeground(P p);
+
+ default Color getForegroundFor(P p, boolean isModified, boolean isSelected) {
+ return isModified ? isSelected ? getDiffSelForeground(p) : getDiffForeground(p)
+ : isSelected ? getSelForeground(p) : getForeground(p);
+ }
+
+ interface InTable extends ColorsModified {
+ @Override
+ default Color getForeground(JTable table) {
+ return table.getForeground();
+ }
+
+ @Override
+ default Color getSelForeground(JTable table) {
+ return table.getSelectionForeground();
+ }
+ }
+
+ interface InTree extends ColorsModified, TreeCellRenderer {
+
+ Color getTextNonSelectionColor();
+
+ Color getTextSelectionColor();
+
+ @Override
+ default Color getForeground(JTree tree) {
+ return getTextNonSelectionColor();
+ }
+
+ @Override
+ default Color getSelForeground(JTree tree) {
+ return getTextSelectionColor();
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java
new file mode 100644
index 0000000000..bf65d7c6e0
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelPlugin.java
@@ -0,0 +1,157 @@
+/* ###
+ * 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.model;
+
+import java.util.*;
+
+import ghidra.app.plugin.PluginCategoryNames;
+import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
+import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
+import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
+import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior;
+import ghidra.app.services.DebuggerTraceManagerService;
+import ghidra.framework.options.SaveState;
+import ghidra.framework.plugintool.*;
+import ghidra.framework.plugintool.util.PluginStatus;
+import ghidra.trace.model.Trace;
+
+@PluginInfo(
+ shortDescription = "Debugger model browser",
+ description = "GUI to browse objects recorded to the trace",
+ category = PluginCategoryNames.DEBUGGER,
+ packageName = DebuggerPluginPackage.NAME,
+ status = PluginStatus.STABLE,
+ eventsConsumed = {
+ TraceActivatedPluginEvent.class,
+ TraceClosedPluginEvent.class,
+ },
+ servicesRequired = {
+ DebuggerTraceManagerService.class,
+ })
+public class DebuggerModelPlugin extends Plugin {
+
+ private final class ForModelMultiProviderSaveBehavior
+ extends MultiProviderSaveBehavior {
+ @Override
+ protected DebuggerModelProvider getConnectedProvider() {
+ return connectedProvider;
+ }
+
+ @Override
+ protected List getDisconnectedProviders() {
+ return disconnectedProviders;
+ }
+
+ @Override
+ protected DebuggerModelProvider createDisconnectedProvider() {
+ return DebuggerModelPlugin.this.createDisconnectedProvider();
+ }
+
+ @Override
+ protected void removeDisconnectedProvider(DebuggerModelProvider p) {
+ p.removeFromTool();
+ }
+ }
+
+ private DebuggerModelProvider connectedProvider;
+ private final List disconnectedProviders = new ArrayList<>();
+ private final ForModelMultiProviderSaveBehavior saveBehavior =
+ new ForModelMultiProviderSaveBehavior();
+
+ public DebuggerModelPlugin(PluginTool tool) {
+ super(tool);
+ }
+
+ @Override
+ protected void init() {
+ this.connectedProvider = newProvider(false);
+ super.init();
+ }
+
+ @Override
+ protected void dispose() {
+ tool.removeComponentProvider(connectedProvider);
+ super.dispose();
+ }
+
+ protected DebuggerModelProvider newProvider(boolean isClone) {
+ return new DebuggerModelProvider(this, isClone);
+ }
+
+ protected DebuggerModelProvider createDisconnectedProvider() {
+ DebuggerModelProvider p = newProvider(true);
+ synchronized (disconnectedProviders) {
+ disconnectedProviders.add(p);
+ }
+ return p;
+ }
+
+ public DebuggerModelProvider getConnectedProvider() {
+ return connectedProvider;
+ }
+
+ public List getDisconnectedProviders() {
+ return Collections.unmodifiableList(disconnectedProviders);
+ }
+
+ @Override
+ public void processEvent(PluginEvent event) {
+ super.processEvent(event);
+ if (event instanceof TraceActivatedPluginEvent) {
+ TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
+ connectedProvider.coordinatesActivated(ev.getActiveCoordinates());
+ }
+ if (event instanceof TraceClosedPluginEvent) {
+ TraceClosedPluginEvent ev = (TraceClosedPluginEvent) event;
+ traceClosed(ev.getTrace());
+ }
+ }
+
+ private void traceClosed(Trace trace) {
+ connectedProvider.traceClosed(trace);
+ synchronized (disconnectedProviders) {
+ for (DebuggerModelProvider p : disconnectedProviders) {
+ p.traceClosed(trace);
+ }
+ }
+ }
+
+ void providerRemoved(DebuggerModelProvider p) {
+ synchronized (disconnectedProviders) {
+ disconnectedProviders.remove(p);
+ }
+ }
+
+ @Override
+ public void writeConfigState(SaveState saveState) {
+ saveBehavior.writeConfigState(saveState);
+ }
+
+ @Override
+ public void readConfigState(SaveState saveState) {
+ saveBehavior.readConfigState(saveState);
+ }
+
+ @Override
+ public void writeDataState(SaveState saveState) {
+ saveBehavior.writeDataState(saveState);
+ }
+
+ @Override
+ public void readDataState(SaveState saveState) {
+ saveBehavior.readDataState(saveState);
+ }
+}
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
new file mode 100644
index 0000000000..85dab75099
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerModelProvider.java
@@ -0,0 +1,681 @@
+/* ###
+ * 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.model;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.swing.*;
+
+import docking.*;
+import docking.action.DockingAction;
+import docking.action.ToggleDockingAction;
+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.MultiProviderSaveBehavior.SaveableProvider;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ObjectRow;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.app.services.DebuggerTraceManagerService;
+import ghidra.framework.options.AutoOptions;
+import ghidra.framework.options.SaveState;
+import ghidra.framework.options.annotation.*;
+import ghidra.framework.plugintool.AutoConfigState;
+import ghidra.framework.plugintool.AutoService;
+import ghidra.framework.plugintool.annotation.AutoConfigStateField;
+import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.*;
+import ghidra.util.Msg;
+
+public class DebuggerModelProvider extends ComponentProvider implements SaveableProvider {
+
+ private static final AutoConfigState.ClassHandler CONFIG_STATE_HANDLER =
+ AutoConfigState.wireHandler(DebuggerModelProvider.class, MethodHandles.lookup());
+ private static final String KEY_DEBUGGER_COORDINATES = "DebuggerCoordinates";
+ private static final String KEY_PATH = "Path";
+
+ private final DebuggerModelPlugin plugin;
+ private final boolean isClone;
+
+ private JPanel mainPanel = new JPanel(new BorderLayout());
+
+ protected JTextField pathField;
+ protected JButton goButton;
+ protected ObjectsTreePanel objectsTreePanel;
+ protected ObjectsTablePanel elementsTablePanel;
+ protected PathsTablePanel attributesTablePanel;
+
+ /*testing*/ DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
+ /*testing*/ TraceObjectKeyPath path = TraceObjectKeyPath.of();
+
+ @AutoServiceConsumed
+ protected DebuggerTraceManagerService traceManager;
+ @SuppressWarnings("unused")
+ private final AutoService.Wiring autoServiceWiring;
+
+ @AutoOptionDefined(
+ description = "Text color for values that have just changed",
+ name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED,
+ help = @HelpInfo(anchor = "colors"))
+ private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED;
+
+ @AutoOptionDefined(
+ description = "Select text color for values that have just changed",
+ name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED_SEL,
+ help = @HelpInfo(anchor = "colors"))
+ private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL;
+
+ @SuppressWarnings("unused")
+ private final AutoOptions.Wiring autoOptionsWiring;
+
+ @AutoConfigStateField
+ private boolean limitToSnap = false;
+ @AutoConfigStateField
+ private boolean showHidden = false;
+ @AutoConfigStateField
+ private boolean showPrimitivesInTree = false;
+ @AutoConfigStateField
+ private boolean showMethodsInTree = false;
+
+ DockingAction actionCloneWindow;
+ ToggleDockingAction actionLimitToCurrentSnap;
+ ToggleDockingAction actionShowHidden;
+ ToggleDockingAction actionShowPrimitivesInTree;
+ ToggleDockingAction actionShowMethodsInTree;
+ DockingAction actionFollowLink;
+ // TODO: Remove stopgap
+ DockingAction actionStepBackward;
+ DockingAction actionStepForward;
+
+ DebuggerObjectActionContext myActionContext;
+
+ public DebuggerModelProvider(DebuggerModelPlugin plugin, boolean isClone) {
+ super(plugin.getTool(), DebuggerResources.TITLE_PROVIDER_MODEL, plugin.getName());
+ this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
+ this.autoOptionsWiring = AutoOptions.wireOptions(plugin, this);
+ this.plugin = plugin;
+ this.isClone = isClone;
+
+ setIcon(DebuggerResources.ICON_PROVIDER_MODEL);
+ setHelpLocation(DebuggerResources.HELP_PROVIDER_MODEL);
+ setWindowMenuGroup(DebuggerPluginPackage.NAME);
+
+ buildMainPanel();
+
+ setDefaultWindowPosition(WindowPosition.LEFT);
+ createActions();
+
+ if (isClone) {
+ setTitle("[" + DebuggerResources.TITLE_PROVIDER_MODEL + "]");
+ setWindowGroup("Debugger.Core.disconnected");
+ setIntraGroupPosition(WindowPosition.STACK);
+ mainPanel.setBorder(BorderFactory.createLineBorder(Color.ORANGE, 2));
+ setTransient();
+ }
+ else {
+ setTitle(DebuggerResources.TITLE_PROVIDER_MODEL);
+ setWindowGroup("Debugger.Core");
+ }
+
+ doSetLimitToCurrentSnap(limitToSnap);
+
+ setVisible(true);
+ contextChanged();
+ }
+
+ @Override
+ public void removeFromTool() {
+ plugin.providerRemoved(this);
+ super.removeFromTool();
+ }
+
+ protected void buildMainPanel() {
+ pathField = new JTextField();
+ pathField.setInputVerifier(new InputVerifier() {
+ @Override
+ public boolean verify(JComponent input) {
+ try {
+ setPath(TraceObjectKeyPath.parse(pathField.getText()), pathField);
+ return true;
+ }
+ catch (IllegalArgumentException e) {
+ plugin.getTool().setStatusInfo("Invalid Path: " + pathField.getText(), true);
+ return false;
+ }
+ }
+ });
+ goButton = new JButton("Go");
+ ActionListener gotoPath = evt -> {
+ try {
+ setPath(TraceObjectKeyPath.parse(pathField.getText()), pathField);
+ KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner();
+ }
+ catch (IllegalArgumentException e) {
+ Msg.showError(this, mainPanel, DebuggerResources.TITLE_PROVIDER_MODEL,
+ "Invalid Query: " + pathField.getText());
+ }
+ };
+ goButton.addActionListener(gotoPath);
+ pathField.addActionListener(gotoPath);
+ pathField.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+ pathField.setText(path.toString());
+ KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner();
+ }
+ }
+ });
+
+ objectsTreePanel = new ObjectsTreePanel();
+ elementsTablePanel = new ObjectsTablePanel(plugin);
+ attributesTablePanel = new PathsTablePanel(plugin);
+
+ JSplitPane lrSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+ lrSplit.setResizeWeight(0.2);
+ JSplitPane tbSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+ tbSplit.setResizeWeight(0.7);
+ lrSplit.setRightComponent(tbSplit);
+
+ JPanel queryPanel = new JPanel(new BorderLayout());
+
+ queryPanel.add(new JLabel("Path: "), BorderLayout.WEST);
+ queryPanel.add(pathField, BorderLayout.CENTER);
+ queryPanel.add(goButton, BorderLayout.EAST);
+
+ JPanel labeledElementsTablePanel = new JPanel(new BorderLayout());
+ labeledElementsTablePanel.add(elementsTablePanel);
+ labeledElementsTablePanel.add(new JLabel("Elements"), BorderLayout.NORTH);
+
+ JPanel labeledAttributesTablePanel = new JPanel(new BorderLayout());
+ labeledAttributesTablePanel.add(attributesTablePanel);
+ labeledAttributesTablePanel.add(new JLabel("Attributes"), BorderLayout.NORTH);
+
+ lrSplit.setLeftComponent(objectsTreePanel);
+ tbSplit.setLeftComponent(labeledElementsTablePanel);
+ tbSplit.setRightComponent(labeledAttributesTablePanel);
+
+ mainPanel.add(queryPanel, BorderLayout.NORTH);
+ mainPanel.add(lrSplit, BorderLayout.CENTER);
+
+ objectsTreePanel.addTreeSelectionListener(evt -> {
+ Trace trace = current.getTrace();
+ if (trace == null) {
+ return;
+ }
+ if (trace.getObjectManager().getRootObject() == null) {
+ return;
+ }
+ List sel = objectsTreePanel.getSelectedItems();
+ if (!sel.isEmpty()) {
+ myActionContext = new DebuggerObjectActionContext(sel.stream()
+ .map(n -> n.getValue())
+ .collect(Collectors.toList()),
+ this, objectsTreePanel);
+ }
+ else {
+ myActionContext = null;
+ }
+ contextChanged();
+
+ if (sel.size() != 1) {
+ // TODO: Multiple paths? PathMatcher can do it, just have to parse
+ // Just leave whatever was there.
+ return;
+ }
+ TraceObjectValue value = sel.get(0).getValue();
+ TraceObject parent = value.getParent();
+ TraceObjectKeyPath path;
+ if (parent == null) {
+ path = TraceObjectKeyPath.of();
+ }
+ else {
+ path = parent.getCanonicalPath().key(value.getEntryKey());
+ }
+ setPath(path, objectsTreePanel);
+ });
+ elementsTablePanel.addSelectionListener(evt -> {
+ if (evt.getValueIsAdjusting()) {
+ return;
+ }
+ List sel = elementsTablePanel.getSelectedItems();
+ if (!sel.isEmpty()) {
+ myActionContext = new DebuggerObjectActionContext(sel.stream()
+ .map(r -> r.getValue())
+ .collect(Collectors.toList()),
+ this, elementsTablePanel);
+ }
+ else {
+ myActionContext = null;
+ }
+ contextChanged();
+
+ if (sel.size() != 1) {
+ attributesTablePanel.setQuery(ModelQuery.attributesOf(path));
+ return;
+ }
+ TraceObjectValue value = sel.get(0).getValue();
+ if (!value.isObject()) {
+ return;
+ }
+ attributesTablePanel
+ .setQuery(ModelQuery.attributesOf(value.getChild().getCanonicalPath()));
+ });
+ attributesTablePanel.addSelectionListener(evt -> {
+ if (evt.getValueIsAdjusting()) {
+ return;
+ }
+ List sel = attributesTablePanel.getSelectedItems();
+ if (!sel.isEmpty()) {
+ myActionContext = new DebuggerObjectActionContext(sel.stream()
+ .map(r -> Objects.requireNonNull(r.getPath().getLastEntry()))
+ .collect(Collectors.toList()),
+ this, attributesTablePanel);
+ }
+ else {
+ myActionContext = null;
+ }
+ contextChanged();
+ });
+
+ elementsTablePanel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() != 2 || e.getButton() != MouseEvent.BUTTON1) {
+ return;
+ }
+ activatedElementsTable();
+ }
+ });
+ elementsTablePanel.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() != KeyEvent.VK_ENTER) {
+ return;
+ }
+ activatedElementsTable();
+ e.consume();
+ }
+ });
+ attributesTablePanel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() != 2 || e.getButton() != MouseEvent.BUTTON1) {
+ return;
+ }
+ activatedAttributesTable();
+ }
+ });
+ attributesTablePanel.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() != KeyEvent.VK_ENTER) {
+ return;
+ }
+ activatedAttributesTable();
+ e.consume();
+ }
+ });
+ }
+
+ @Override
+ public ActionContext getActionContext(MouseEvent event) {
+ if (myActionContext != null) {
+ return myActionContext;
+ }
+ return super.getActionContext(event);
+ }
+
+ protected void createActions() {
+ actionCloneWindow = CloneWindowAction.builder(plugin)
+ .enabledWhen(c -> current.getTrace() != null)
+ .onAction(c -> activatedCloneWindow())
+ .buildAndInstallLocal(this);
+ actionLimitToCurrentSnap = LimitToCurrentSnapAction.builder(plugin)
+ .onAction(this::toggledLimitToCurrentSnap)
+ .buildAndInstallLocal(this);
+ actionShowHidden = ShowHiddenAction.builder(plugin)
+ .onAction(this::toggledShowHidden)
+ .buildAndInstallLocal(this);
+ actionShowPrimitivesInTree = ShowPrimitivesInTreeAction.builder(plugin)
+ .onAction(this::toggledShowPrimitivesInTree)
+ .buildAndInstallLocal(this);
+ actionShowMethodsInTree = ShowMethodsInTreeAction.builder(plugin)
+ .onAction(this::toggledShowMethodsInTree)
+ .buildAndInstallLocal(this);
+ actionFollowLink = FollowLinkAction.builder(plugin)
+ .withContext(DebuggerObjectActionContext.class)
+ .enabledWhen(this::hasSingleLink)
+ .onAction(this::activatedFollowLink)
+ .buildAndInstallLocal(this);
+
+ // TODO: These are a stopgap until the plot column header provides nav
+ actionStepBackward = StepSnapBackwardAction.builder(plugin)
+ .enabledWhen(this::isStepBackwardEnabled)
+ .onAction(this::activatedStepBackward)
+ .buildAndInstallLocal(this);
+ actionStepForward = StepSnapForwardAction.builder(plugin)
+ .enabledWhen(this::isStepForwardEnabled)
+ .onAction(this::activatedStepForward)
+ .buildAndInstallLocal(this);
+ }
+
+ private void activatedElementsTable() {
+ ValueRow row = elementsTablePanel.getSelectedItem();
+ if (row == null) {
+ return;
+ }
+ if (!(row instanceof ObjectRow)) {
+ return;
+ }
+ ObjectRow objectRow = (ObjectRow) row;
+ setPath(objectRow.getTraceObject().getCanonicalPath());
+ }
+
+ private void activatedAttributesTable() {
+ PathRow row = attributesTablePanel.getSelectedItem();
+ if (row == null) {
+ return;
+ }
+ Object value = row.getValue();
+ if (!(value instanceof TraceObject)) {
+ return;
+ }
+ TraceObject object = (TraceObject) value;
+ setPath(object.getCanonicalPath());
+ }
+
+ private void activatedCloneWindow() {
+ DebuggerModelProvider clone = plugin.createDisconnectedProvider();
+ SaveState configState = new SaveState();
+ this.writeConfigState(configState);
+ clone.readConfigState(configState);
+ SaveState dataState = new SaveState();
+ this.writeDataState(dataState);
+ // coords are omitted by main window
+ // also, cannot save unless trace is in a project
+ clone.coordinatesActivated(current);
+ clone.readDataState(dataState);
+ plugin.getTool().showComponentProvider(clone, true);
+ }
+
+ private void toggledLimitToCurrentSnap(ActionContext ctx) {
+ setLimitToCurrentSnap(actionLimitToCurrentSnap.isSelected());
+ }
+
+ private void toggledShowHidden(ActionContext ctx) {
+ setShowHidden(actionShowHidden.isSelected());
+ }
+
+ private void toggledShowPrimitivesInTree(ActionContext ctx) {
+ setShowPrimitivesInTree(actionShowPrimitivesInTree.isSelected());
+ }
+
+ private void toggledShowMethodsInTree(ActionContext ctx) {
+ setShowMethodsInTree(actionShowMethodsInTree.isSelected());
+ }
+
+ private boolean hasSingleLink(DebuggerObjectActionContext ctx) {
+ List values = ctx.getObjectValues();
+ if (values.size() != 1) {
+ return false;
+ }
+ TraceObjectValue val = values.get(0);
+ if (val.isCanonical() || !val.isObject()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void activatedFollowLink(DebuggerObjectActionContext ctx) {
+ List values = ctx.getObjectValues();
+ if (values.size() != 1) {
+ return;
+ }
+ setPath(values.get(0).getChild().getCanonicalPath(), null);
+ }
+
+ private boolean isStepBackwardEnabled(ActionContext ignored) {
+ if (current.getTrace() == null) {
+ return false;
+ }
+ if (!current.getTime().isSnapOnly()) {
+ return true;
+ }
+ if (current.getSnap() <= 0) {
+ return false;
+ }
+ return true;
+ }
+
+ private void activatedStepBackward(ActionContext ignored) {
+ if (current.getTime().isSnapOnly()) {
+ traceManager.activateSnap(current.getSnap() - 1);
+ }
+ else {
+ traceManager.activateSnap(current.getSnap());
+ }
+ }
+
+ private boolean isStepForwardEnabled(ActionContext ignored) {
+ Trace curTrace = current.getTrace();
+ if (curTrace == null) {
+ return false;
+ }
+ Long maxSnap = curTrace.getTimeManager().getMaxSnap();
+ if (maxSnap == null || current.getSnap() >= maxSnap) {
+ return false;
+ }
+ return true;
+ }
+
+ private void activatedStepForward(ActionContext ignored) {
+ traceManager.activateSnap(current.getSnap() + 1);
+ }
+
+ @Override
+ public JComponent getComponent() {
+ return mainPanel;
+ }
+
+ public void coordinatesActivated(DebuggerCoordinates coords) {
+ this.current = coords;
+ objectsTreePanel.goToCoordinates(coords);
+ elementsTablePanel.goToCoordinates(coords);
+ attributesTablePanel.goToCoordinates(coords);
+
+ checkPath();
+ }
+
+ public void traceClosed(Trace trace) {
+ if (current.getTrace() == trace) {
+ coordinatesActivated(DebuggerCoordinates.NOWHERE);
+ }
+ }
+
+ protected void setPath(TraceObjectKeyPath path, JComponent source) {
+ if (Objects.equals(this.path, path)) {
+ return;
+ }
+ this.path = path;
+ if (source != pathField) {
+ pathField.setText(path.toString());
+ }
+ if (source != objectsTreePanel) {
+ selectInTree(path);
+ }
+ elementsTablePanel.setQuery(ModelQuery.elementsOf(path));
+ attributesTablePanel.setQuery(ModelQuery.attributesOf(path));
+
+ checkPath();
+ }
+
+ protected void checkPath() {
+ if (objectsTreePanel.getNode(path) == null) {
+ plugin.getTool().setStatusInfo("No such object at path " + path, true);
+ }
+ }
+
+ public void setPath(TraceObjectKeyPath path) {
+ setPath(path, null);
+ }
+
+ public TraceObjectKeyPath getPath() {
+ return path;
+ }
+
+ protected void doSetLimitToCurrentSnap(boolean limitToSnap) {
+ this.limitToSnap = limitToSnap;
+ actionLimitToCurrentSnap.setSelected(limitToSnap);
+ objectsTreePanel.setLimitToSnap(limitToSnap);
+ elementsTablePanel.setLimitToSnap(limitToSnap);
+ attributesTablePanel.setLimitToSnap(limitToSnap);
+ }
+
+ public void setLimitToCurrentSnap(boolean limitToSnap) {
+ if (this.limitToSnap == limitToSnap) {
+ return;
+ }
+ doSetLimitToCurrentSnap(limitToSnap);
+ }
+
+ public boolean isLimitToCurrentSnap() {
+ return limitToSnap;
+ }
+
+ protected void doSetShowHidden(boolean showHidden) {
+ this.showHidden = showHidden;
+ actionShowHidden.setSelected(showHidden);
+ objectsTreePanel.setShowHidden(showHidden);
+ elementsTablePanel.setShowHidden(showHidden);
+ attributesTablePanel.setShowHidden(showHidden);
+ }
+
+ public void setShowHidden(boolean showHidden) {
+ if (this.showHidden == showHidden) {
+ return;
+ }
+ doSetShowHidden(showHidden);
+ }
+
+ public boolean isShowHidden() {
+ return showHidden;
+ }
+
+ protected void doSetShowPrimitivesInTree(boolean showPrimitivesInTree) {
+ this.showPrimitivesInTree = showPrimitivesInTree;
+ actionShowPrimitivesInTree.setSelected(showPrimitivesInTree);
+ objectsTreePanel.setShowPrimitives(showPrimitivesInTree);
+ }
+
+ public void setShowPrimitivesInTree(boolean showPrimitivesInTree) {
+ if (this.showPrimitivesInTree == showPrimitivesInTree) {
+ return;
+ }
+ doSetShowPrimitivesInTree(showPrimitivesInTree);
+ }
+
+ public boolean isShowPrimitivesInTree() {
+ return showPrimitivesInTree;
+ }
+
+ protected void doSetShowMethodsInTree(boolean showMethodsInTree) {
+ this.showMethodsInTree = showMethodsInTree;
+ actionShowMethodsInTree.setSelected(showMethodsInTree);
+ objectsTreePanel.setShowMethods(showMethodsInTree);
+ }
+
+ public void setShowMethodsInTree(boolean showMethodsInTree) {
+ if (this.showMethodsInTree == showMethodsInTree) {
+ return;
+ }
+ doSetShowMethodsInTree(showMethodsInTree);
+ }
+
+ public boolean isShowMethodsInTree() {
+ return showMethodsInTree;
+ }
+
+ @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED)
+ public void setDiffColor(Color diffColor) {
+ if (Objects.equals(this.diffColor, diffColor)) {
+ return;
+ }
+ this.diffColor = diffColor;
+ objectsTreePanel.setDiffColor(diffColor);
+ elementsTablePanel.setDiffColor(diffColor);
+ attributesTablePanel.setDiffColor(diffColor);
+ }
+
+ @AutoOptionConsumed(name = DebuggerResources.OPTION_NAME_COLORS_VALUE_CHANGED_SEL)
+ public void setDiffColorSel(Color diffColorSel) {
+ if (Objects.equals(this.diffColorSel, diffColorSel)) {
+ return;
+ }
+ this.diffColorSel = diffColorSel;
+ objectsTreePanel.setDiffColorSel(diffColorSel);
+ elementsTablePanel.setDiffColorSel(diffColorSel);
+ attributesTablePanel.setDiffColorSel(diffColorSel);
+ }
+
+ protected void selectInTree(TraceObjectKeyPath path) {
+ objectsTreePanel.setSelectedKeyPaths(List.of(path));
+ }
+
+ @Override
+ public void writeConfigState(SaveState saveState) {
+ CONFIG_STATE_HANDLER.writeConfigState(this, saveState);
+ }
+
+ @Override
+ public void readConfigState(SaveState saveState) {
+ CONFIG_STATE_HANDLER.readConfigState(this, saveState);
+ doSetLimitToCurrentSnap(limitToSnap);
+ doSetShowHidden(showHidden);
+ doSetShowPrimitivesInTree(showPrimitivesInTree);
+ doSetShowMethodsInTree(showMethodsInTree);
+ }
+
+ @Override
+ public void writeDataState(SaveState saveState) {
+ if (isClone) {
+ current.writeDataState(plugin.getTool(), saveState, KEY_DEBUGGER_COORDINATES);
+ }
+ saveState.putString(KEY_PATH, path.toString());
+ // TODO?
+ //GTreeState treeState = objectsTreePanel.tree.getTreeState();
+ }
+
+ @Override
+ public void readDataState(SaveState saveState) {
+ if (isClone) {
+ DebuggerCoordinates coords = DebuggerCoordinates.readDataState(plugin.getTool(),
+ saveState, KEY_DEBUGGER_COORDINATES, true);
+ if (coords != DebuggerCoordinates.NOWHERE) {
+ coordinatesActivated(coords);
+ }
+ }
+ setPath(TraceObjectKeyPath.parse(saveState.getString(KEY_PATH, "")));
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java
new file mode 100644
index 0000000000..05afd6d10b
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DebuggerObjectActionContext.java
@@ -0,0 +1,38 @@
+/* ###
+ * 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.model;
+
+import java.awt.Component;
+import java.util.Collection;
+import java.util.List;
+
+import docking.ActionContext;
+import docking.ComponentProvider;
+import ghidra.trace.model.target.TraceObjectValue;
+
+public class DebuggerObjectActionContext extends ActionContext {
+ private final List objectValues;
+
+ public DebuggerObjectActionContext(Collection objectValues,
+ ComponentProvider provider, Component sourceComponent) {
+ super(provider, sourceComponent);
+ this.objectValues = List.copyOf(objectValues);
+ }
+
+ public List getObjectValues() {
+ return objectValues;
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java
new file mode 100644
index 0000000000..29c0d01366
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysModified.java
@@ -0,0 +1,151 @@
+/* ###
+ * 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.model;
+
+import java.util.Objects;
+
+import com.google.common.collect.Range;
+
+import ghidra.dbg.util.PathPredicates;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+
+public interface DisplaysModified {
+ /**
+ * Get the current trace
+ *
+ * @return the trace
+ */
+ Trace getTrace();
+
+ /**
+ * Get the current snap
+ *
+ * @return the snap
+ */
+ long getSnap();
+
+ /**
+ * Get the trace for comparison, which may be the same as the current trace
+ *
+ * @return the trace, or null to disable comparison
+ */
+ Trace getDiffTrace();
+
+ /**
+ * Get the snap for comparison
+ *
+ * @return the snap
+ */
+ long getDiffSnap();
+
+ /**
+ * Determine whether two objects differ
+ *
+ *
+ * By default the objects are considered equal if their canonical paths agree, without regard to
+ * the source trace or child values. To compare child values would likely recurse all the way to
+ * the leaves, which is costly and not exactly informative. This method should only be called
+ * for objects at the same path, meaning the two objects have at least one path in common. If
+ * this path is the canonical path, then the two objects (by default) cannot differ. This will
+ * detect changes in object links, though.
+ *
+ * @param newObject the current object
+ * @param oldObject the previous object
+ * @return true if the objects differ, i.e., should be displayed in red
+ */
+ default boolean isObjectsDiffer(TraceObject newObject, TraceObject oldObject) {
+ if (newObject == oldObject) {
+ return false;
+ }
+ return !Objects.equals(newObject.getCanonicalPath(), oldObject.getCanonicalPath());
+ }
+
+ /**
+ * Determine whether two values differ
+ *
+ *
+ * By default this defers to the values' Object{@link #equals(Object)} methods, or in case both
+ * are of type {@link TraceObject}, to {@link #isObjectsDiffer(TraceObject, TraceObject)}. This
+ * method should only be called for values at the same path.
+ *
+ * @param newValue the current value
+ * @param oldValue the previous value
+ * @return true if the values differ, i.e., should be displayed in red
+ */
+ default boolean isValuesDiffer(Object newValue, Object oldValue) {
+ if (newValue instanceof TraceObject && oldValue instanceof TraceObject) {
+ return isObjectsDiffer((TraceObject) newValue, (TraceObject) oldValue);
+ }
+ return !Objects.equals(newValue, oldValue);
+ }
+
+ /**
+ * Determine whether two object values (edges) differ
+ *
+ *
+ * By default, this behaves as in {@link Objects#equals(Object)}, deferring to
+ * {@link #isValuesDiffer(Object, Object)}. Note that newEdge can be null because span may
+ * include more than the current snap. It will be null for edges that are displayed but do not
+ * contains the current snap.
+ *
+ * @param newEdge the current edge, possibly null
+ * @param oldEdge the previous edge, possibly null
+ * @return true if the edges' values differ
+ */
+ default boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) {
+ if (newEdge == oldEdge) { // Covers case where both are null
+ return false;
+ }
+ if (newEdge == null || oldEdge == null) {
+ return true;
+ }
+ return isValuesDiffer(newEdge.getValue(), oldEdge.getValue());
+ }
+
+ default boolean isValueModified(TraceObjectValue value) {
+ if (value == null || value.getParent() == null) {
+ return false;
+ }
+ Trace diffTrace = getDiffTrace();
+ if (diffTrace == null) {
+ return false;
+ }
+ Trace trace = getTrace();
+ long snap = getSnap();
+ long diffSnap = getDiffSnap();
+ if (diffTrace == trace && diffSnap == snap) {
+ return false;
+ }
+ if (diffTrace == trace) {
+ boolean newContains = value.getLifespan().contains(snap);
+ boolean oldContains = value.getLifespan().contains(diffSnap);
+ if (newContains == oldContains) {
+ return newContains ? isEdgesDiffer(value, value) : true;
+ }
+ TraceObjectValue diffEdge = value.getParent().getValue(diffSnap, value.getEntryKey());
+ return isEdgesDiffer(newContains ? value : null, diffEdge);
+ }
+ TraceObjectValue diffEdge = diffTrace.getObjectManager()
+ .getValuePaths(Range.singleton(diffSnap),
+ PathPredicates.pattern(value.getCanonicalPath().getKeyList()))
+ .findAny()
+ .map(p -> p.getLastEntry())
+ .orElse(null);
+ return isEdgesDiffer(value, diffEdge);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.java
new file mode 100644
index 0000000000..523c380897
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/DisplaysObjectValues.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 ghidra.app.plugin.core.debug.gui.model;
+
+import ghidra.dbg.target.TargetObject;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.HTMLUtilities;
+
+public interface DisplaysObjectValues {
+ long getSnap();
+
+ default String getNullDisplay() {
+ return "";
+ }
+
+ default String getPrimitiveValueDisplay(Object value) {
+ assert !(value instanceof TraceObject);
+ assert !(value instanceof TraceObjectValue);
+ // TODO: Choose decimal or hex for integral types?
+ if (value == null) {
+ return getNullDisplay();
+ }
+ return value.toString();
+ }
+
+ default String getPrimitiveEdgeType(TraceObjectValue edge) {
+ return edge.getTargetSchema().getName() + ":" + edge.getValue().getClass().getSimpleName();
+ }
+
+ default String getPrimitiveEdgeToolTip(TraceObjectValue edge) {
+ return getPrimitiveValueDisplay(edge.getValue()) + " (" + getPrimitiveEdgeType(edge) + ")";
+ }
+
+ default String getObjectLinkDisplay(TraceObjectValue edge) {
+ return getObjectDisplay(edge);
+ }
+
+ default String getObjectType(TraceObjectValue edge) {
+ TraceObject object = edge.getChild();
+ return object.getTargetSchema().getName().toString();
+ }
+
+ default String getObjectLinkToolTip(TraceObjectValue edge) {
+ return "Link to " + getObjectToolTip(edge);
+ }
+
+ default String getRawObjectDisplay(TraceObjectValue edge) {
+ TraceObject object = edge.getChild();
+ if (object.isRoot()) {
+ return "";
+ }
+ return object.getCanonicalPath().toString();
+ }
+
+ default String getObjectDisplay(TraceObjectValue edge) {
+ TraceObject object = edge.getChild();
+ TraceObjectValue displayAttr =
+ object.getAttribute(getSnap(), TargetObject.DISPLAY_ATTRIBUTE_NAME);
+ if (displayAttr != null) {
+ return displayAttr.getValue().toString();
+ }
+ return getRawObjectDisplay(edge);
+ }
+
+ default String getObjectToolTip(TraceObjectValue edge) {
+ String display = getObjectDisplay(edge);
+ String raw = getRawObjectDisplay(edge);
+ if (display.equals(raw)) {
+ return display + " (" + getObjectType(edge) + ")";
+ }
+ return display + " (" + getObjectType(edge) + ":" + raw + ")";
+ }
+
+ default String getEdgeDisplay(TraceObjectValue edge) {
+ if (edge == null) {
+ return "";
+ }
+ if (edge.isCanonical()) {
+ return getObjectDisplay(edge);
+ }
+ if (edge.isObject()) {
+ return getObjectLinkDisplay(edge);
+ }
+ return getPrimitiveValueDisplay(edge.getValue());
+ }
+
+ /**
+ * Get an HTML string representing how the edge's value should be displayed
+ *
+ * @return the display string
+ */
+ default String getEdgeHtmlDisplay(TraceObjectValue edge) {
+ if (edge == null) {
+ return "";
+ }
+ if (!edge.isObject()) {
+ return "" + HTMLUtilities.escapeHTML(getPrimitiveValueDisplay(edge.getValue()));
+ }
+ if (edge.isCanonical()) {
+ return "" + HTMLUtilities.escapeHTML(getObjectDisplay(edge));
+ }
+ return "" + HTMLUtilities.escapeHTML(getObjectLinkDisplay(edge)) + "";
+ }
+
+ default String getEdgeToolTip(TraceObjectValue edge) {
+ if (edge == null) {
+ return null;
+ }
+ if (edge.isCanonical()) {
+ return getObjectToolTip(edge);
+ }
+ if (edge.isObject()) {
+ return getObjectLinkToolTip(edge);
+ }
+ return getPrimitiveEdgeToolTip(edge);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java
new file mode 100644
index 0000000000..6a4f5c9ec4
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ModelQuery.java
@@ -0,0 +1,165 @@
+/* ###
+ * 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.model;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import com.google.common.collect.Range;
+
+import ghidra.dbg.target.schema.TargetObjectSchema;
+import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
+import ghidra.dbg.util.*;
+import ghidra.trace.database.DBTraceUtils;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.*;
+
+public class ModelQuery {
+ // TODO: A more capable query language, e.g., with WHERE clauses.
+ // Could also want math expressions for the conditionals... Hmm.
+ // They need to be user enterable, so just a Java API won't suffice.
+
+ public static ModelQuery parse(String queryString) {
+ return new ModelQuery(PathPredicates.parse(queryString));
+ }
+
+ public static ModelQuery elementsOf(TraceObjectKeyPath path) {
+ return new ModelQuery(new PathPattern(PathUtils.extend(path.getKeyList(), "[]")));
+ }
+
+ public static ModelQuery attributesOf(TraceObjectKeyPath path) {
+ return new ModelQuery(new PathPattern(PathUtils.extend(path.getKeyList(), "")));
+ }
+
+ private final PathPredicates predicates;
+
+ /**
+ * TODO: This should probably be more capable, but for now, just support simple path patterns
+ *
+ * @param predicates the patterns
+ */
+ public ModelQuery(PathPredicates predicates) {
+ this.predicates = predicates;
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof ModelQuery)) {
+ return false;
+ }
+ ModelQuery that = (ModelQuery) obj;
+ if (!Objects.equals(this.predicates, that.predicates)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Render the query as a string as in {@link #parse(String)}
+ *
+ * @return the string
+ */
+ public String toQueryString() {
+ return predicates.getSingletonPattern().toPatternString();
+ }
+
+ /**
+ * Execute the query
+ *
+ * @param trace the data source
+ * @param span the span of snapshots to search, usually all or a singleton
+ * @return the stream of resulting objects
+ */
+ public Stream streamObjects(Trace trace, Range span) {
+ TraceObjectManager objects = trace.getObjectManager();
+ TraceObject root = objects.getRootObject();
+ return objects.getValuePaths(span, predicates)
+ .map(p -> p.getDestinationValue(root))
+ .filter(v -> v instanceof TraceObject)
+ .map(v -> (TraceObject) v);
+ }
+
+ public Stream streamValues(Trace trace, Range span) {
+ TraceObjectManager objects = trace.getObjectManager();
+ return objects.getValuePaths(span, predicates).map(p -> {
+ TraceObjectValue last = p.getLastEntry();
+ return last == null ? objects.getRootObject().getCanonicalParent(0) : last;
+ });
+ }
+
+ public Stream streamPaths(Trace trace, Range span) {
+ return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p);
+ }
+
+ /**
+ * Compute the named attributes for resulting objects, according to the schema
+ *
+ *
+ * This does not include the "default attribute schema."
+ *
+ * @param trace the data source
+ * @return the list of attributes
+ */
+ public Stream computeAttributes(Trace trace) {
+ TraceObjectManager objects = trace.getObjectManager();
+ TargetObjectSchema schema =
+ objects.getRootSchema().getSuccessorSchema(predicates.getSingletonPattern().asPath());
+ return schema.getAttributeSchemas()
+ .values()
+ .stream()
+ .filter(as -> !"".equals(as.getName()));
+ }
+
+ /**
+ * Determine whether this query would include the given value in its result
+ *
+ *
+ * More precisely, determine whether it would traverse the given value, accept it, and include
+ * its child in the result. It's possible the child could be included via another value, but
+ * this only considers the given value.
+ *
+ * @param span the span to consider
+ * @param value the value to examine
+ * @return true if the value would be accepted
+ */
+ public boolean includes(Range span, TraceObjectValue value) {
+ List path = predicates.getSingletonPattern().asPath();
+ if (path.isEmpty()) {
+ return value.getParent() == null;
+ }
+ if (!PathPredicates.keyMatches(PathUtils.getKey(path), value.getEntryKey())) {
+ return false;
+ }
+ if (!DBTraceUtils.intersect(span, value.getLifespan())) {
+ return false;
+ }
+ TraceObject parent = value.getParent();
+ if (parent == null) {
+ return false;
+ }
+ return parent.getAncestors(span, predicates.removeRight(1))
+ .anyMatch(v -> v.getSource(parent).isRoot());
+ }
+}
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
new file mode 100644
index 0000000000..157bddde06
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTableModel.java
@@ -0,0 +1,412 @@
+/* ###
+ * 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.model;
+
+import java.awt.Color;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.google.common.collect.*;
+
+import docking.widgets.table.DynamicTableColumn;
+import docking.widgets.table.TableColumnDescriptor;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.app.plugin.core.debug.gui.model.columns.*;
+import ghidra.dbg.target.schema.SchemaContext;
+import ghidra.dbg.target.schema.TargetObjectSchema;
+import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.HTMLUtilities;
+
+public class ObjectTableModel extends AbstractQueryTableModel {
+ /** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
+ private TraceValueValColumn valueColumn;
+ private TraceValueLifePlotColumn lifePlotColumn;
+
+ protected static Stream extends TraceObjectValue> distinctCanonical(
+ Stream extends TraceObjectValue> stream) {
+ Set seen = new HashSet<>();
+ return stream.filter(value -> {
+ if (!value.isCanonical()) {
+ return true;
+ }
+ return seen.add(value.getChild());
+ });
+ }
+
+ public interface ValueRow {
+ String getKey();
+
+ RangeSet getLife();
+
+ TraceObjectValue getValue();
+
+ /**
+ * Get a non-HTML string representing how this row's value should be sorted, filtered, etc.
+ *
+ * @return the display string
+ */
+ String getDisplay();
+
+ /**
+ * Get an HTML string representing how this row's value should be displayed
+ *
+ * @return the display string
+ */
+ String getHtmlDisplay();
+
+ String getToolTip();
+
+ /**
+ * Determine whether the value in the row has changed since the diff coordinates
+ *
+ * @return true if they differ, i.e., should be rendered in red
+ */
+ boolean isModified();
+
+ TraceObjectValue getAttribute(String attributeName);
+
+ String getAttributeDisplay(String attributeName);
+
+ String getAttributeHtmlDisplay(String attributeName);
+
+ String getAttributeToolTip(String attributeName);
+
+ boolean isAttributeModified(String attributeName);
+
+ }
+
+ protected abstract class AbstractValueRow implements ValueRow {
+ protected final TraceObjectValue value;
+
+ public AbstractValueRow(TraceObjectValue value) {
+ this.value = value;
+ }
+
+ @Override
+ public TraceObjectValue getValue() {
+ return value;
+ }
+
+ @Override
+ public String getKey() {
+ return value.getEntryKey();
+ }
+
+ @Override
+ public RangeSet getLife() {
+ RangeSet life = TreeRangeSet.create();
+ life.add(value.getLifespan());
+ return life;
+ }
+
+ @Override
+ public boolean isModified() {
+ return isValueModified(getValue());
+ }
+ }
+
+ protected class PrimitiveRow extends AbstractValueRow {
+ public PrimitiveRow(TraceObjectValue value) {
+ super(value);
+ }
+
+ @Override
+ public String getDisplay() {
+ return display.getPrimitiveValueDisplay(value.getValue());
+ }
+
+ @Override
+ public String getHtmlDisplay() {
+ return "" +
+ HTMLUtilities.escapeHTML(display.getPrimitiveValueDisplay(value.getValue()));
+ }
+
+ @Override
+ public String getToolTip() {
+ return display.getPrimitiveEdgeToolTip(value);
+ }
+
+ @Override
+ public TraceObjectValue getAttribute(String attributeName) {
+ return null;
+ }
+
+ @Override
+ public String getAttributeDisplay(String attributeName) {
+ return null;
+ }
+
+ @Override
+ public String getAttributeHtmlDisplay(String attributeName) {
+ return null;
+ }
+
+ @Override
+ public String getAttributeToolTip(String attributeName) {
+ return null;
+ }
+
+ @Override
+ public boolean isAttributeModified(String attributeName) {
+ return false;
+ }
+ }
+
+ protected class ObjectRow extends AbstractValueRow {
+ private final TraceObject object;
+
+ public ObjectRow(TraceObjectValue value) {
+ super(value);
+ this.object = value.getChild();
+ }
+
+ public TraceObject getTraceObject() {
+ return object;
+ }
+
+ @Override
+ public String getDisplay() {
+ return display.getEdgeDisplay(value);
+ }
+
+ @Override
+ public String getHtmlDisplay() {
+ return display.getEdgeHtmlDisplay(value);
+ }
+
+ @Override
+ public String getToolTip() {
+ return display.getEdgeToolTip(value);
+ }
+
+ @Override
+ public TraceObjectValue getAttribute(String attributeName) {
+ return object.getAttribute(getSnap(), attributeName);
+ }
+
+ @Override
+ public String getAttributeDisplay(String attributeName) {
+ return display.getEdgeDisplay(getAttribute(attributeName));
+ }
+
+ @Override
+ public String getAttributeHtmlDisplay(String attributeName) {
+ return display.getEdgeHtmlDisplay(getAttribute(attributeName));
+ }
+
+ @Override
+ public String getAttributeToolTip(String attributeName) {
+ return display.getEdgeToolTip(getAttribute(attributeName));
+ }
+
+ @Override
+ public boolean isAttributeModified(String attributeName) {
+ return isValueModified(getAttribute(attributeName));
+ }
+ }
+
+ protected ValueRow rowForValue(TraceObjectValue value) {
+ if (value.getValue() instanceof TraceObject) {
+ return new ObjectRow(value);
+ }
+ return new PrimitiveRow(value);
+ }
+
+ protected static class ColKey {
+ public static ColKey fromSchema(SchemaContext ctx, AttributeSchema attributeSchema) {
+ String name = attributeSchema.getName();
+ Class> type = TraceValueObjectAttributeColumn.computeColumnType(ctx, attributeSchema);
+ return new ColKey(name, type);
+ }
+
+ private final String name;
+ private final Class> type;
+ private final int hash;
+
+ public ColKey(String name, Class> type) {
+ this.name = name;
+ this.type = type;
+ this.hash = Objects.hash(name, type);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof ColKey)) {
+ return false;
+ }
+ ColKey that = (ColKey) obj;
+ if (!Objects.equals(this.name, that.name)) {
+ return false;
+ }
+ if (this.type != that.type) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return hash;
+ }
+ }
+
+ // TODO: Save and restore these between sessions, esp., their settings
+ private Map columnCache = new HashMap<>();
+
+ protected ObjectTableModel(Plugin plugin) {
+ super("Object Model", plugin);
+ }
+
+ @Override
+ protected void traceChanged() {
+ reloadAttributeColumns();
+ updateTimelineMax();
+ super.traceChanged();
+ }
+
+ @Override
+ protected void queryChanged() {
+ reloadAttributeColumns();
+ super.queryChanged();
+ }
+
+ @Override
+ protected void showHiddenChanged() {
+ reloadAttributeColumns();
+ super.showHiddenChanged();
+ }
+
+ @Override
+ protected void maxSnapChanged() {
+ updateTimelineMax();
+ refresh();
+ }
+
+ protected void updateTimelineMax() {
+ Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
+ Range fullRange = Range.closed(0L, max == null ? 1 : max + 1);
+ lifePlotColumn.setFullRange(fullRange);
+ }
+
+ protected List computeAttributeSchemas() {
+ Trace trace = getTrace();
+ ModelQuery query = getQuery();
+ if (trace == null || query == null) {
+ return List.of();
+ }
+ TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
+ if (rootSchema == null) {
+ return List.of();
+ }
+ SchemaContext ctx = rootSchema.getContext();
+ return query.computeAttributes(trace)
+ .filter(a -> isShowHidden() || !a.isHidden())
+ .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer())
+ .collect(Collectors.toList());
+ }
+
+ protected void reloadAttributeColumns() {
+ List attributes;
+ Trace trace = getTrace();
+ ModelQuery query = getQuery();
+ if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) {
+ attributes = List.of();
+ }
+ else {
+ SchemaContext ctx = trace.getObjectManager().getRootSchema().getContext();
+ attributes = query.computeAttributes(trace)
+ .filter(a -> isShowHidden() || !a.isHidden())
+ .filter(a -> !ctx.getSchema(a.getSchema()).isCanonicalContainer())
+ .collect(Collectors.toList());
+ }
+ resyncAttributeColumns(attributes);
+ }
+
+ protected Set> computeAttributeColumns(
+ Collection attributes) {
+ Trace trace = getTrace();
+ if (trace == null) {
+ return Set.of();
+ }
+ TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
+ if (rootSchema == null) {
+ return Set.of();
+ }
+ SchemaContext ctx = rootSchema.getContext();
+ return attributes.stream()
+ .map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as),
+ ck -> TraceValueObjectAttributeColumn.fromSchema(ctx, as)))
+ .collect(Collectors.toSet());
+ }
+
+ protected void resyncAttributeColumns(Collection attributes) {
+ Set> columns =
+ new HashSet<>(computeAttributeColumns(attributes));
+ Set> toRemove = new HashSet<>();
+ for (int i = 0; i < getColumnCount(); i++) {
+ DynamicTableColumn exists = getColumn(i);
+ if (!(exists instanceof TraceValueObjectAttributeColumn)) {
+ continue;
+ }
+ if (!columns.remove(exists)) {
+ toRemove.add(exists);
+ }
+ }
+ removeTableColumns(toRemove);
+ addTableColumns(columns);
+ }
+
+ @Override
+ protected Stream streamRows(Trace trace, ModelQuery query, Range span) {
+ return distinctCanonical(query.streamValues(trace, span)
+ .filter(v -> isShowHidden() || !v.isHidden()))
+ .map(this::rowForValue);
+ }
+
+ @Override
+ protected TableColumnDescriptor createTableColumnDescriptor() {
+ TableColumnDescriptor descriptor = new TableColumnDescriptor<>();
+ descriptor.addVisibleColumn(new TraceValueKeyColumn());
+ descriptor.addVisibleColumn(valueColumn = new TraceValueValColumn());
+ descriptor.addVisibleColumn(new TraceValueLifeColumn());
+ descriptor.addHiddenColumn(lifePlotColumn = new TraceValueLifePlotColumn());
+ return descriptor;
+ }
+
+ @Override
+ public void setDiffColor(Color diffColor) {
+ valueColumn.setDiffColor(diffColor);
+ for (TraceValueObjectAttributeColumn column : columnCache.values()) {
+ column.setDiffColor(diffColor);
+ }
+ }
+
+ @Override
+ public void setDiffColorSel(Color diffColorSel) {
+ valueColumn.setDiffColorSel(diffColorSel);
+ for (TraceValueObjectAttributeColumn column : columnCache.values()) {
+ column.setDiffColorSel(diffColorSel);
+ }
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java
new file mode 100644
index 0000000000..31cd0c4034
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectTreeModel.java
@@ -0,0 +1,777 @@
+/* ###
+ * 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.model;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import javax.swing.Icon;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.tree.GTreeLazyNode;
+import docking.widgets.tree.GTreeNode;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.dbg.target.*;
+import ghidra.trace.database.DBTraceUtils;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.Trace.TraceObjectChangeType;
+import ghidra.trace.model.TraceDomainObjectListener;
+import ghidra.trace.model.target.*;
+import ghidra.util.HTMLUtilities;
+import ghidra.util.datastruct.WeakValueHashMap;
+import utilities.util.IDKeyed;
+
+public class ObjectTreeModel implements DisplaysModified {
+
+ class ListenerForChanges extends TraceDomainObjectListener {
+ public ListenerForChanges() {
+ listenFor(TraceObjectChangeType.CREATED, this::objectCreated);
+ listenFor(TraceObjectChangeType.VALUE_CREATED, this::valueCreated);
+ listenFor(TraceObjectChangeType.VALUE_DELETED, this::valueDeleted);
+ listenFor(TraceObjectChangeType.VALUE_LIFESPAN_CHANGED, this::valueLifespanChanged);
+ }
+
+ protected boolean isEventValue(TraceObjectValue value) {
+ if (!value.getParent()
+ .getTargetSchema()
+ .getInterfaces()
+ .contains(TargetEventScope.class)) {
+ return false;
+ }
+ if (!TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME.equals(value.getEntryKey())) {
+ return false;
+ }
+ return true;
+ }
+
+ protected boolean isEnabledValue(TraceObjectValue value) {
+ Set> interfaces =
+ value.getParent().getTargetSchema().getInterfaces();
+ if (!interfaces.contains(TargetBreakpointSpec.class) &&
+ !interfaces.contains(TargetBreakpointLocation.class)) {
+ return false;
+ }
+ if (!TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME.equals(value.getEntryKey())) {
+ return false;
+ }
+ return true;
+ }
+
+ private void objectCreated(TraceObject object) {
+ if (object.isRoot()) {
+ reload();
+ }
+ }
+
+ private void valueCreated(TraceObjectValue value) {
+ if (!DBTraceUtils.intersect(value.getLifespan(), span)) {
+ return;
+ }
+ AbstractNode node = nodeCache.getByObject(value.getParent());
+ if (node == null) {
+ return;
+ }
+ if (isEventValue(value)) {
+ refresh();
+ }
+ if (isEnabledValue(value)) {
+ node.fireNodeChanged();
+ }
+ node.childCreated(value);
+ }
+
+ private void valueDeleted(TraceObjectValue value) {
+ if (!DBTraceUtils.intersect(value.getLifespan(), span)) {
+ return;
+ }
+ AbstractNode node = nodeCache.getByObject(value.getParent());
+ if (node == null) {
+ return;
+ }
+ if (isEventValue(value)) {
+ refresh();
+ }
+ if (isEnabledValue(value)) {
+ node.fireNodeChanged();
+ }
+ node.childDeleted(value);
+ }
+
+ private void valueLifespanChanged(TraceObjectValue value, Range oldSpan,
+ Range newSpan) {
+ boolean inOld = DBTraceUtils.intersect(oldSpan, span);
+ boolean inNew = DBTraceUtils.intersect(newSpan, span);
+ if (inOld == inNew) {
+ return;
+ }
+ AbstractNode node = nodeCache.getByObject(value.getParent());
+ if (node == null) {
+ return;
+ }
+ if (isEventValue(value)) {
+ refresh();
+ }
+ if (isEnabledValue(value)) {
+ node.fireNodeChanged();
+ }
+ if (inNew) {
+ node.childCreated(value);
+ }
+ else {
+ node.childDeleted(value);
+ }
+ }
+ }
+
+ class NodeCache {
+ Map, AbstractNode> byValue = new WeakValueHashMap<>();
+ Map, AbstractNode> byObject = new WeakValueHashMap<>();
+
+ protected AbstractNode createNode(TraceObjectValue value) {
+ if (value.isCanonical()) {
+ return new CanonicalNode(value);
+ }
+ if (value.isObject()) {
+ return new LinkNode(value);
+ }
+ return new PrimitiveNode(value);
+ }
+
+ protected AbstractNode getOrCreateNode(TraceObjectValue value) {
+ if (value.getParent() == null) {
+ return root;
+ }
+ AbstractNode node =
+ byValue.computeIfAbsent(new IDKeyed<>(value), k -> createNode(value));
+ //AbstractNode node = createNode(value);
+ if (value.isCanonical()) {
+ byObject.put(new IDKeyed<>(value.getChild()), node);
+ }
+ return node;
+ }
+
+ protected AbstractNode getByValue(TraceObjectValue value) {
+ return byValue.get(new IDKeyed<>(value));
+ }
+
+ protected AbstractNode getByObject(TraceObject object) {
+ if (object.isRoot()) {
+ return root;
+ }
+ return byObject.get(new IDKeyed<>(object));
+ }
+ }
+
+ public abstract class AbstractNode extends GTreeLazyNode {
+ public abstract TraceObjectValue getValue();
+
+ protected void childCreated(TraceObjectValue value) {
+ if (getParent() == null || !isLoaded()) {
+ return;
+ }
+ if (isValueVisible(value)) {
+ AbstractNode child = nodeCache.getOrCreateNode(value);
+ addNode(child);
+ }
+ }
+
+ protected void childDeleted(TraceObjectValue value) {
+ if (getParent() == null || !isLoaded()) {
+ return;
+ }
+ AbstractNode child = nodeCache.getByValue(value);
+ if (child != null) {
+ removeNode(child);
+ }
+ }
+
+ protected AbstractNode getNode(TraceObjectKeyPath p, int pos) {
+ if (pos >= p.getKeyList().size()) {
+ return this;
+ }
+ String key = p.getKeyList().get(pos);
+ AbstractNode matched = children().stream()
+ .map(c -> (AbstractNode) c)
+ .filter(c -> key.equals(c.getValue().getEntryKey()))
+ .findFirst()
+ .orElse(null);
+ if (matched == null) {
+ return null;
+ }
+ return matched.getNode(p, pos + 1);
+ }
+
+ public AbstractNode getNode(TraceObjectKeyPath p) {
+ return getNode(p, 0);
+ }
+
+ protected boolean isModified() {
+ return isValueModified(getValue());
+ }
+ }
+
+ class RootNode extends AbstractNode {
+ @Override
+ public TraceObjectValue getValue() {
+ if (trace == null) {
+ return null;
+ }
+ TraceObject root = trace.getObjectManager().getRootObject();
+ if (root == null) {
+ return null;
+ }
+ return root.getCanonicalParent(0);
+ }
+
+ @Override
+ public String getName() {
+ if (trace == null) {
+ return "No trace is active";
+ }
+ TraceObject root = trace.getObjectManager().getRootObject();
+ if (root == null) {
+ return "Trace has no model";
+ }
+ return "" +
+ HTMLUtilities.escapeHTML(display.getObjectDisplay(root.getCanonicalParent(0)));
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return DebuggerResources.ICON_DEBUGGER; // TODO
+ }
+
+ @Override
+ public String getToolTip() {
+ if (trace == null) {
+ return "No trace is active";
+ }
+ TraceObject root = trace.getObjectManager().getRootObject();
+ if (root == null) {
+ return "Trace has no model";
+ }
+ return display.getObjectToolTip(root.getCanonicalParent(0));
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ @Override
+ protected List generateChildren() {
+ if (trace == null) {
+ return List.of();
+ }
+ TraceObject root = trace.getObjectManager().getRootObject();
+ if (root == null) {
+ return List.of();
+ }
+ return generateObjectChildren(root);
+ }
+
+ @Override
+ protected boolean isModified() {
+ return false;
+ }
+
+ @Override
+ protected void childCreated(TraceObjectValue value) {
+ unloadChildren();
+ }
+ }
+
+ public class PrimitiveNode extends AbstractNode {
+ protected final TraceObjectValue value;
+
+ public PrimitiveNode(TraceObjectValue value) {
+ this.value = value;
+ }
+
+ @Override
+ public TraceObjectValue getValue() {
+ return value;
+ }
+
+ @Override
+ protected List generateChildren() {
+ return List.of();
+ }
+
+ @Override
+ public String getName() {
+ String html = HTMLUtilities.escapeHTML(
+ value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue()));
+ return "" + html;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return DebuggerResources.ICON_OBJECT_UNPOPULATED;
+ }
+
+ @Override
+ public String getToolTip() {
+ return display.getPrimitiveEdgeToolTip(value);
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+ }
+
+ public abstract class AbstractObjectNode extends AbstractNode {
+ protected final TraceObjectValue value;
+ protected final TraceObject object;
+
+ public AbstractObjectNode(TraceObjectValue value) {
+ this.value = value;
+ this.object = Objects.requireNonNull(value.getChild());
+ }
+
+ @Override
+ public TraceObjectValue getValue() {
+ return value;
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ return getObjectIcon(value, expanded);
+ }
+ }
+
+ public class LinkNode extends AbstractObjectNode {
+ public LinkNode(TraceObjectValue value) {
+ super(value);
+ }
+
+ @Override
+ public String getName() {
+ return "" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": " +
+ HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "";
+ }
+
+ @Override
+ public String getToolTip() {
+ return display.getObjectLinkToolTip(value);
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+
+ @Override
+ protected List generateChildren() {
+ return List.of();
+ }
+
+ @Override
+ protected void childCreated(TraceObjectValue value) {
+ throw new AssertionError();
+ }
+
+ @Override
+ protected void childDeleted(TraceObjectValue value) {
+ throw new AssertionError();
+ }
+ }
+
+ public class CanonicalNode extends AbstractObjectNode {
+ public CanonicalNode(TraceObjectValue value) {
+ super(value);
+ }
+
+ @Override
+ protected List generateChildren() {
+ return generateObjectChildren(object);
+ }
+
+ @Override
+ public String getName() {
+ return "" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value));
+ }
+
+ @Override
+ public String getToolTip() {
+ return display.getObjectToolTip(value);
+ }
+
+ @Override
+ public Icon getIcon(boolean expanded) {
+ TraceObjectValue parentValue = object.getCanonicalParent(snap);
+ if (parentValue == null) {
+ return super.getIcon(expanded);
+ }
+ if (!parentValue.getParent().getTargetSchema().isCanonicalContainer()) {
+ return super.getIcon(expanded);
+ }
+ if (!isOnEventPath(object)) {
+ return super.getIcon(expanded);
+ }
+ return DebuggerResources.ICON_EVENT_MARKER;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+ }
+
+ interface LastKeyDisplaysObjectValues extends DisplaysObjectValues {
+ @Override
+ default String getRawObjectDisplay(TraceObjectValue edge) {
+ TraceObject object = edge.getChild();
+ if (object.isRoot()) {
+ return "Root";
+ }
+ if (edge.isCanonical()) {
+ return edge.getEntryKey();
+ }
+ return object.getCanonicalPath().toString();
+ }
+ }
+
+ protected class TreeDisplaysObjectValues implements LastKeyDisplaysObjectValues {
+ @Override
+ public long getSnap() {
+ return snap;
+ }
+ }
+
+ protected class DiffTreeDisplaysObjectValues implements LastKeyDisplaysObjectValues {
+ @Override
+ public long getSnap() {
+ return diffSnap;
+ }
+ }
+
+ private Trace trace;
+ private long snap;
+ private Trace diffTrace;
+ private long diffSnap;
+ private Range span = Range.all();
+ private boolean showHidden;
+ private boolean showPrimitives;
+ private boolean showMethods;
+
+ private final RootNode root = new RootNode();
+ private final NodeCache nodeCache = new NodeCache();
+
+ // TODO: User-modifiable?
+ // TODO: Load and save this. Options panel? Defaults for GDB/dbgeng?
+ private Map icons = fillIconMap(new HashMap<>());
+
+ private final ListenerForChanges listenerForChanges = newListenerForChanges();
+ protected final DisplaysObjectValues display = new TreeDisplaysObjectValues();
+ protected final DisplaysObjectValues diffDisplay = new DiffTreeDisplaysObjectValues();
+
+ protected ListenerForChanges newListenerForChanges() {
+ return new ListenerForChanges();
+ }
+
+ protected Map fillIconMap(Map map) {
+ map.put("Process", DebuggerResources.ICON_PROCESS);
+ map.put("Thread", DebuggerResources.ICON_THREAD);
+ map.put("Memory", DebuggerResources.ICON_REGIONS);
+ map.put("Interpreter", DebuggerResources.ICON_CONSOLE);
+ map.put("Console", DebuggerResources.ICON_CONSOLE);
+ map.put("Stack", DebuggerResources.ICON_PROVIDER_STACK);
+ // TODO: StackFrame
+ map.put("BreakpointContainer", DebuggerResources.ICON_BREAKPOINTS);
+ map.put("BreakpointLocationContainer", DebuggerResources.ICON_BREAKPOINTS);
+ // NOTE: Breakpoints done dynamically for enabled/disabled.
+ map.put("RegisterContainer", DebuggerResources.ICON_REGISTERS);
+ // TODO: Register
+ map.put("ModuleContainer", DebuggerResources.ICON_MODULES);
+ // TODO: single module / section
+ return map;
+ }
+
+ protected TraceObject getEventObject(TraceObject object) {
+ TraceObject scope = object.queryCanonicalAncestorsTargetInterface(TargetEventScope.class)
+ .findFirst()
+ .orElse(null);
+ if (scope == null) {
+ return null;
+ }
+ if (scope == object) {
+ return null;
+ }
+ TraceObjectValue eventValue =
+ scope.getAttribute(snap, TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME);
+ if (eventValue == null || !eventValue.isObject()) {
+ return null;
+ }
+ return eventValue.getChild();
+ }
+
+ protected boolean isOnEventPath(TraceObject object) {
+ TraceObject eventObject = getEventObject(object);
+ if (eventObject == null) {
+ return false;
+ }
+ if (object.getCanonicalPath().isAncestor(eventObject.getCanonicalPath())) {
+ return true;
+ }
+ return false;
+ }
+
+ protected Icon getObjectIcon(TraceObjectValue edge, boolean expanded) {
+ String type = display.getObjectType(edge);
+ Icon forType = icons.get(type);
+ if (forType != null) {
+ return forType;
+ }
+ if (type.contains("Breakpoint")) {
+ TraceObject object = edge.getChild();
+ TraceObjectValue en =
+ object.getAttribute(snap, TargetBreakpointSpec.ENABLED_ATTRIBUTE_NAME);
+ // includes true or non-boolean values
+ if (en == null || !Objects.equals(false, en.getValue())) {
+ return DebuggerResources.ICON_SET_BREAKPOINT;
+ }
+ return DebuggerResources.ICON_DISABLE_BREAKPOINT;
+ }
+ return DebuggerResources.ICON_OBJECT_POPULATED;
+ /*
+ * TODO?: Populated/unpopulated? Seems to duplicate isLeaf. The absence/presence of an
+ * expander should already communicate this info.... We could instead use icon to indicate
+ * freshness, but how would we know? The sync mode from the schema might help.
+ */
+ }
+
+ protected boolean isValueVisible(TraceObjectValue value) {
+ if (!showHidden && value.isHidden()) {
+ return false;
+ }
+ if (!showPrimitives && !value.isObject()) {
+ return false;
+ }
+ if (!showMethods && value.isObject() && value.getChild().isMethod(snap)) {
+ return false;
+ }
+ if (!DBTraceUtils.intersect(value.getLifespan(), span)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isEdgesDiffer(TraceObjectValue newEdge, TraceObjectValue oldEdge) {
+ if (DisplaysModified.super.isEdgesDiffer(newEdge, oldEdge)) {
+ return true;
+ }
+ // Hack to incorporate _display logic to differencing.
+ // This ensures "boxed" primitives show as differing at the object level
+ return !Objects.equals(diffDisplay.getEdgeDisplay(oldEdge),
+ display.getEdgeDisplay(newEdge));
+ }
+
+ protected List generateObjectChildren(TraceObject object) {
+ List result = ObjectTableModel
+ .distinctCanonical(object.getValues().stream().filter(this::isValueVisible))
+ .map(v -> nodeCache.getOrCreateNode(v))
+ .collect(Collectors.toList());
+ return result;
+ }
+
+ public GTreeLazyNode getRoot() {
+ return root;
+ }
+
+ protected void removeOldListeners() {
+ if (trace != null) {
+ trace.removeListener(listenerForChanges);
+ }
+ }
+
+ protected void addNewListeners() {
+ if (trace != null) {
+ trace.addListener(listenerForChanges);
+ }
+ }
+
+ protected void refresh() {
+ for (AbstractNode node : nodeCache.byObject.values()) {
+ node.fireNodeChanged();
+ }
+ }
+
+ protected void reload() {
+ root.unloadChildren();
+ }
+
+ public void setTrace(Trace trace) {
+ if (this.trace == trace) {
+ return;
+ }
+ removeOldListeners();
+ this.trace = trace;
+ addNewListeners();
+ traceChanged();
+ }
+
+ protected void traceChanged() {
+ reload();
+ }
+
+ @Override
+ public Trace getTrace() {
+ return trace;
+ }
+
+ protected void snapChanged() {
+ // Span will be set to singleton by client, if desired
+ refresh();
+ }
+
+ public void setSnap(long snap) {
+ if (this.snap == snap) {
+ return;
+ }
+ this.snap = snap;
+ snapChanged();
+ }
+
+ @Override
+ public long getSnap() {
+ return snap;
+ }
+
+ protected void diffTraceChanged() {
+ refresh();
+ }
+
+ /**
+ * Set alternative trace to colorize values that differ
+ *
+ *
+ * The same trace can be used, but with an alternative snap, if desired. See
+ * {@link #setDiffSnap(long)}. One common use is to compare with the previous snap of the same
+ * trace. Another common use is to compare with the previous navigation.
+ *
+ * @param diffTrace the alternative trace
+ */
+ public void setDiffTrace(Trace diffTrace) {
+ if (this.diffTrace == diffTrace) {
+ return;
+ }
+ this.diffTrace = diffTrace;
+ diffTraceChanged();
+ }
+
+ @Override
+ public Trace getDiffTrace() {
+ return diffTrace;
+ }
+
+ protected void diffSnapChanged() {
+ refresh();
+ }
+
+ /**
+ * Set alternative snap to colorize values that differ
+ *
+ *
+ * The diff trace must be set, even if it's the same as the trace being displayed. See
+ * {@link #setDiffTrace(Trace)}.
+ *
+ * @param diffSnap the alternative snap
+ */
+ public void setDiffSnap(long diffSnap) {
+ if (this.diffSnap == diffSnap) {
+ return;
+ }
+ this.diffSnap = diffSnap;
+ diffSnapChanged();
+ }
+
+ @Override
+ public long getDiffSnap() {
+ return diffSnap;
+ }
+
+ protected void spanChanged() {
+ reload();
+ }
+
+ public void setSpan(Range span) {
+ if (Objects.equals(this.span, span)) {
+ return;
+ }
+ this.span = span;
+ spanChanged();
+ }
+
+ public Range getSpan() {
+ return span;
+ }
+
+ protected void showHiddenChanged() {
+ reload();
+ }
+
+ public void setShowHidden(boolean showHidden) {
+ if (this.showHidden == showHidden) {
+ return;
+ }
+ this.showHidden = showHidden;
+ showHiddenChanged();
+ }
+
+ public boolean isShowHidden() {
+ return showHidden;
+ }
+
+ protected void showPrimitivesChanged() {
+ reload();
+ }
+
+ public void setShowPrimitives(boolean showPrimitives) {
+ if (this.showPrimitives == showPrimitives) {
+ return;
+ }
+ this.showPrimitives = showPrimitives;
+ showPrimitivesChanged();
+ }
+
+ public boolean isShowPrimitives() {
+ return showPrimitives;
+ }
+
+ protected void showMethodsChanged() {
+ reload();
+ }
+
+ public void setShowMethods(boolean showMethods) {
+ if (this.showMethods == showMethods) {
+ return;
+ }
+ this.showMethods = showMethods;
+ showMethodsChanged();
+ }
+
+ public boolean isShowMethods() {
+ return showMethods;
+ }
+
+ public AbstractNode getNode(TraceObjectKeyPath p) {
+ return root.getNode(p);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java
new file mode 100644
index 0000000000..5bdb24ebe2
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTablePanel.java
@@ -0,0 +1,30 @@
+/* ###
+ * 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.model;
+
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.framework.plugintool.Plugin;
+
+public class ObjectsTablePanel extends AbstractQueryTablePanel {
+ public ObjectsTablePanel(Plugin plugin) {
+ super(plugin);
+ }
+
+ @Override
+ protected AbstractQueryTableModel createModel(Plugin plugin) {
+ return new ObjectTableModel(plugin);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java
new file mode 100644
index 0000000000..8925d0c566
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/ObjectsTreePanel.java
@@ -0,0 +1,265 @@
+/* ###
+ * 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.model;
+
+import java.awt.*;
+import java.awt.event.MouseListener;
+import java.util.*;
+import java.util.List;
+import java.util.stream.*;
+
+import javax.swing.JPanel;
+import javax.swing.JTree;
+import javax.swing.tree.TreePath;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.tree.*;
+import docking.widgets.tree.support.GTreeRenderer;
+import docking.widgets.tree.support.GTreeSelectionListener;
+import ghidra.app.plugin.core.debug.DebuggerCoordinates;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTreeModel.AbstractNode;
+import ghidra.trace.model.target.TraceObjectKeyPath;
+
+public class ObjectsTreePanel extends JPanel {
+
+ protected class ObjectsTreeRenderer extends GTreeRenderer implements ColorsModified.InTree {
+ {
+ setHTMLRenderingEnabled(true);
+ }
+
+ @Override
+ public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
+ boolean expanded, boolean leaf, int row, boolean hasFocus) {
+ super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row,
+ hasFocus);
+ if (!(value instanceof AbstractNode)) {
+ return this;
+ }
+
+ AbstractNode node = (AbstractNode) value;
+ setForeground(getForegroundFor(tree, node.isModified(), selected));
+ return this;
+ }
+
+ @Override
+ public Color getDiffForeground(JTree tree) {
+ return diffColor;
+ }
+
+ @Override
+ public Color getDiffSelForeground(JTree tree) {
+ return diffColorSel;
+ }
+ }
+
+ protected final ObjectTreeModel treeModel;
+ protected final GTree tree;
+
+ protected DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
+ protected boolean limitToSnap = true;
+ protected boolean showHidden = false;
+ protected boolean showPrimitives = false;
+ protected boolean showMethods = false;
+
+ protected Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED;
+ protected Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL;
+
+ public ObjectsTreePanel() {
+ super(new BorderLayout());
+ treeModel = createModel();
+ tree = new GTree(treeModel.getRoot());
+
+ tree.setCellRenderer(new ObjectsTreeRenderer());
+ add(tree, BorderLayout.CENTER);
+ }
+
+ protected ObjectTreeModel createModel() {
+ return new ObjectTreeModel();
+ }
+
+ protected class KeepTreeState implements AutoCloseable {
+ private final GTreeState state;
+
+ public KeepTreeState() {
+ this.state = tree.getTreeState();
+ }
+
+ @Override
+ public void close() {
+ tree.restoreTreeState(state);
+ }
+ }
+
+ public void goToCoordinates(DebuggerCoordinates coords) {
+ // TODO: thread should probably become a TraceObject once we transition
+ if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) {
+ return;
+ }
+ DebuggerCoordinates previous = current;
+ this.current = coords;
+ try (KeepTreeState keep = new KeepTreeState()) {
+ treeModel.setDiffTrace(previous.getTrace());
+ treeModel.setTrace(current.getTrace());
+ treeModel.setDiffSnap(previous.getSnap());
+ treeModel.setSnap(current.getSnap());
+ if (limitToSnap) {
+ treeModel.setSpan(Range.singleton(current.getSnap()));
+ }
+ tree.filterChanged();
+ }
+ }
+
+ public void setLimitToSnap(boolean limitToSnap) {
+ if (this.limitToSnap == limitToSnap) {
+ return;
+ }
+ this.limitToSnap = limitToSnap;
+ try (KeepTreeState keep = new KeepTreeState()) {
+ treeModel.setSpan(limitToSnap ? Range.singleton(current.getSnap()) : Range.all());
+ }
+ }
+
+ public boolean isLimitToSnap() {
+ return limitToSnap;
+ }
+
+ public void setShowHidden(boolean showHidden) {
+ if (this.showHidden == showHidden) {
+ return;
+ }
+ this.showHidden = showHidden;
+ try (KeepTreeState keep = new KeepTreeState()) {
+ treeModel.setShowHidden(showHidden);
+ }
+ }
+
+ public boolean isShowHidden() {
+ return showHidden;
+ }
+
+ public void setShowPrimitives(boolean showPrimitives) {
+ if (this.showPrimitives == showPrimitives) {
+ return;
+ }
+ this.showPrimitives = showPrimitives;
+ try (KeepTreeState keep = new KeepTreeState()) {
+ treeModel.setShowPrimitives(showPrimitives);
+ }
+ }
+
+ public boolean isShowPrimitives() {
+ return showPrimitives;
+ }
+
+ public void setShowMethods(boolean showMethods) {
+ if (this.showMethods == showMethods) {
+ return;
+ }
+ this.showMethods = showMethods;
+ try (KeepTreeState keep = new KeepTreeState()) {
+ treeModel.setShowMethods(showMethods);
+ }
+ }
+
+ public boolean isShowMethods() {
+ return showMethods;
+ }
+
+ public void setDiffColor(Color diffColor) {
+ if (Objects.equals(this.diffColor, diffColor)) {
+ return;
+ }
+ this.diffColor = diffColor;
+ repaint();
+ }
+
+ public void setDiffColorSel(Color diffColorSel) {
+ if (Objects.equals(this.diffColorSel, diffColorSel)) {
+ return;
+ }
+ this.diffColorSel = diffColorSel;
+ repaint();
+ }
+
+ public void addTreeSelectionListener(GTreeSelectionListener listener) {
+ tree.addGTreeSelectionListener(listener);
+ }
+
+ public void removeTreeSelectionListener(GTreeSelectionListener listener) {
+ tree.removeGTreeSelectionListener(listener);
+ }
+
+ @Override
+ public synchronized void addMouseListener(MouseListener l) {
+ super.addMouseListener(l);
+ // Is this a HACK?
+ tree.addMouseListener(l);
+ }
+
+ @Override
+ public synchronized void removeMouseListener(MouseListener l) {
+ super.removeMouseListener(l);
+ // HACK?
+ tree.removeMouseListener(l);
+ }
+
+ public void setSelectionMode(int selectionMode) {
+ tree.getSelectionModel().setSelectionMode(selectionMode);
+ }
+
+ public int getSelectionMode() {
+ return tree.getSelectionModel().getSelectionMode();
+ }
+
+ protected R getItemsFromPaths(TreePath[] paths,
+ Collector super AbstractNode, A, R> collector) {
+ return Stream.of(paths)
+ .map(p -> (AbstractNode) p.getLastPathComponent())
+ .collect(collector);
+ }
+
+ protected AbstractNode getItemFromPath(TreePath path) {
+ if (path == null) {
+ return null;
+ }
+ return (AbstractNode) path.getLastPathComponent();
+ }
+
+ public List getSelectedItems() {
+ return getItemsFromPaths(tree.getSelectionPaths(), Collectors.toList());
+ }
+
+ public AbstractNode getSelectedItem() {
+ return getItemFromPath(tree.getSelectionPath());
+ }
+
+ public AbstractNode getNode(TraceObjectKeyPath path) {
+ return treeModel.getNode(path);
+ }
+
+ public void setSelectedKeyPaths(Collection keyPaths) {
+ List nodes = new ArrayList<>();
+ for (TraceObjectKeyPath path : keyPaths) {
+ AbstractNode node = getNode(path);
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ tree.setSelectedNodes(nodes);
+ }
+}
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
new file mode 100644
index 0000000000..8e3e1ae898
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathTableModel.java
@@ -0,0 +1,155 @@
+/* ###
+ * 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.model;
+
+import java.awt.Color;
+import java.util.*;
+import java.util.stream.Stream;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.table.TableColumnDescriptor;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.app.plugin.core.debug.gui.model.columns.*;
+import ghidra.framework.plugintool.Plugin;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObjectValPath;
+
+public class PathTableModel extends AbstractQueryTableModel {
+ /** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
+ private TracePathValueColumn valueColumn;
+ private TracePathLastLifespanPlotColumn lifespanPlotColumn;
+
+ protected static Stream extends TraceObjectValPath> distinctKeyPath(
+ Stream extends TraceObjectValPath> stream) {
+ Set> seen = new HashSet<>();
+ return stream.filter(path -> seen.add(path.getKeyList()));
+ }
+
+ public class PathRow {
+ private final TraceObjectValPath path;
+ private final Object value;
+
+ public PathRow(TraceObjectValPath path) {
+ this.path = path;
+ this.value = computeValue();
+ }
+
+ public TraceObjectValPath getPath() {
+ return path;
+ }
+
+ public Object computeValue() {
+ // Spare fetching the root unless it's really needed
+ if (path.getLastEntry() == null) {
+ return getTrace().getObjectManager().getRootObject();
+ }
+ return path.getDestinationValue(null);
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ /**
+ * Get a non-HTML string representing how this row's value should be sorted, filtered, etc.
+ *
+ * @return the display string
+ */
+ public String getDisplay() {
+ return display.getEdgeDisplay(path.getLastEntry());
+ }
+
+ /**
+ * Get an HTML string representing how this row's value should be displayed
+ *
+ * @return the display string
+ */
+ public String getHtmlDisplay() {
+ return display.getEdgeHtmlDisplay(path.getLastEntry());
+ }
+
+ public String getToolTip() {
+ return display.getEdgeToolTip(path.getLastEntry());
+ }
+
+ public boolean isModified() {
+ return isValueModified(path.getLastEntry());
+ }
+ }
+
+ public PathTableModel(Plugin plugin) {
+ super("Attribute Model", plugin);
+ }
+
+ protected void updateTimelineMax() {
+ Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
+ Range fullRange = Range.closed(0L, max == null ? 1 : max + 1);
+ lifespanPlotColumn.setFullRange(fullRange);
+ }
+
+ @Override
+ protected void traceChanged() {
+ updateTimelineMax();
+ super.traceChanged();
+ }
+
+ @Override
+ protected void showHiddenChanged() {
+ reload();
+ super.showHiddenChanged();
+ }
+
+ @Override
+ protected void maxSnapChanged() {
+ updateTimelineMax();
+ refresh();
+ }
+
+ protected static boolean isAnyHidden(TraceObjectValPath path) {
+ return path.getEntryList().stream().anyMatch(v -> v.isHidden());
+ }
+
+ @Override
+ protected Stream streamRows(Trace trace, ModelQuery query, Range span) {
+ // TODO: For queries with early wildcards, this is not efficient
+ // May need to incorporate filtering hidden into the query execution itself.
+ return distinctKeyPath(query.streamPaths(trace, span)
+ .filter(p -> isShowHidden() || !isAnyHidden(p)))
+ .map(PathRow::new);
+ }
+
+ @Override
+ protected TableColumnDescriptor createTableColumnDescriptor() {
+ TableColumnDescriptor descriptor = new TableColumnDescriptor<>();
+ descriptor.addHiddenColumn(new TracePathStringColumn());
+ descriptor.addVisibleColumn(new TracePathLastKeyColumn());
+ descriptor.addVisibleColumn(valueColumn = new TracePathValueColumn());
+ descriptor.addVisibleColumn(new TracePathLastLifespanColumn());
+ descriptor.addHiddenColumn(lifespanPlotColumn = new TracePathLastLifespanPlotColumn());
+ return descriptor;
+ }
+
+ @Override
+ public void setDiffColor(Color diffColor) {
+ valueColumn.setDiffColor(diffColor);
+ }
+
+ @Override
+ public void setDiffColorSel(Color diffColorSel) {
+ valueColumn.setDiffColorSel(diffColorSel);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java
new file mode 100644
index 0000000000..f3d9cf4a3a
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/PathsTablePanel.java
@@ -0,0 +1,30 @@
+/* ###
+ * 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.model;
+
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.framework.plugintool.Plugin;
+
+public class PathsTablePanel extends AbstractQueryTablePanel {
+ public PathsTablePanel(Plugin plugin) {
+ super(plugin);
+ }
+
+ @Override
+ protected AbstractQueryTableModel createModel(Plugin plugin) {
+ return new PathTableModel(plugin);
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java
new file mode 100644
index 0000000000..55eee69287
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastKeyColumn.java
@@ -0,0 +1,42 @@
+/* ###
+ * 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.model.columns;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObjectValPath;
+import ghidra.trace.model.target.TraceObjectValue;
+
+public class TracePathLastKeyColumn extends AbstractDynamicTableColumn {
+ @Override
+ public String getColumnName() {
+ return "Key";
+ }
+
+ @Override
+ public String getValue(PathRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ TraceObjectValPath path = rowObject.getPath();
+ TraceObjectValue lastEntry = path.getLastEntry();
+ if (lastEntry == null) {
+ return "";
+ }
+ return lastEntry.getEntryKey();
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java
new file mode 100644
index 0000000000..d27a693786
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanColumn.java
@@ -0,0 +1,44 @@
+/* ###
+ * 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.model.columns;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObjectValue;
+
+public class TracePathLastLifespanColumn
+ extends AbstractDynamicTableColumn, Trace> {
+
+ @Override
+ public String getColumnName() {
+ return "Life";
+ }
+
+ @Override
+ public Range getValue(PathRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ TraceObjectValue lastEntry = rowObject.getPath().getLastEntry();
+ if (lastEntry == null) {
+ return Range.all();
+ }
+ return lastEntry.getLifespan();
+ }
+}
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
new file mode 100644
index 0000000000..1be1443188
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathLastLifespanPlotColumn.java
@@ -0,0 +1,60 @@
+/* ###
+ * 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.model.columns;
+
+import com.google.common.collect.Range;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import docking.widgets.table.RangeTableCellRenderer;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.table.column.GColumnRenderer;
+
+public class TracePathLastLifespanPlotColumn
+ extends AbstractDynamicTableColumn, Trace> {
+
+ private final RangeTableCellRenderer cellRenderer = new RangeTableCellRenderer<>();
+
+ @Override
+ public String getColumnName() {
+ return "Plot";
+ }
+
+ @Override
+ public Range getValue(PathRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ TraceObjectValue lastEntry = rowObject.getPath().getLastEntry();
+ if (lastEntry == null) {
+ return Range.all();
+ }
+ return lastEntry.getLifespan();
+ }
+
+ @Override
+ public GColumnRenderer> getColumnRenderer() {
+ return cellRenderer;
+ }
+
+ // TODO: header renderer
+
+ public void setFullRange(Range fullRange) {
+ cellRenderer.setFullRange(fullRange);
+ // TODO: header, too
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java
new file mode 100644
index 0000000000..93e6c6137f
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathStringColumn.java
@@ -0,0 +1,36 @@
+/* ###
+ * 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.model.columns;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.dbg.util.PathUtils;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+
+public class TracePathStringColumn extends AbstractDynamicTableColumn {
+ @Override
+ public String getColumnName() {
+ return "Path";
+ }
+
+ @Override
+ public String getValue(PathRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return PathUtils.toString(rowObject.getPath().getKeyList());
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java
new file mode 100644
index 0000000000..a65e8259ff
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TracePathValueColumn.java
@@ -0,0 +1,93 @@
+/* ###
+ * 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.model.columns;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.JTable;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import docking.widgets.table.GTableCellRenderingData;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.model.ColorsModified;
+import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.util.table.column.AbstractGColumnRenderer;
+import ghidra.util.table.column.GColumnRenderer;
+
+public class TracePathValueColumn extends AbstractDynamicTableColumn {
+ private final class ValueRenderer extends AbstractGColumnRenderer
+ implements ColorsModified.InTable {
+ {
+ setHTMLRenderingEnabled(true);
+ }
+
+ @Override
+ public String getFilterString(PathRow t, Settings settings) {
+ return t.getDisplay();
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(GTableCellRenderingData data) {
+ super.getTableCellRendererComponent(data);
+ PathRow row = (PathRow) data.getValue();
+ setText(row.getHtmlDisplay());
+ setToolTipText(row.getToolTip());
+ setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected()));
+ return this;
+ }
+
+ @Override
+ public Color getDiffForeground(JTable table) {
+ return diffColor;
+ }
+
+ @Override
+ public Color getDiffSelForeground(JTable table) {
+ return diffColorSel;
+ }
+ }
+
+ private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED;
+ private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL;
+
+ @Override
+ public String getColumnName() {
+ return "Value";
+ }
+
+ @Override
+ public PathRow getValue(PathRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject;
+ }
+
+ @Override
+ public GColumnRenderer getColumnRenderer() {
+ return new ValueRenderer();
+ }
+
+ public void setDiffColor(Color diffColor) {
+ this.diffColor = diffColor;
+ }
+
+ public void setDiffColorSel(Color diffColorSel) {
+ this.diffColorSel = diffColorSel;
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java
new file mode 100644
index 0000000000..875e01f877
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueKeyColumn.java
@@ -0,0 +1,35 @@
+/* ###
+ * 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.model.columns;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+
+public class TraceValueKeyColumn extends AbstractDynamicTableColumn {
+ @Override
+ public String getColumnName() {
+ return "Key";
+ }
+
+ @Override
+ public String getValue(ValueRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject.getKey();
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java
new file mode 100644
index 0000000000..f63a7d93aa
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifeColumn.java
@@ -0,0 +1,39 @@
+/* ###
+ * 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.model.columns;
+
+import com.google.common.collect.RangeSet;
+
+import docking.widgets.table.AbstractDynamicTableColumn;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+
+public class TraceValueLifeColumn
+ extends AbstractDynamicTableColumn, Trace> {
+
+ @Override
+ public String getColumnName() {
+ return "Life";
+ }
+
+ @Override
+ public RangeSet getValue(ValueRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject.getLife();
+ }
+}
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
new file mode 100644
index 0000000000..c7d408f18b
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueLifePlotColumn.java
@@ -0,0 +1,56 @@
+/* ###
+ * 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.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 ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.util.table.column.GColumnRenderer;
+
+public class TraceValueLifePlotColumn
+ extends AbstractDynamicTableColumn, Trace> {
+
+ private final RangeSetTableCellRenderer cellRenderer = new RangeSetTableCellRenderer<>();
+
+ @Override
+ public String getColumnName() {
+ return "Plot";
+ }
+
+ @Override
+ public RangeSet getValue(ValueRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject.getLife();
+ }
+
+ @Override
+ public GColumnRenderer> getColumnRenderer() {
+ return cellRenderer;
+ }
+
+ // TODO: The header renderer
+
+ public void setFullRange(Range fullRange) {
+ cellRenderer.setFullRange(fullRange);
+ // TODO: set header's full range, too
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java
new file mode 100644
index 0000000000..dc313d38dc
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueObjectAttributeColumn.java
@@ -0,0 +1,180 @@
+/* ###
+ * 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.model.columns;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.util.Comparator;
+import java.util.function.Function;
+
+import javax.swing.JTable;
+
+import docking.widgets.table.*;
+import docking.widgets.table.sort.ColumnRenderedValueBackupComparator;
+import docking.widgets.table.sort.DefaultColumnComparator;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.model.ColorsModified;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
+import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
+import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
+import ghidra.dbg.target.TargetMethod.TargetParameterMap;
+import ghidra.dbg.target.TargetObject;
+import ghidra.dbg.target.TargetSteppable.TargetStepKindSet;
+import ghidra.dbg.target.schema.SchemaContext;
+import ghidra.dbg.target.schema.TargetObjectSchema;
+import ghidra.dbg.target.schema.TargetObjectSchema.AttributeSchema;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.trace.model.target.TraceObject;
+import ghidra.trace.model.target.TraceObjectValue;
+import ghidra.util.table.column.AbstractGColumnRenderer;
+import ghidra.util.table.column.GColumnRenderer;
+
+public class TraceValueObjectAttributeColumn
+ extends AbstractDynamicTableColumn {
+
+ public class AttributeRenderer extends AbstractGColumnRenderer
+ implements ColorsModified.InTable {
+ {
+ setHTMLRenderingEnabled(true);
+ }
+
+ @Override
+ public String getFilterString(ValueRow t, Settings settings) {
+ return t.getAttributeDisplay(attributeName);
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(GTableCellRenderingData data) {
+ super.getTableCellRendererComponent(data);
+ ValueRow row = (ValueRow) data.getValue();
+ setText(row.getAttributeHtmlDisplay(attributeName));
+ setToolTipText(row.getAttributeToolTip(attributeName));
+ setForeground(getForegroundFor(data.getTable(), row.isAttributeModified(attributeName),
+ data.isSelected()));
+ return this;
+ }
+
+ @Override
+ public Color getDiffForeground(JTable table) {
+ return diffColor;
+ }
+
+ @Override
+ public Color getDiffSelForeground(JTable table) {
+ return diffColorSel;
+ }
+ }
+
+ public static Class> computeColumnType(SchemaContext ctx, AttributeSchema attributeSchema) {
+ TargetObjectSchema schema = ctx.getSchema(attributeSchema.getSchema());
+ Class> type = schema.getType();
+ if (type == TargetObject.class) {
+ return TraceObject.class;
+ }
+ if (type == TargetExecutionState.class) {
+ return String.class;
+ }
+ if (type == TargetParameterMap.class) {
+ return String.class;
+ }
+ if (type == TargetAttachKindSet.class) {
+ return String.class;
+ }
+ if (type == TargetBreakpointKindSet.class) {
+ return String.class;
+ }
+ if (type == TargetStepKindSet.class) {
+ return String.class;
+ }
+ return type;
+ }
+
+ public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx,
+ AttributeSchema attributeSchema) {
+ String name = attributeSchema.getName();
+ Class> type = computeColumnType(ctx, attributeSchema);
+ return new TraceValueObjectAttributeColumn(name, type);
+ }
+
+ private final String attributeName;
+ private final Class> attributeType;
+ private final AttributeRenderer renderer = new AttributeRenderer();
+ private final Comparator comparator;
+
+ private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED;
+ private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL;
+
+ public TraceValueObjectAttributeColumn(String attributeName, Class> attributeType) {
+ this.attributeName = attributeName;
+ this.attributeType = attributeType;
+ this.comparator = newTypedComparator();
+ }
+
+ @Override
+ public String getColumnName() {
+ /**
+ * TODO: These are going to have "_"-prefixed things.... Sure, they're "hidden", but if we
+ * remove them, we're going to hide important info. I'd like a way in the schema to specify
+ * which "interface attribute" an attribute satisfies. That way, the name can be
+ * human-friendly, but the interface can still find what it needs.
+ */
+ return attributeName;
+ }
+
+ @Override
+ public ValueRow getValue(ValueRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject;
+ }
+
+ @Override
+ public GColumnRenderer getColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public Comparator getComparator(DynamicColumnTableModel> model, int columnIndex) {
+ return comparator == null ? null
+ : comparator.thenComparing(
+ new ColumnRenderedValueBackupComparator<>(model, columnIndex));
+ }
+
+ protected Object getAttributeValue(ValueRow row) {
+ TraceObjectValue edge = row.getAttribute(attributeName);
+ return edge == null ? null : edge.getValue();
+ }
+
+ protected > Comparator newTypedComparator() {
+ if (Comparable.class.isAssignableFrom(attributeType)) {
+ @SuppressWarnings("unchecked")
+ Class cls = (Class) attributeType.asSubclass(Comparable.class);
+ Function keyExtractor = r -> cls.cast(getAttributeValue(r));
+ return Comparator.comparing(keyExtractor, new DefaultColumnComparator());
+ }
+ return null; // Opt for the default filter-string-based comparator
+ }
+
+ public void setDiffColor(Color diffColor) {
+ this.diffColor = diffColor;
+ }
+
+ public void setDiffColorSel(Color diffColorSel) {
+ this.diffColorSel = diffColorSel;
+ }
+}
diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java
new file mode 100644
index 0000000000..c3c0ad88fb
--- /dev/null
+++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/model/columns/TraceValueValColumn.java
@@ -0,0 +1,116 @@
+/* ###
+ * 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.model.columns;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.util.Comparator;
+
+import javax.swing.JTable;
+
+import docking.widgets.table.*;
+import docking.widgets.table.sort.ColumnRenderedValueBackupComparator;
+import ghidra.app.plugin.core.debug.gui.DebuggerResources;
+import ghidra.app.plugin.core.debug.gui.model.ColorsModified;
+import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
+import ghidra.docking.settings.Settings;
+import ghidra.framework.plugintool.ServiceProvider;
+import ghidra.trace.model.Trace;
+import ghidra.util.table.column.AbstractGColumnRenderer;
+import ghidra.util.table.column.GColumnRenderer;
+
+public class TraceValueValColumn extends AbstractDynamicTableColumn {
+ private final class ValRenderer extends AbstractGColumnRenderer
+ implements ColorsModified.InTable {
+ {
+ setHTMLRenderingEnabled(true);
+ }
+
+ @Override
+ public String getFilterString(ValueRow t, Settings settings) {
+ return t.getDisplay();
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(GTableCellRenderingData data) {
+ super.getTableCellRendererComponent(data);
+ ValueRow row = (ValueRow) data.getValue();
+ setText(row.getHtmlDisplay());
+ setToolTipText(row.getToolTip());
+ setForeground(getForegroundFor(data.getTable(), row.isModified(), data.isSelected()));
+ return this;
+ }
+
+ @Override
+ public Color getDiffForeground(JTable table) {
+ return diffColor;
+ }
+
+ @Override
+ public Color getDiffSelForeground(JTable table) {
+ return diffColorSel;
+ }
+ }
+
+ private Color diffColor = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED;
+ private Color diffColorSel = DebuggerResources.DEFAULT_COLOR_VALUE_CHANGED_SEL;
+ private final ValRenderer renderer = new ValRenderer();
+
+ @Override
+ public String getColumnName() {
+ return "Value";
+ }
+
+ @Override
+ public ValueRow getValue(ValueRow rowObject, Settings settings, Trace data,
+ ServiceProvider serviceProvider) throws IllegalArgumentException {
+ return rowObject;
+ }
+
+ @Override
+ public GColumnRenderer getColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public Comparator getComparator(DynamicColumnTableModel> model, int columnIndex) {
+ return getComparator()
+ .thenComparing(new ColumnRenderedValueBackupComparator<>(model, columnIndex));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Comparator getComparator() {
+ return (r1, r2) -> {
+ Object v1 = r1.getValue().getValue();
+ Object v2 = r2.getValue().getValue();
+ if (v1 instanceof Comparable) {
+ if (v1.getClass() == v2.getClass()) {
+ return ((Comparable