mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-23 12:49:45 +00:00
GP-1969: Add 'Model' provider for inspecting object-based traces.
This commit is contained in:
parent
eb0a23aecc
commit
2a4b4f9bcf
@ -100,7 +100,7 @@
|
||||
and Service</A>. <B>Note:</B> Only the raw "Value" column can be edited directly. The "Repr"
|
||||
column cannot be edited, yet.</P>
|
||||
|
||||
<H3><A name="snapshot_window"></A>Snapshot Window</H3>
|
||||
<H3><A name="clone_window"></A>Clone Window</H3>
|
||||
|
||||
<P>This button is analogous to the "snapshot" action of other Ghidra windows. It generates a
|
||||
clone of this window. The clone will no longer follow the current thread, but it will follow
|
||||
|
@ -39,6 +39,7 @@ import ghidra.app.plugin.core.debug.gui.console.DebuggerConsolePlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.listing.DebuggerListingPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.memory.DebuggerMemoryBytesPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.memory.DebuggerRegionsPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.model.DebuggerModelPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.modules.DebuggerStaticMappingPlugin;
|
||||
import ghidra.app.plugin.core.debug.gui.objects.DebuggerObjectsPlugin;
|
||||
@ -132,6 +133,9 @@ public interface DebuggerResources {
|
||||
ImageIcon ICON_SELECT_ROWS = ResourceManager.loadImage("images/table_go.png");
|
||||
ImageIcon ICON_AUTOREAD = ResourceManager.loadImage("images/autoread.png");
|
||||
|
||||
ImageIcon ICON_OBJECT_POPULATED = ResourceManager.loadImage("images/object-populated.png");
|
||||
ImageIcon ICON_OBJECT_UNPOPULATED = ResourceManager.loadImage("images/object-unpopulated.png");
|
||||
|
||||
// TODO: Draw a real icon.
|
||||
ImageIcon ICON_REFRESH_MEMORY = ICON_REFRESH;
|
||||
|
||||
@ -255,6 +259,11 @@ public interface DebuggerResources {
|
||||
HelpLocation HELP_PROVIDER_OBJECTS = new HelpLocation(
|
||||
PluginUtils.getPluginNameFromClass(DebuggerObjectsPlugin.class), HELP_ANCHOR_PLUGIN);
|
||||
|
||||
String TITLE_PROVIDER_MODEL = "Model"; // TODO: An icon
|
||||
ImageIcon ICON_PROVIDER_MODEL = ResourceManager.loadImage("images/function_graph.png");
|
||||
HelpLocation HELP_PROVIDER_MODEL = new HelpLocation(
|
||||
PluginUtils.getPluginNameFromClass(DebuggerModelPlugin.class), HELP_ANCHOR_PLUGIN);
|
||||
|
||||
String TITLE_PROVIDER_WATCHES = "Watches";
|
||||
ImageIcon ICON_PROVIDER_WATCHES = ICON_AUTOREAD; // TODO: Another icon?
|
||||
HelpLocation HELP_PROVIDER_WATCHES = new HelpLocation(
|
||||
@ -275,6 +284,9 @@ public interface DebuggerResources {
|
||||
Color DEFAULT_COLOR_REGISTER_MARKERS = new Color(0.75f, 0.875f, 0.75f);
|
||||
ImageIcon ICON_REGISTER_MARKER = ResourceManager.loadImage("images/register-marker.png");
|
||||
|
||||
ImageIcon ICON_EVENT_MARKER = ICON_REGISTER_MARKER; // TODO: Another icon?
|
||||
// At least rename to "marker-arrow", and then have both ref it.
|
||||
|
||||
String OPTION_NAME_COLORS_REGISTER_STALE = "Colors.Stale Registers";
|
||||
Color DEFAULT_COLOR_REGISTER_STALE = Color.GRAY;
|
||||
String OPTION_NAME_COLORS_REGISTER_STALE_SEL = "Colors.Stale Registers (selected)";
|
||||
@ -293,6 +305,11 @@ public interface DebuggerResources {
|
||||
String OPTION_NAME_COLORS_WATCH_CHANGED_SEL = "Colors.Changed Watches (selected)";
|
||||
Color DEFAULT_COLOR_WATCH_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f);
|
||||
|
||||
String OPTION_NAME_COLORS_VALUE_CHANGED = "Colors.Changed Values";
|
||||
Color DEFAULT_COLOR_VALUE_CHANGED = Color.RED;
|
||||
String OPTION_NAME_COLORS_VALUE_CHANGED_SEL = "Colors.Changed Values (selected)";
|
||||
Color DEFAULT_COLOR_VALUE_CHANGED_SEL = ColorUtils.blend(Color.RED, Color.WHITE, 0.5f);
|
||||
|
||||
String OPTION_NAME_COLORS_PCODE_COUNTER = "Colors.Pcode Counter";
|
||||
Color DEFAULT_COLOR_PCODE_COUNTER = new Color(0.75f, 0.875f, 0.75f);
|
||||
|
||||
@ -994,12 +1011,12 @@ public interface DebuggerResources {
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateSnapshotAction {
|
||||
String NAME = "Create Snapshot";
|
||||
String DESCRIPTION = "Create a (disconnected) snapshot copy of this window";
|
||||
interface CloneWindowAction {
|
||||
String NAME = "Clone Window";
|
||||
String DESCRIPTION = "Create a disconnected copy of this window";
|
||||
String GROUP = "zzzz";
|
||||
Icon ICON = ResourceManager.loadImage("images/camera-photo.png");
|
||||
String HELP_ANCHOR = "snapshot_window";
|
||||
String HELP_ANCHOR = "clone_window";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
@ -1624,14 +1641,31 @@ public interface DebuggerResources {
|
||||
}
|
||||
}
|
||||
|
||||
interface StepSnapForwardAction {
|
||||
String NAME = "Step Trace Snap Forward";
|
||||
String DESCRIPTION = "Navigate the recording forward one snap";
|
||||
Icon ICON = ICON_SNAP_FORWARD;
|
||||
String GROUP = GROUP_CONTROL;
|
||||
String HELP_ANCHOR = "step_trace_snap_forward";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, "4")
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractStepSnapForwardAction extends DockingAction {
|
||||
public static final String NAME = "Step Trace Snap Forward";
|
||||
public static final Icon ICON = ICON_SNAP_FORWARD;
|
||||
public static final String HELP_ANCHOR = "step_trace_snap_forward";
|
||||
public static final String NAME = StepSnapForwardAction.NAME;
|
||||
public static final Icon ICON = StepSnapForwardAction.ICON;
|
||||
public static final String HELP_ANCHOR = StepSnapForwardAction.HELP_ANCHOR;
|
||||
|
||||
public AbstractStepSnapForwardAction(Plugin owner) {
|
||||
super(NAME, owner.getName());
|
||||
setDescription("Navigate the recording forward one snap");
|
||||
setDescription(StepSnapForwardAction.DESCRIPTION);
|
||||
setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
@ -1677,14 +1711,31 @@ public interface DebuggerResources {
|
||||
}
|
||||
}
|
||||
|
||||
interface StepSnapBackwardAction {
|
||||
String NAME = "Step Trace Snap Backward";
|
||||
String DESCRIPTION = "Navigate the recording backward one snap";
|
||||
Icon ICON = ICON_SNAP_BACKWARD;
|
||||
String GROUP = GROUP_CONTROL;
|
||||
String HELP_ANCHOR = "step_trace_snap_backward";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarIcon(ICON)
|
||||
.toolBarGroup(GROUP, "1")
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractStepSnapBackwardAction extends DockingAction {
|
||||
public static final String NAME = "Step Trace Snap Backward";
|
||||
public static final Icon ICON = ICON_SNAP_BACKWARD;
|
||||
public static final String HELP_ANCHOR = "step_trace_snap_backward";
|
||||
public static final String NAME = StepSnapBackwardAction.NAME;
|
||||
public static final Icon ICON = StepSnapBackwardAction.ICON;;
|
||||
public static final String HELP_ANCHOR = StepSnapBackwardAction.HELP_ANCHOR;
|
||||
|
||||
public AbstractStepSnapBackwardAction(Plugin owner) {
|
||||
super(NAME, owner.getName());
|
||||
setDescription("Navigate the recording backward one snap");
|
||||
setDescription(StepSnapBackwardAction.DESCRIPTION);
|
||||
setHelpLocation(new HelpLocation(owner.getName(), HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
@ -2107,6 +2158,87 @@ public interface DebuggerResources {
|
||||
}
|
||||
}
|
||||
|
||||
interface LimitToCurrentSnapAction {
|
||||
String NAME = "Limit to Current Snap";
|
||||
String DESCRIPTION = "Choose whether displayed objects must be alive at the current snap";
|
||||
String GROUP = GROUP_GENERAL;
|
||||
Icon ICON = ICON_TIME; // TODO
|
||||
String HELP_ANCHOR = "limit_to_current_snap";
|
||||
|
||||
static ToggleActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ToggleActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.toolBarGroup(GROUP)
|
||||
.toolBarIcon(ICON)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface ShowHiddenAction {
|
||||
String NAME = "Show Hidden";
|
||||
String DESCRIPTION = "Choose whether to display hidden children";
|
||||
String GROUP = GROUP_GENERAL;
|
||||
String HELP_ANCHOR = "show_hidden";
|
||||
|
||||
static ToggleActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ToggleActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.menuPath(NAME)
|
||||
.menuGroup(GROUP)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface ShowPrimitivesInTreeAction {
|
||||
String NAME = "Show Primitives in Tree";
|
||||
String DESCRIPTION = "Choose whether to display primitive values in the tree";
|
||||
String GROUP = GROUP_GENERAL;
|
||||
String HELP_ANCHOR = "show_primitives";
|
||||
|
||||
static ToggleActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ToggleActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.menuPath(NAME)
|
||||
.menuGroup(GROUP)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface ShowMethodsInTreeAction {
|
||||
String NAME = "Show Methods in Tree";
|
||||
String DESCRIPTION = "Choose whether to display methods in the tree";
|
||||
String GROUP = GROUP_GENERAL;
|
||||
String HELP_ANCHOR = "show_methods";
|
||||
|
||||
static ToggleActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ToggleActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.menuPath(NAME)
|
||||
.menuGroup(GROUP)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
interface FollowLinkAction {
|
||||
String NAME = "Follow Link";
|
||||
String DESCRIPTION = "Navigate to the link target";
|
||||
String GROUP = GROUP_GENERAL;
|
||||
String HELP_ANCHOR = "follow_link";
|
||||
|
||||
static ActionBuilder builder(Plugin owner) {
|
||||
String ownerName = owner.getName();
|
||||
return new ActionBuilder(NAME, ownerName)
|
||||
.description(DESCRIPTION)
|
||||
.popupMenuPath(NAME)
|
||||
.popupMenuGroup(GROUP)
|
||||
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AbstractDebuggerConnectionsNode extends GTreeNode {
|
||||
@Override
|
||||
public String getName() {
|
||||
|
@ -0,0 +1,115 @@
|
||||
/* ###
|
||||
* 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;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jdom.Element;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.MultiProviderSaveBehavior.SaveableProvider;
|
||||
import ghidra.framework.options.SaveState;
|
||||
|
||||
public abstract class MultiProviderSaveBehavior<P extends SaveableProvider> {
|
||||
private static final String KEY_CONNECTED_PROVIDER = "connectedProvider";
|
||||
private static final String KEY_DISCONNECTED_COUNT = "disconnectedCount";
|
||||
private static final String PREFIX_DISCONNECTED_PROVIDER = "disconnectedProvider";
|
||||
|
||||
public interface SaveableProvider {
|
||||
void writeConfigState(SaveState saveState);
|
||||
|
||||
void readConfigState(SaveState saveState);
|
||||
|
||||
void writeDataState(SaveState saveState);
|
||||
|
||||
void readDataState(SaveState saveState);
|
||||
}
|
||||
|
||||
protected abstract P getConnectedProvider();
|
||||
|
||||
protected abstract List<P> 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<P> disconnectedProviders = getDisconnectedProviders();
|
||||
List<P> 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<P> 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);
|
||||
}
|
||||
}
|
@ -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<T> extends ThreadedTableModel<T, Trace>
|
||||
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<Long> oldSpan,
|
||||
Range<Long> 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<Long> 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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <p>
|
||||
* 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<Long> span) {
|
||||
if (Objects.equals(this.span, span)) {
|
||||
return;
|
||||
}
|
||||
this.span = span;
|
||||
|
||||
spanChanged();
|
||||
}
|
||||
|
||||
public Range<Long> 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<T> streamRows(Trace trace, ModelQuery query, Range<Long> span);
|
||||
|
||||
@Override
|
||||
protected void doLoad(Accumulator<T> accumulator, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
if (trace == null || query == null || trace.getObjectManager().getRootSchema() == null) {
|
||||
return;
|
||||
}
|
||||
for (T t : (Iterable<T>) 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);
|
||||
}
|
@ -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<T> extends JPanel {
|
||||
|
||||
protected final AbstractQueryTableModel<T> tableModel;
|
||||
protected final GhidraTable table;
|
||||
private final GhidraTableFilterPanel<T> 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<T> 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<T> 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);
|
||||
}
|
||||
}
|
@ -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<P extends JComponent> {
|
||||
|
||||
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<JTable> {
|
||||
@Override
|
||||
default Color getForeground(JTable table) {
|
||||
return table.getForeground();
|
||||
}
|
||||
|
||||
@Override
|
||||
default Color getSelForeground(JTable table) {
|
||||
return table.getSelectionForeground();
|
||||
}
|
||||
}
|
||||
|
||||
interface InTree extends ColorsModified<JTree>, TreeCellRenderer {
|
||||
|
||||
Color getTextNonSelectionColor();
|
||||
|
||||
Color getTextSelectionColor();
|
||||
|
||||
@Override
|
||||
default Color getForeground(JTree tree) {
|
||||
return getTextNonSelectionColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
default Color getSelForeground(JTree tree) {
|
||||
return getTextSelectionColor();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<DebuggerModelProvider> {
|
||||
@Override
|
||||
protected DebuggerModelProvider getConnectedProvider() {
|
||||
return connectedProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<DebuggerModelProvider> 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<DebuggerModelProvider> 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<DebuggerModelProvider> 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);
|
||||
}
|
||||
}
|
@ -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<DebuggerModelProvider> 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<AbstractNode> 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<ValueRow> 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<PathRow> 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<TraceObjectValue> 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<TraceObjectValue> 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, "")));
|
||||
}
|
||||
}
|
@ -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<TraceObjectValue> objectValues;
|
||||
|
||||
public DebuggerObjectActionContext(Collection<TraceObjectValue> objectValues,
|
||||
ComponentProvider provider, Component sourceComponent) {
|
||||
super(provider, sourceComponent);
|
||||
this.objectValues = List.copyOf(objectValues);
|
||||
}
|
||||
|
||||
public List<TraceObjectValue> getObjectValues() {
|
||||
return objectValues;
|
||||
}
|
||||
}
|
@ -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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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 "<root>";
|
||||
}
|
||||
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 "<html>" + HTMLUtilities.escapeHTML(getPrimitiveValueDisplay(edge.getValue()));
|
||||
}
|
||||
if (edge.isCanonical()) {
|
||||
return "<html>" + HTMLUtilities.escapeHTML(getObjectDisplay(edge));
|
||||
}
|
||||
return "<html><em>" + HTMLUtilities.escapeHTML(getObjectLinkDisplay(edge)) + "</em>";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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 "<ModelQuery: " + predicates.toString() + ">";
|
||||
}
|
||||
|
||||
@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<TraceObject> streamObjects(Trace trace, Range<Long> 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<TraceObjectValue> streamValues(Trace trace, Range<Long> 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<TraceObjectValPath> streamPaths(Trace trace, Range<Long> span) {
|
||||
return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the named attributes for resulting objects, according to the schema
|
||||
*
|
||||
* <p>
|
||||
* This does not include the "default attribute schema."
|
||||
*
|
||||
* @param trace the data source
|
||||
* @return the list of attributes
|
||||
*/
|
||||
public Stream<AttributeSchema> 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
|
||||
*
|
||||
* <p>
|
||||
* 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<Long> span, TraceObjectValue value) {
|
||||
List<String> 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());
|
||||
}
|
||||
}
|
@ -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<ValueRow> {
|
||||
/** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
|
||||
private TraceValueValColumn valueColumn;
|
||||
private TraceValueLifePlotColumn lifePlotColumn;
|
||||
|
||||
protected static Stream<? extends TraceObjectValue> distinctCanonical(
|
||||
Stream<? extends TraceObjectValue> stream) {
|
||||
Set<TraceObject> seen = new HashSet<>();
|
||||
return stream.filter(value -> {
|
||||
if (!value.isCanonical()) {
|
||||
return true;
|
||||
}
|
||||
return seen.add(value.getChild());
|
||||
});
|
||||
}
|
||||
|
||||
public interface ValueRow {
|
||||
String getKey();
|
||||
|
||||
RangeSet<Long> 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<Long> getLife() {
|
||||
RangeSet<Long> 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 "<html>" +
|
||||
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<ColKey, TraceValueObjectAttributeColumn> 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<Long> fullRange = Range.closed(0L, max == null ? 1 : max + 1);
|
||||
lifePlotColumn.setFullRange(fullRange);
|
||||
}
|
||||
|
||||
protected List<AttributeSchema> 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<AttributeSchema> 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<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns(
|
||||
Collection<AttributeSchema> 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<AttributeSchema> attributes) {
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> columns =
|
||||
new HashSet<>(computeAttributeColumns(attributes));
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> toRemove = new HashSet<>();
|
||||
for (int i = 0; i < getColumnCount(); i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> exists = getColumn(i);
|
||||
if (!(exists instanceof TraceValueObjectAttributeColumn)) {
|
||||
continue;
|
||||
}
|
||||
if (!columns.remove(exists)) {
|
||||
toRemove.add(exists);
|
||||
}
|
||||
}
|
||||
removeTableColumns(toRemove);
|
||||
addTableColumns(columns);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<ValueRow> streamRows(Trace trace, ModelQuery query, Range<Long> span) {
|
||||
return distinctCanonical(query.streamValues(trace, span)
|
||||
.filter(v -> isShowHidden() || !v.isHidden()))
|
||||
.map(this::rowForValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<ValueRow> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Class<? extends TargetObject>> 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<Long> oldSpan,
|
||||
Range<Long> 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<IDKeyed<TraceObjectValue>, AbstractNode> byValue = new WeakValueHashMap<>();
|
||||
Map<IDKeyed<TraceObject>, 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 "<html><em>No trace is active</em>";
|
||||
}
|
||||
TraceObject root = trace.getObjectManager().getRootObject();
|
||||
if (root == null) {
|
||||
return "<html><em>Trace has no model</em>";
|
||||
}
|
||||
return "<html>" +
|
||||
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<GTreeNode> 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<GTreeNode> generateChildren() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
String html = HTMLUtilities.escapeHTML(
|
||||
value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue()));
|
||||
return "<html>" + 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 "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" +
|
||||
HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolTip() {
|
||||
return display.getObjectLinkToolTip(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<GTreeNode> 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<GTreeNode> generateChildren() {
|
||||
return generateObjectChildren(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "<html>" + 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<Long> 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<String, Icon> 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<String, Icon> fillIconMap(Map<String, Icon> 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<GTreeNode> generateObjectChildren(TraceObject object) {
|
||||
List<GTreeNode> 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
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
*
|
||||
* <p>
|
||||
* 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<Long> span) {
|
||||
if (Objects.equals(this.span, span)) {
|
||||
return;
|
||||
}
|
||||
this.span = span;
|
||||
spanChanged();
|
||||
}
|
||||
|
||||
public Range<Long> 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);
|
||||
}
|
||||
}
|
@ -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<ValueRow> {
|
||||
public ObjectsTablePanel(Plugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractQueryTableModel<ValueRow> createModel(Plugin plugin) {
|
||||
return new ObjectTableModel(plugin);
|
||||
}
|
||||
}
|
@ -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, A> 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<AbstractNode> 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<TraceObjectKeyPath> keyPaths) {
|
||||
List<GTreeNode> nodes = new ArrayList<>();
|
||||
for (TraceObjectKeyPath path : keyPaths) {
|
||||
AbstractNode node = getNode(path);
|
||||
if (node != null) {
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
tree.setSelectedNodes(nodes);
|
||||
}
|
||||
}
|
@ -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<PathRow> {
|
||||
/** Initialized in {@link #createTableColumnDescriptor()}, which precedes this. */
|
||||
private TracePathValueColumn valueColumn;
|
||||
private TracePathLastLifespanPlotColumn lifespanPlotColumn;
|
||||
|
||||
protected static Stream<? extends TraceObjectValPath> distinctKeyPath(
|
||||
Stream<? extends TraceObjectValPath> stream) {
|
||||
Set<List<String>> 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<Long> 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<PathRow> streamRows(Trace trace, ModelQuery query, Range<Long> 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<PathRow> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<PathRow> 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);
|
||||
}
|
||||
}
|
@ -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<PathRow> {
|
||||
public PathsTablePanel(Plugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractQueryTableModel<PathRow> createModel(Plugin plugin) {
|
||||
return new PathTableModel(plugin);
|
||||
}
|
||||
}
|
@ -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<PathRow, String, Trace> {
|
||||
@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 "<root>";
|
||||
}
|
||||
return lastEntry.getEntryKey();
|
||||
}
|
||||
}
|
@ -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<PathRow, Range<Long>, Trace> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Life";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<Long> 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();
|
||||
}
|
||||
}
|
@ -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<PathRow, Range<Long>, Trace> {
|
||||
|
||||
private final RangeTableCellRenderer<Long> cellRenderer = new RangeTableCellRenderer<>();
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Plot";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<Long> 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<Range<Long>> getColumnRenderer() {
|
||||
return cellRenderer;
|
||||
}
|
||||
|
||||
// TODO: header renderer
|
||||
|
||||
public void setFullRange(Range<Long> fullRange) {
|
||||
cellRenderer.setFullRange(fullRange);
|
||||
// TODO: header, too
|
||||
}
|
||||
}
|
@ -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<PathRow, String, Trace> {
|
||||
@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());
|
||||
}
|
||||
}
|
@ -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<PathRow, PathRow, Trace> {
|
||||
private final class ValueRenderer extends AbstractGColumnRenderer<PathRow>
|
||||
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<PathRow> getColumnRenderer() {
|
||||
return new ValueRenderer();
|
||||
}
|
||||
|
||||
public void setDiffColor(Color diffColor) {
|
||||
this.diffColor = diffColor;
|
||||
}
|
||||
|
||||
public void setDiffColorSel(Color diffColorSel) {
|
||||
this.diffColorSel = diffColorSel;
|
||||
}
|
||||
}
|
@ -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<ValueRow, String, Trace> {
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Key";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ValueRow rowObject, Settings settings, Trace data,
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
return rowObject.getKey();
|
||||
}
|
||||
}
|
@ -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<ValueRow, RangeSet<Long>, Trace> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Life";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RangeSet<Long> getValue(ValueRow rowObject, Settings settings, Trace data,
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
return rowObject.getLife();
|
||||
}
|
||||
}
|
@ -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<ValueRow, RangeSet<Long>, Trace> {
|
||||
|
||||
private final RangeSetTableCellRenderer<Long> cellRenderer = new RangeSetTableCellRenderer<>();
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Plot";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RangeSet<Long> getValue(ValueRow rowObject, Settings settings, Trace data,
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
return rowObject.getLife();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GColumnRenderer<RangeSet<Long>> getColumnRenderer() {
|
||||
return cellRenderer;
|
||||
}
|
||||
|
||||
// TODO: The header renderer
|
||||
|
||||
public void setFullRange(Range<Long> fullRange) {
|
||||
cellRenderer.setFullRange(fullRange);
|
||||
// TODO: set header's full range, too
|
||||
}
|
||||
}
|
@ -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<ValueRow, ValueRow, Trace> {
|
||||
|
||||
public class AttributeRenderer extends AbstractGColumnRenderer<ValueRow>
|
||||
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<ValueRow> 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<ValueRow> getColumnRenderer() {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<ValueRow> 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 <C extends Comparable<C>> Comparator<ValueRow> newTypedComparator() {
|
||||
if (Comparable.class.isAssignableFrom(attributeType)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<C> cls = (Class<C>) attributeType.asSubclass(Comparable.class);
|
||||
Function<ValueRow, C> 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;
|
||||
}
|
||||
}
|
@ -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<ValueRow, ValueRow, Trace> {
|
||||
private final class ValRenderer extends AbstractGColumnRenderer<ValueRow>
|
||||
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<ValueRow> getColumnRenderer() {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<ValueRow> getComparator(DynamicColumnTableModel<?> model, int columnIndex) {
|
||||
return getComparator()
|
||||
.thenComparing(new ColumnRenderedValueBackupComparator<>(model, columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Comparator<ValueRow> 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<Object>) v1).compareTo(v2);
|
||||
}
|
||||
}
|
||||
return 0; // Defer to backup comparator
|
||||
};
|
||||
}
|
||||
|
||||
public void setDiffColor(Color diffColor) {
|
||||
this.diffColor = diffColor;
|
||||
}
|
||||
|
||||
public void setDiffColorSel(Color diffColorSel) {
|
||||
this.diffColorSel = diffColorSel;
|
||||
}
|
||||
}
|
@ -608,9 +608,9 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
|
||||
.onAction(c -> selectRegistersActivated())
|
||||
.buildAndInstallLocal(this);
|
||||
if (!isClone) {
|
||||
actionCreateSnapshot = DebuggerResources.CreateSnapshotAction.builder(plugin)
|
||||
actionCreateSnapshot = DebuggerResources.CloneWindowAction.builder(plugin)
|
||||
.enabledWhen(c -> current.getThread() != null)
|
||||
.onAction(c -> createSnapshotActivated())
|
||||
.onAction(c -> cloneWindowActivated())
|
||||
.buildAndInstallLocal(this);
|
||||
}
|
||||
actionEnableEdits = DebuggerResources.EnableEditsAction.builder(plugin)
|
||||
@ -639,7 +639,7 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
|
||||
tool.showDialog(availableRegsDialog);
|
||||
}
|
||||
|
||||
private void createSnapshotActivated() {
|
||||
private void cloneWindowActivated() {
|
||||
DebuggerRegistersProvider clone = cloneAsDisconnected();
|
||||
clone.setIntraGroupPosition(WindowPosition.RIGHT);
|
||||
tool.showComponentProvider(clone, true);
|
||||
@ -961,8 +961,14 @@ public class DebuggerRegistersProvider extends ComponentProviderAdapter
|
||||
public static LinkedHashSet<Register> collectCommonRegisters(CompilerSpec cSpec) {
|
||||
Language lang = cSpec.getLanguage();
|
||||
LinkedHashSet<Register> result = new LinkedHashSet<>();
|
||||
result.add(cSpec.getStackPointer());
|
||||
result.add(lang.getProgramCounter());
|
||||
Register sp = cSpec.getStackPointer();
|
||||
if (sp != null) {
|
||||
result.add(sp);
|
||||
}
|
||||
Register pc = lang.getProgramCounter();
|
||||
if (pc != null) {
|
||||
result.add(pc);
|
||||
}
|
||||
for (Register reg : lang.getRegisters()) {
|
||||
//if (reg.getGroup() != null) {
|
||||
// continue;
|
||||
|
@ -392,6 +392,9 @@ public class ObjectBasedTraceRecorder implements TraceRecorder {
|
||||
|
||||
@Override
|
||||
public TargetThread getTargetThread(TraceThread thread) {
|
||||
if (thread == null) {
|
||||
return null;
|
||||
}
|
||||
return objectRecorder.getTargetInterface(thread, TraceObjectThread.class,
|
||||
TargetThread.class);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.dbg.DebuggerObjectModel;
|
||||
import ghidra.dbg.target.TargetAttacher.TargetAttachKind;
|
||||
import ghidra.dbg.target.TargetAttacher.TargetAttachKindSet;
|
||||
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
|
||||
@ -75,6 +76,23 @@ class ObjectRecorder {
|
||||
return targetObject == null ? null : targetObject.obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the names of interfaces on the object not already covered by the schema
|
||||
*
|
||||
* @param object the object
|
||||
* @return the comma-separated list of interface names
|
||||
*/
|
||||
protected String computeExtraInterfaces(TargetObject object) {
|
||||
Set<String> result = new LinkedHashSet<>(object.getInterfaceNames());
|
||||
for (Class<? extends TargetObject> iface : object.getSchema().getInterfaces()) {
|
||||
result.remove(DebuggerObjectModel.requireIfaceName(iface));
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return result.stream().collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
protected void recordCreated(long snap, TargetObject object) {
|
||||
TraceObject traceObject;
|
||||
if (object.isRoot()) {
|
||||
@ -91,6 +109,10 @@ class ObjectRecorder {
|
||||
Msg.error(this, "Received created for an object that already exists: " + exists);
|
||||
}
|
||||
}
|
||||
String extras = computeExtraInterfaces(object);
|
||||
// Note: null extras will erase previous value, if necessary.
|
||||
traceObject.setAttribute(Range.atLeast(snap),
|
||||
TraceObject.EXTRA_INTERFACES_ATTRIBUTE_NAME, extras);
|
||||
}
|
||||
|
||||
protected void recordInvalidated(long snap, TargetObject object) {
|
||||
|
@ -0,0 +1,766 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jdom.JDOMException;
|
||||
import org.junit.*;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import docking.widgets.table.DynamicTableColumn;
|
||||
import docking.widgets.table.GDynamicColumnTableModel;
|
||||
import generic.Unique;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
|
||||
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.PrimitiveRow;
|
||||
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.plugin.core.debug.gui.model.columns.TraceValueValColumn;
|
||||
import ghidra.dbg.target.TargetEventScope;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.SchemaContext;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.trace.database.target.DBTraceObject;
|
||||
import ghidra.trace.database.target.DBTraceObjectManager;
|
||||
import ghidra.trace.model.target.*;
|
||||
import ghidra.trace.model.target.TraceObject.ConflictResolution;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
|
||||
public class DebuggerModelProviderTest extends AbstractGhidraHeadedDebuggerGUITest {
|
||||
|
||||
protected static final SchemaContext CTX;
|
||||
|
||||
static {
|
||||
try {
|
||||
CTX = XmlSchemaContext.deserialize("" + //
|
||||
"<context>" + //
|
||||
" <schema name='Session' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Processes' schema='ProcessContainer' />" + //
|
||||
" <interface name='EventScope' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ProcessContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element schema='Process' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='Process' elementResync='NEVER' attributeResync='ONCE'>" + //
|
||||
" <attribute name='Threads' schema='ThreadContainer' />" + //
|
||||
" <attribute name='Handles' schema='HandleContainer' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='ThreadContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element schema='Thread' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>" + //
|
||||
" <interface name='Thread' />" + //
|
||||
" <attribute name='_display' schema='STRING' />" + //
|
||||
" <attribute name='_self' schema='Thread' />" + //
|
||||
" </schema>" + //
|
||||
" <schema name='HandleContainer' canonical='yes' elementResync='NEVER' " + //
|
||||
" attributeResync='ONCE'>" + //
|
||||
" <element schema='INT' />" + //
|
||||
" </schema>" + //
|
||||
"</context>");
|
||||
}
|
||||
catch (JDOMException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
protected static Integer findColumnOfClass(GDynamicColumnTableModel<?, ?> model,
|
||||
Class<? extends DynamicTableColumn<?, ?, ?>> cls) {
|
||||
for (int i = 0; i < model.getColumnCount(); i++) {
|
||||
DynamicTableColumn<?, ?, ?> column = model.getColumn(i);
|
||||
if (cls.isAssignableFrom(column.getClass())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected DebuggerModelPlugin modelPlugin;
|
||||
protected DebuggerModelProvider modelProvider;
|
||||
|
||||
@Before
|
||||
public void setUpModelProviderTest() throws Exception {
|
||||
modelPlugin = addPlugin(tool, DebuggerModelPlugin.class);
|
||||
modelProvider = waitForComponentProvider(DebuggerModelProvider.class);
|
||||
|
||||
// So I can manipulate the coordinates
|
||||
//addPlugin(tool, DebuggerThreadsPlugin.class);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDownModelProviderTest() throws Exception {
|
||||
traceManager.activate(DebuggerCoordinates.NOWHERE);
|
||||
waitForSwing();
|
||||
waitForCondition(() -> !modelProvider.objectsTreePanel.tree.isBusy());
|
||||
waitForCondition(() -> !modelProvider.elementsTablePanel.tableModel.isBusy());
|
||||
waitForCondition(() -> !modelProvider.attributesTablePanel.tableModel.isBusy());
|
||||
runSwing(() -> traceManager.closeAllTraces());
|
||||
}
|
||||
|
||||
protected void populateSnapshots() throws Throwable {
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
tb.trace.getTimeManager().getSnapshot(20, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected TraceObjectValue createSessionObject() throws Throwable {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
return objects.createRootObject(CTX.getSchema(new SchemaName("Session")));
|
||||
}
|
||||
}
|
||||
|
||||
protected DBTraceObject createThread(long i, DBTraceObject prevThread) {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
TraceObjectKeyPath threadContainerPath = TraceObjectKeyPath.parse("Processes[0].Threads");
|
||||
DBTraceObject thread = objects.createObject(threadContainerPath.index(i));
|
||||
thread.insert(Range.closed(i, 10L), ConflictResolution.DENY);
|
||||
thread.insert(Range.atLeast(10 + i), ConflictResolution.DENY);
|
||||
thread.setAttribute(Range.atLeast(i), "Attribute " + i, "Some value");
|
||||
thread.setAttribute(Range.atLeast(i), "_display", "Thread " + i);
|
||||
thread.setAttribute(Range.atLeast(i), "_self", thread);
|
||||
if (prevThread != null) {
|
||||
thread.setAttribute(Range.atLeast(i), "_prev", prevThread);
|
||||
prevThread.setAttribute(Range.atLeast(i), "_next", thread);
|
||||
}
|
||||
objects.getRootObject()
|
||||
.setAttribute(Range.atLeast(i), TargetEventScope.EVENT_OBJECT_ATTRIBUTE_NAME,
|
||||
thread);
|
||||
return thread;
|
||||
}
|
||||
|
||||
protected void populateThreads() throws Throwable {
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject prevThread = null;
|
||||
for (long i = 0; i < 10; i++) {
|
||||
DBTraceObject thread = createThread(i, prevThread);
|
||||
prevThread = thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void addThread10() throws Throwable {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
createThread(10, objects.getObjectByCanonicalPath(
|
||||
TraceObjectKeyPath.parse("Processes[0].Threads[9]")));
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateHandles() throws Throwable {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject handleContainer =
|
||||
objects.createObject(TraceObjectKeyPath.parse("Processes[0].Handles"));
|
||||
handleContainer.insert(Range.atLeast(0L), ConflictResolution.DENY);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
handleContainer.setElement(Range.atLeast((long) -i), i,
|
||||
(i * 0xdeadbeef) % 0xbadc0de);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateLinks() throws Throwable {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
TraceObjectKeyPath threadContainerPath = TraceObjectKeyPath.parse("Processes[0].Threads");
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject linkContainer =
|
||||
objects.createObject(TraceObjectKeyPath.parse("Processes[0].Links"));
|
||||
linkContainer.insert(Range.atLeast(0L), ConflictResolution.DENY);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
linkContainer.setElement(Range.atLeast(0L), i,
|
||||
objects.getObjectByCanonicalPath(threadContainerPath.index(9 - i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void populateBoxedPrimitive() throws Throwable {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
TraceObject boxed =
|
||||
objects.createObject(TraceObjectKeyPath.parse("Processes[0].Boxed"));
|
||||
boxed.insert(Range.atLeast(0L), ConflictResolution.DENY);
|
||||
boxed.setAttribute(Range.atLeast(2L), TargetObject.DISPLAY_ATTRIBUTE_NAME, "2");
|
||||
boxed.setAttribute(Range.atLeast(4L), TargetObject.DISPLAY_ATTRIBUTE_NAME, "4");
|
||||
}
|
||||
}
|
||||
|
||||
protected void createTraceAndPopulateObjects() throws Throwable {
|
||||
createTrace();
|
||||
populateSnapshots();
|
||||
createSessionObject();
|
||||
populateThreads();
|
||||
populateHandles();
|
||||
populateLinks();
|
||||
populateBoxedPrimitive();
|
||||
}
|
||||
|
||||
protected void assertPathIs(TraceObjectKeyPath path, int elemCount, int attrCount) {
|
||||
assertEquals(path, modelProvider.getPath());
|
||||
assertEquals(path.toString(), modelProvider.pathField.getText());
|
||||
AbstractNode item = modelProvider.objectsTreePanel.getSelectedItem();
|
||||
assertNotNull(item);
|
||||
assertEquals(path, item.getValue().getChild().getCanonicalPath());
|
||||
// Table model is threaded
|
||||
waitForPass(() -> assertEquals(elemCount,
|
||||
modelProvider.elementsTablePanel.tableModel.getModelData().size()));
|
||||
waitForPass(() -> assertEquals(attrCount,
|
||||
modelProvider.attributesTablePanel.tableModel.getModelData().size()));
|
||||
}
|
||||
|
||||
protected void assertPathIsThreadsContainer() {
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPathWOutTrace() throws Throwable {
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse(""));
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse(""));
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRootWOutTrace() throws Throwable {
|
||||
modelProvider.objectsTreePanel.setSelectedKeyPaths(Set.of(TraceObjectKeyPath.parse("")));
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectRootWOutObjects() throws Throwable {
|
||||
createTrace();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
|
||||
modelProvider.objectsTreePanel.setSelectedKeyPaths(Set.of(TraceObjectKeyPath.parse("")));
|
||||
waitForSwing();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPathApi() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPathViaField() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.pathField.setText("Processes[0].Threads");
|
||||
modelProvider.pathField.getInputVerifier().verify(modelProvider.pathField);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPathViaTree() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.objectsTreePanel
|
||||
.setSelectedKeyPaths(List.of(TraceObjectKeyPath.parse("Processes[0].Threads")));
|
||||
waitForSwing();
|
||||
|
||||
waitForPass(() -> assertPathIsThreadsContainer());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectElementDisplaysAttributes() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
|
||||
ValueRow selElem = waitForValue(() -> {
|
||||
List<ValueRow> rows = modelProvider.elementsTablePanel.tableModel.getModelData();
|
||||
if (rows.size() != 10) {
|
||||
return null;
|
||||
}
|
||||
return rows.get(2);
|
||||
});
|
||||
modelProvider.elementsTablePanel.setSelectedItem(selElem);
|
||||
waitForSwing();
|
||||
|
||||
waitForPass(() -> assertEquals(3,
|
||||
modelProvider.attributesTablePanel.tableModel.getModelData().size()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPathNoExist() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].NoSuch"));
|
||||
waitForSwing();
|
||||
|
||||
assertEquals("No such object at path Processes[0].NoSuch", tool.getStatusInfo());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrimitiveElements() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Handles"));
|
||||
waitForSwing();
|
||||
|
||||
int valColIndex =
|
||||
waitForValue(() -> findColumnOfClass(modelProvider.elementsTablePanel.tableModel,
|
||||
TraceValueValColumn.class));
|
||||
|
||||
waitForPass(() -> {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Object obj = modelProvider.elementsTablePanel.tableModel.getValueAt(i, valColIndex);
|
||||
assertTrue(obj instanceof PrimitiveRow);
|
||||
PrimitiveRow row = (PrimitiveRow) obj;
|
||||
assertEquals(Integer.toString((0xdeadbeef * i) % 0xbadc0de), row.getDisplay());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelEditPath() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
|
||||
modelProvider.pathField.setText("SomeNonsenseToBeCancelled");
|
||||
triggerEscapeKey(modelProvider.pathField);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleClickLinkInElementsTable() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Links"));
|
||||
waitForSwing();
|
||||
|
||||
ValueRow row2 = waitForValue(() -> {
|
||||
return modelProvider.elementsTablePanel.tableModel.getModelData()
|
||||
.stream()
|
||||
.filter(r -> r.getValue().getEntryKey().equals("[2]"))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
});
|
||||
modelProvider.elementsTablePanel.setSelectedItem(row2);
|
||||
waitForSwing();
|
||||
int rowIndex = waitForValue(() -> {
|
||||
int index = modelProvider.elementsTablePanel.table.getSelectedRow();
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
clickTableCell(modelProvider.elementsTablePanel.table, rowIndex, 0, 2);
|
||||
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[7]"), 0, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleClickObjectInElementsTable() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
|
||||
ValueRow row2 = waitForValue(() -> {
|
||||
return modelProvider.elementsTablePanel.tableModel.getModelData()
|
||||
.stream()
|
||||
.filter(r -> r.getValue().getEntryKey().equals("[2]"))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
});
|
||||
modelProvider.elementsTablePanel.setSelectedItem(row2);
|
||||
waitForSwing();
|
||||
int rowIndex = waitForValue(() -> {
|
||||
int index = modelProvider.elementsTablePanel.table.getSelectedRow();
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
clickTableCell(modelProvider.elementsTablePanel.table, rowIndex, 0, 2);
|
||||
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[2]"), 0, 3);
|
||||
}
|
||||
|
||||
protected void selectAttribute(String key) {
|
||||
PathRow rowNext = waitForValue(() -> {
|
||||
return modelProvider.attributesTablePanel.tableModel.getModelData()
|
||||
.stream()
|
||||
.filter(r -> {
|
||||
TraceObjectValue last = r.getPath().getLastEntry();
|
||||
if (last == null) {
|
||||
return false;
|
||||
}
|
||||
return last.getEntryKey().equals(key);
|
||||
})
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
});
|
||||
modelProvider.attributesTablePanel.setSelectedItem(rowNext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleClickLinkInAttributesTable() throws Throwable {
|
||||
modelProvider.setShowHidden(true);
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]"));
|
||||
waitForSwing();
|
||||
selectAttribute("_next");
|
||||
waitForSwing();
|
||||
|
||||
int rowIndex = waitForValue(() -> {
|
||||
int index = modelProvider.attributesTablePanel.table.getSelectedRow();
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
clickTableCell(modelProvider.attributesTablePanel.table, rowIndex, 0, 2);
|
||||
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[3]"), 0, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleClickObjectInAttributesTable() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0]"));
|
||||
waitForSwing();
|
||||
|
||||
PathRow rowNext = waitForValue(() -> {
|
||||
return modelProvider.attributesTablePanel.tableModel.getModelData()
|
||||
.stream()
|
||||
.filter(r -> {
|
||||
TraceObjectValue last = r.getPath().getLastEntry();
|
||||
if (last == null) {
|
||||
return false;
|
||||
}
|
||||
return last.getEntryKey().equals("Threads");
|
||||
})
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
});
|
||||
modelProvider.attributesTablePanel.setSelectedItem(rowNext);
|
||||
waitForSwing();
|
||||
int rowIndex = waitForValue(() -> {
|
||||
int index = modelProvider.attributesTablePanel.table.getSelectedRow();
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
clickTableCell(modelProvider.attributesTablePanel.table, rowIndex, 0, 2);
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionLimitToSnap() throws Throwable {
|
||||
assertFalse(modelProvider.isLimitToCurrentSnap());
|
||||
assertFalse(modelProvider.actionLimitToCurrentSnap.isSelected());
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads"));
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0);
|
||||
|
||||
performAction(modelProvider.actionLimitToCurrentSnap);
|
||||
assertTrue(modelProvider.isLimitToCurrentSnap());
|
||||
assertTrue(modelProvider.actionLimitToCurrentSnap.isSelected());
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 1, 0);
|
||||
|
||||
traceManager.activateSnap(5);
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 6, 0);
|
||||
|
||||
performAction(modelProvider.actionLimitToCurrentSnap);
|
||||
assertFalse(modelProvider.isLimitToCurrentSnap());
|
||||
assertFalse(modelProvider.actionLimitToCurrentSnap.isSelected());
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads"), 10, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionShowPrimitivesInTree() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
assertFalse(modelProvider.isShowPrimitivesInTree());
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]"));
|
||||
waitForSwing();
|
||||
|
||||
AbstractNode nodeThread2 = modelProvider.objectsTreePanel.getSelectedItem();
|
||||
assertEquals(1, nodeThread2.getChildren().size());
|
||||
|
||||
performAction(modelProvider.actionShowPrimitivesInTree, modelProvider, true);
|
||||
assertTrue(modelProvider.isShowPrimitivesInTree());
|
||||
assertEquals(3, nodeThread2.getChildren().size());
|
||||
assertEquals(nodeThread2, modelProvider.objectsTreePanel.getSelectedItem());
|
||||
|
||||
performAction(modelProvider.actionShowPrimitivesInTree, modelProvider, true);
|
||||
assertFalse(modelProvider.isShowPrimitivesInTree());
|
||||
assertEquals(1, nodeThread2.getChildren().size());
|
||||
assertEquals(nodeThread2, modelProvider.objectsTreePanel.getSelectedItem());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionFollowLink() throws Throwable {
|
||||
modelProvider.setShowHidden(true);
|
||||
assertDisabled(modelProvider, modelProvider.actionFollowLink);
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]"));
|
||||
waitForSwing();
|
||||
selectAttribute("_next");
|
||||
waitForSwing();
|
||||
|
||||
assertEnabled(modelProvider, modelProvider.actionFollowLink);
|
||||
performAction(modelProvider.actionFollowLink, modelProvider, true);
|
||||
|
||||
assertPathIs(TraceObjectKeyPath.parse("Processes[0].Threads[3]"), 0, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActionCloneWindow() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(TraceObjectKeyPath.parse("Processes[0].Threads[2]"));
|
||||
waitForSwing();
|
||||
|
||||
performAction(modelProvider.actionCloneWindow);
|
||||
|
||||
DebuggerModelProvider clone = Unique.assertOne(modelPlugin.getDisconnectedProviders());
|
||||
|
||||
assertEquals(tb.trace, clone.current.getTrace());
|
||||
assertEquals(TraceObjectKeyPath.parse("Processes[0].Threads[2]"), clone.path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackAddElement() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads");
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
|
||||
addThread10();
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 11, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackAddAttribute() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]");
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 3);
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
thread.setAttribute(Range.atLeast(0L), "NewAttribute", 11);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackRemoveElement() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads");
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIsThreadsContainer();
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject threads = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
threads.setElement(Range.all(), 2, null);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 9, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackRemoveAttribute() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]");
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 3);
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
DBTraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
thread.setAttribute(Range.all(), "_self", null);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackLifespanChangedElement() throws Throwable {
|
||||
modelProvider.setLimitToCurrentSnap(true);
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads");
|
||||
TraceObject threads = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
TraceObjectValue element2 = threads.getElement(2, 2);
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
traceManager.activateSnap(2);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 3, 0);
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
element2.setLifespan(Range.atLeast(10L), ConflictResolution.DENY);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 2, 0);
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
element2.setLifespan(Range.atLeast(2L), ConflictResolution.DENY);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 3, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPanesTrackLifespanChangedAttribute() throws Throwable {
|
||||
modelProvider.setLimitToCurrentSnap(true);
|
||||
modelProvider.setShowHidden(true);
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]");
|
||||
TraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
TraceObjectValue attrSelf = thread.getAttribute(2, "_self");
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
traceManager.activateSnap(2);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 4); // _next created at snap 3
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
attrSelf.setLifespan(Range.atLeast(10L), ConflictResolution.DENY);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 3);
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
attrSelf.setLifespan(Range.atLeast(2L), ConflictResolution.DENY);
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
assertPathIs(path, 0, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTreeTracksDisplayChange() throws Throwable {
|
||||
createTraceAndPopulateObjects();
|
||||
TraceObjectKeyPath path = TraceObjectKeyPath.parse("Processes[0].Threads[2]");
|
||||
TraceObject thread = tb.trace.getObjectManager().getObjectByCanonicalPath(path);
|
||||
|
||||
traceManager.activateTrace(tb.trace);
|
||||
waitForSwing();
|
||||
modelProvider.setPath(path);
|
||||
waitForSwing();
|
||||
|
||||
AbstractNode node =
|
||||
waitForValue(() -> modelProvider.objectsTreePanel.treeModel.getNode(path));
|
||||
assertEquals("<html>[2]", node.getDisplayText());
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
thread.setAttribute(Range.atLeast(0L), "_display", "Renamed Thread");
|
||||
}
|
||||
waitForSwing();
|
||||
|
||||
waitForPass(() -> assertEquals("<html>Renamed Thread", node.getDisplayText()));
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/* ###
|
||||
* 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 static ghidra.app.plugin.core.debug.gui.model.DebuggerModelProviderTest.CTX;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerGUITest;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.trace.database.target.DBTraceObjectManager;
|
||||
import ghidra.trace.model.target.TraceObject.ConflictResolution;
|
||||
import ghidra.trace.model.target.TraceObjectKeyPath;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
|
||||
public class ModelQueryTest extends AbstractGhidraHeadedDebuggerGUITest {
|
||||
@Test
|
||||
public void testIncludes() throws Throwable {
|
||||
createTrace();
|
||||
|
||||
ModelQuery rootQuery = ModelQuery.parse("");
|
||||
ModelQuery threadQuery = ModelQuery.parse("Processes[].Threads[]");
|
||||
|
||||
try (UndoableTransaction tid = UndoableTransaction.start(tb.trace, "Init", true)) {
|
||||
DBTraceObjectManager objects = tb.trace.getObjectManager();
|
||||
|
||||
TraceObjectValue rootVal =
|
||||
objects.createRootObject(CTX.getSchema(new SchemaName("Session")));
|
||||
|
||||
TraceObjectValue thread0Val =
|
||||
objects.createObject(TraceObjectKeyPath.parse("Processes[0].Threads[0]"))
|
||||
.insert(Range.atLeast(0L), ConflictResolution.DENY)
|
||||
.getLastEntry();
|
||||
|
||||
assertTrue(rootQuery.includes(Range.all(), rootVal));
|
||||
assertFalse(rootQuery.includes(Range.all(), thread0Val));
|
||||
|
||||
assertFalse(threadQuery.includes(Range.all(), rootVal));
|
||||
assertTrue(threadQuery.includes(Range.all(), thread0Val));
|
||||
assertFalse(threadQuery.includes(Range.lessThan(0L), thread0Val));
|
||||
}
|
||||
}
|
||||
}
|
@ -414,7 +414,8 @@ public interface TargetObjectSchema {
|
||||
*
|
||||
* <p>
|
||||
* If this is the schema of the root object, then this gives the schema of the object at the
|
||||
* given path in the model.
|
||||
* given path in the model. This will always give a non-null result, though that result might be
|
||||
* {@link EnumerableTargetObjectSchema#VOID}.
|
||||
*
|
||||
* @param path the relative path from an object having this schema to the desired successor
|
||||
* @return the schema for the successor
|
||||
|
@ -1,81 +0,0 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.dbg.util;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public enum AllPathsMatcher implements PathPredicates {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public PathPredicates or(PathPredicates that) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(List<String> path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean successorCouldMatch(List<String> path, boolean strict) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ancestorMatches(List<String> path, boolean strict) {
|
||||
if (path.isEmpty() && strict) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNextKeys(List<String> path) {
|
||||
return Set.of("", "[]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNextNames(List<String> path) {
|
||||
return Set.of("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNextIndices(List<String> path) {
|
||||
return Set.of("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSingletonPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathPattern getSingletonPattern() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathPredicates applyKeys(List<String> keys) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -38,6 +38,21 @@ public class PathMatcher implements PathPredicates {
|
||||
return String.format("<PathMatcher\n %s\n>", StringUtils.join(patterns, "\n "));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof PathMatcher)) {
|
||||
return false;
|
||||
}
|
||||
PathMatcher that = (PathMatcher) obj;
|
||||
if (!Objects.equals(this.patterns, that.patterns)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathPredicates or(PathPredicates that) {
|
||||
PathMatcher result = new PathMatcher();
|
||||
@ -82,6 +97,11 @@ public class PathMatcher implements PathPredicates {
|
||||
return anyPattern(p -> p.ancestorMatches(path, strict));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ancestorCouldMatchRight(List<String> path, boolean strict) {
|
||||
return anyPattern(p -> p.ancestorCouldMatchRight(path, strict));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getSingletonPath() {
|
||||
if (patterns.size() != 1) {
|
||||
@ -98,12 +118,7 @@ public class PathMatcher implements PathPredicates {
|
||||
return patterns.iterator().next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNextKeys(List<String> path) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (PathPattern pattern : patterns) {
|
||||
result.addAll(pattern.getNextKeys(path));
|
||||
}
|
||||
protected void coalesceWilds(Set<String> result) {
|
||||
if (result.contains("")) {
|
||||
result.removeIf(PathUtils::isName);
|
||||
result.add("");
|
||||
@ -112,6 +127,15 @@ public class PathMatcher implements PathPredicates {
|
||||
result.removeIf(PathUtils::isIndex);
|
||||
result.add("[]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNextKeys(List<String> path) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (PathPattern pattern : patterns) {
|
||||
result.addAll(pattern.getNextKeys(path));
|
||||
}
|
||||
coalesceWilds(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -139,6 +163,16 @@ public class PathMatcher implements PathPredicates {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPrevKeys(List<String> path) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (PathPattern pattern : patterns) {
|
||||
result.addAll(pattern.getPrevKeys(path));
|
||||
}
|
||||
coalesceWilds(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return patterns.isEmpty();
|
||||
@ -152,4 +186,13 @@ public class PathMatcher implements PathPredicates {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathMatcher removeRight(int count) {
|
||||
PathMatcher result = new PathMatcher();
|
||||
for (PathPattern pat : patterns) {
|
||||
pat.doRemoveRight(count, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -48,13 +48,25 @@ public class PathPattern implements PathPredicates {
|
||||
return String.format("<PathPattern %s>", PathUtils.toString(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this pattern to a string as in {@link PathPredicates#parse(String)}.
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
public String toPatternString() {
|
||||
return PathUtils.toString(pattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof PathPattern)) {
|
||||
return false;
|
||||
}
|
||||
PathPattern that = (PathPattern) obj;
|
||||
return Objects.equals(this.pattern, that.pattern);
|
||||
if (!Objects.equals(this.pattern, that.pattern)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -95,6 +107,17 @@ public class PathPattern implements PathPredicates {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean matchesBackTo(List<String> path, int length) {
|
||||
int patternMax = pattern.size() - 1;
|
||||
int pathMax = path.size() - 1;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!PathPredicates.keyMatches(pattern.get(patternMax - i), path.get(pathMax - i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(List<String> path) {
|
||||
if (path.size() != pattern.size()) {
|
||||
@ -125,6 +148,17 @@ public class PathPattern implements PathPredicates {
|
||||
return matchesUpTo(path, pattern.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ancestorCouldMatchRight(List<String> path, boolean strict) {
|
||||
if (path.size() > pattern.size()) {
|
||||
return false;
|
||||
}
|
||||
if (strict && path.size() == pattern.size()) {
|
||||
return false;
|
||||
}
|
||||
return matchesBackTo(path, path.size());
|
||||
}
|
||||
|
||||
protected static boolean containsWildcards(List<String> pattern) {
|
||||
for (String pat : pattern) {
|
||||
if (isWildcard(pat)) {
|
||||
@ -142,6 +176,20 @@ public class PathPattern implements PathPredicates {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the pattern as a list of key patterns
|
||||
*
|
||||
* @return the list of key patterns
|
||||
*/
|
||||
public List<String> asPath() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of wildcard keys in this pattern
|
||||
*
|
||||
* @return the count
|
||||
*/
|
||||
public int countWildcards() {
|
||||
return (int) pattern.stream().filter(k -> isWildcard(k)).count();
|
||||
}
|
||||
@ -192,6 +240,17 @@ public class PathPattern implements PathPredicates {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPrevKeys(List<String> path) {
|
||||
if (path.size() >= pattern.size()) {
|
||||
return Set.of();
|
||||
}
|
||||
if (!matchesBackTo(path, path.size())) {
|
||||
return Set.of();
|
||||
}
|
||||
return Set.of(pattern.get(pattern.size() - 1 - path.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return false;
|
||||
@ -254,4 +313,18 @@ public class PathPattern implements PathPredicates {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void doRemoveRight(int count, PathMatcher result) {
|
||||
if (count > pattern.size()) {
|
||||
return;
|
||||
}
|
||||
result.addPattern(pattern.subList(0, pattern.size() - count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathMatcher removeRight(int count) {
|
||||
PathMatcher result = new PathMatcher();
|
||||
doRemoveRight(count, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -54,10 +54,6 @@ public interface PathPredicates {
|
||||
return new PathPattern(PathUtils.parse(pattern));
|
||||
}
|
||||
|
||||
static PathPredicates all() {
|
||||
return AllPathsMatcher.INSTANCE;
|
||||
}
|
||||
|
||||
PathPredicates or(PathPredicates that);
|
||||
|
||||
/**
|
||||
@ -95,6 +91,18 @@ public interface PathPredicates {
|
||||
*/
|
||||
boolean ancestorMatches(List<String> path, boolean strict);
|
||||
|
||||
/**
|
||||
* Check if the given path <em>could</em> have a matching ancestor, right to left
|
||||
*
|
||||
* <p>
|
||||
* This essentially checks if the given path is a viable postfix to the matcher.
|
||||
*
|
||||
* @param path the path (postfix) to check
|
||||
* @param strict true to exclude the case where {@link #matches(List)} would return true
|
||||
* @return true if an ancestor could match, false otherwise
|
||||
*/
|
||||
boolean ancestorCouldMatchRight(List<String> path, boolean strict);
|
||||
|
||||
/**
|
||||
* Get the patterns for the next possible key
|
||||
*
|
||||
@ -130,6 +138,17 @@ public interface PathPredicates {
|
||||
*/
|
||||
Set<String> getNextIndices(List<String> path);
|
||||
|
||||
/**
|
||||
* Get the patterns for the previous possible key (right-to-left matching)
|
||||
*
|
||||
* <p>
|
||||
* If an ancestor of the given path cannot match this pattern, the empty set is returned.
|
||||
*
|
||||
* @param path the successor path
|
||||
* @return a set of patterns where indices are enclosed in brackets ({@code [])
|
||||
*/
|
||||
Set<String> getPrevKeys(List<String> path);
|
||||
|
||||
/**
|
||||
* If this predicate is known to match only one path, i.e., no wildcards, get that path
|
||||
*
|
||||
@ -144,6 +163,14 @@ public interface PathPredicates {
|
||||
*/
|
||||
PathPattern getSingletonPattern();
|
||||
|
||||
/**
|
||||
* Remove count elements from the right
|
||||
*
|
||||
* @param count the number of elements to remove
|
||||
* @return the resulting predicates
|
||||
*/
|
||||
PathPredicates removeRight(int count);
|
||||
|
||||
default NavigableMap<List<String>, ?> getCachedValues(TargetObject seed) {
|
||||
return getCachedValues(List.of(), seed);
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
/* ###
|
||||
* 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.dbg.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class PathPredicatesTest {
|
||||
@Test
|
||||
public void testGetPrevKeys() {
|
||||
PathPredicates pred = PathPredicates.parse("Processes[0].Threads[].Stack");
|
||||
|
||||
assertEquals(Set.of("Stack"), pred.getPrevKeys(PathUtils.parse("")));
|
||||
assertEquals(Set.of("[]"), pred.getPrevKeys(PathUtils.parse("Stack")));
|
||||
assertEquals(Set.of("Threads"), pred.getPrevKeys(PathUtils.parse("[].Stack")));
|
||||
assertEquals(Set.of("[0]"), pred.getPrevKeys(PathUtils.parse("Threads[].Stack")));
|
||||
assertEquals(Set.of("Processes"), pred.getPrevKeys(PathUtils.parse("[0].Threads[].Stack")));
|
||||
assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("Processes[0].Threads[].Stack")));
|
||||
|
||||
assertEquals(Set.of(),
|
||||
pred.getPrevKeys(PathUtils.parse("Foo.Processes[0].Threads[].Stack")));
|
||||
assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("Foo")));
|
||||
assertEquals(Set.of(), pred.getPrevKeys(PathUtils.parse("[]")));
|
||||
}
|
||||
}
|
@ -277,7 +277,7 @@ public class DBTraceObjectBreakpointLocation
|
||||
}
|
||||
|
||||
PathMatcher procMatcher = schema.searchFor(TargetProcess.class, false);
|
||||
return object.getAncestors(getLifespan(), procMatcher)
|
||||
return object.getAncestorsRoot(getLifespan(), procMatcher)
|
||||
.flatMap(proc -> proc.getSource(object)
|
||||
.querySuccessorsInterface(getLifespan(),
|
||||
TraceObjectThread.class))
|
||||
|
@ -203,6 +203,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
|
||||
@Override
|
||||
public RangeSet<Long> getLife() {
|
||||
// TODO: This should really be cached
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
RangeSet<Long> result = TreeRangeSet.create();
|
||||
// NOTE: connected ranges should already be coalesced
|
||||
@ -220,19 +221,20 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
return manager.doGetObject(path.parent());
|
||||
}
|
||||
|
||||
protected void doInsert(Range<Long> lifespan, ConflictResolution resolution) {
|
||||
protected DBTraceObjectValPath doInsert(Range<Long> lifespan, ConflictResolution resolution) {
|
||||
if (path.isRoot()) {
|
||||
return;
|
||||
return DBTraceObjectValPath.of();
|
||||
}
|
||||
DBTraceObject parent = doCreateCanonicalParentObject();
|
||||
parent.setValue(lifespan, path.key(), this, resolution);
|
||||
parent.doInsert(lifespan, resolution);
|
||||
InternalTraceObjectValue value = parent.setValue(lifespan, path.key(), this, resolution);
|
||||
DBTraceObjectValPath path = parent.doInsert(lifespan, resolution);
|
||||
return path.append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(Range<Long> lifespan, ConflictResolution resolution) {
|
||||
public DBTraceObjectValPath insert(Range<Long> lifespan, ConflictResolution resolution) {
|
||||
try (LockHold hold = manager.trace.lockWrite()) {
|
||||
doInsert(lifespan, resolution);
|
||||
return doInsert(lifespan, resolution);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,6 +255,9 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
}
|
||||
|
||||
protected void doRemoveTree(Range<Long> span) {
|
||||
for (DBTraceObjectValue parent : getParents()) {
|
||||
parent.doTruncateOrDeleteAndEmitLifeChange(span);
|
||||
}
|
||||
for (InternalTraceObjectValue value : getValues()) {
|
||||
value.doTruncateOrDeleteAndEmitLifeChange(span);
|
||||
if (value.isCanonical()) {
|
||||
@ -264,7 +269,6 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
@Override
|
||||
public void removeTree(Range<Long> span) {
|
||||
try (LockHold hold = manager.trace.lockWrite()) {
|
||||
getCanonicalParents(span).forEach(v -> v.doTruncateOrDeleteAndEmitLifeChange(span));
|
||||
doRemoveTree(span);
|
||||
}
|
||||
}
|
||||
@ -327,10 +331,14 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
return ifCls.cast(ifaces.get(ifCls));
|
||||
}
|
||||
|
||||
protected Collection<? extends DBTraceObjectValue> doGetParents() {
|
||||
return manager.valuesByChild.get(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends DBTraceObjectValue> getParents() {
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
return manager.valuesByChild.get(this);
|
||||
return doGetParents();
|
||||
}
|
||||
}
|
||||
|
||||
@ -626,23 +634,35 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends DBTraceObjectValPath> getAncestors(
|
||||
public Stream<? extends TraceObjectValPath> getAncestors(Range<Long> span,
|
||||
PathPredicates relativePredicates) {
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
Stream<? extends DBTraceObjectValPath> ancestors =
|
||||
doStreamVisitor(span, new InternalAncestorsRelativeVisitor(relativePredicates));
|
||||
if (relativePredicates.matches(List.of())) {
|
||||
return Stream.concat(Stream.of(DBTraceObjectValPath.of()), ancestors);
|
||||
}
|
||||
return ancestors;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends DBTraceObjectValPath> getAncestorsRoot(
|
||||
Range<Long> span, PathPredicates rootPredicates) {
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
return doStreamVisitor(span, new InternalAncestorsVisitor(rootPredicates));
|
||||
return doStreamVisitor(span, new InternalAncestorsRootVisitor(rootPredicates));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends DBTraceObjectValPath> getSuccessors(
|
||||
Range<Long> span, PathPredicates relativePredicates) {
|
||||
DBTraceObjectValPath empty = DBTraceObjectValPath.of();
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
Stream<? extends DBTraceObjectValPath> succcessors =
|
||||
doStreamVisitor(span, new InternalSuccessorsVisitor(relativePredicates));
|
||||
doStreamVisitor(span, new InternalSuccessorsRelativeVisitor(relativePredicates));
|
||||
if (relativePredicates.matches(List.of())) {
|
||||
// Pre-cat the empty path (not the empty stream)
|
||||
return Stream.concat(Stream.of(empty), succcessors);
|
||||
return Stream.concat(Stream.of(DBTraceObjectValPath.of()), succcessors);
|
||||
}
|
||||
return succcessors;
|
||||
}
|
||||
@ -794,7 +814,7 @@ public class DBTraceObject extends DBAnnotatedObject implements TraceObject {
|
||||
Class<? extends TargetObject> targetIf) {
|
||||
// This is a sort of meet-in-the-middle. The type search must originate from the root
|
||||
PathMatcher matcher = getManager().getRootSchema().searchFor(targetIf, false);
|
||||
return getAncestors(span, matcher);
|
||||
return getAncestorsRoot(span, matcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -23,6 +23,7 @@ import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree;
|
||||
import ghidra.trace.database.map.DBTraceAddressSnapRangePropertyMapTree.AbstractDBTraceAddressSnapRangePropertyMapData;
|
||||
import ghidra.trace.database.target.DBTraceObjectValue.DBTraceObjectDBFieldCodec;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObjectKeyPath;
|
||||
import ghidra.trace.model.target.TraceObjectValue;
|
||||
import ghidra.trace.util.TraceAddressSpace;
|
||||
import ghidra.util.LockHold;
|
||||
@ -66,6 +67,12 @@ public class DBTraceObjectAddressRangeValue
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + ": parent=" + parent + ", key=" + entryKey +
|
||||
", lifespan=" + getLifespan() + ", value=" + getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setRecordValue(DBTraceObjectAddressRangeValue value) {
|
||||
// Nothing to do. I am the value
|
||||
@ -121,11 +128,23 @@ public class DBTraceObjectAddressRangeValue
|
||||
throw new ClassCastException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObject() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBTraceObject getChildOrNull() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceObjectKeyPath getCanonicalPath() {
|
||||
try (LockHold hold = manager.trace.lockRead()) {
|
||||
return parent.getCanonicalPath().extend(entryKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCanonical() {
|
||||
return false;
|
||||
|
@ -368,7 +368,7 @@ public class DBTraceObjectManager implements TraceObjectManager, DBTraceManager
|
||||
if (rootVal == null) {
|
||||
return Stream.of();
|
||||
}
|
||||
return rootVal.doStreamVisitor(span, new InternalSuccessorsVisitor(predicates));
|
||||
return rootVal.doStreamVisitor(span, new InternalSuccessorsRelativeVisitor(predicates));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import ghidra.trace.database.DBTraceUtils;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.Visitor;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.trace.model.target.TraceObjectKeyPath;
|
||||
import ghidra.util.LockHold;
|
||||
import ghidra.util.database.*;
|
||||
import ghidra.util.database.DBCachedObjectStoreFactory.AbstractDBFieldCodec;
|
||||
@ -275,6 +276,11 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
|
||||
return (DBTraceObject) getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isObject() {
|
||||
return child != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBTraceObject getChildOrNull() {
|
||||
return child;
|
||||
@ -320,6 +326,10 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
|
||||
return InternalTreeTraversal.INSTANCE.walkValue(visitor, this, span, null);
|
||||
}
|
||||
|
||||
protected TraceObjectKeyPath doGetCanonicalPath() {
|
||||
return triple.parent.getCanonicalPath().extend(triple.key);
|
||||
}
|
||||
|
||||
protected boolean doIsCanonical() {
|
||||
if (child == null) {
|
||||
return false;
|
||||
@ -327,7 +337,14 @@ public class DBTraceObjectValue extends DBAnnotatedObject implements InternalTra
|
||||
if (triple.parent == null) {
|
||||
return true;
|
||||
}
|
||||
return triple.parent.getCanonicalPath().extend(triple.key).equals(child.getCanonicalPath());
|
||||
return doGetCanonicalPath().equals(child.getCanonicalPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraceObjectKeyPath getCanonicalPath() {
|
||||
try (LockHold hold = LockHold.lock(manager.lock.readLock())) {
|
||||
return doGetCanonicalPath();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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.trace.database.target;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.dbg.util.PathPredicates;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.VisitResult;
|
||||
|
||||
public class InternalAncestorsRelativeVisitor implements SpanIntersectingVisitor {
|
||||
|
||||
protected final PathPredicates predicates;
|
||||
|
||||
public InternalAncestorsRelativeVisitor(PathPredicates predicates) {
|
||||
this.predicates = predicates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBTraceObjectValPath composePath(DBTraceObjectValPath pre,
|
||||
InternalTraceObjectValue value) {
|
||||
return pre == null ? DBTraceObjectValPath.of() : pre.prepend(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisitResult visitValue(InternalTraceObjectValue value, DBTraceObjectValPath path) {
|
||||
List<String> keyList = path.getKeyList();
|
||||
return VisitResult.result(predicates.matches(keyList),
|
||||
predicates.ancestorCouldMatchRight(keyList, true) && value.getChildOrNull() != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DBTraceObject continueObject(InternalTraceObjectValue value) {
|
||||
return value.getParent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends InternalTraceObjectValue> continueValues(DBTraceObject object,
|
||||
Range<Long> span, DBTraceObjectValPath pre) {
|
||||
Set<String> prevKeys = predicates.getPrevKeys(pre.getKeyList());
|
||||
if (prevKeys.isEmpty()) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
return object.doGetParents()
|
||||
.stream()
|
||||
.filter(v -> PathPredicates.anyMatches(prevKeys, v.getEntryKey()));
|
||||
}
|
||||
}
|
@ -23,11 +23,11 @@ import ghidra.dbg.util.PathPredicates;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.VisitResult;
|
||||
|
||||
public class InternalAncestorsVisitor implements SpanIntersectingVisitor {
|
||||
public class InternalAncestorsRootVisitor implements SpanIntersectingVisitor {
|
||||
|
||||
protected final PathPredicates predicates;
|
||||
|
||||
public InternalAncestorsVisitor(PathPredicates predicates) {
|
||||
public InternalAncestorsRootVisitor(PathPredicates predicates) {
|
||||
this.predicates = predicates;
|
||||
}
|
||||
|
@ -26,11 +26,11 @@ import ghidra.trace.database.DBTraceUtils;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.SpanIntersectingVisitor;
|
||||
import ghidra.trace.database.target.InternalTreeTraversal.VisitResult;
|
||||
|
||||
public class InternalSuccessorsVisitor implements SpanIntersectingVisitor {
|
||||
public class InternalSuccessorsRelativeVisitor implements SpanIntersectingVisitor {
|
||||
|
||||
protected final PathPredicates predicates;
|
||||
|
||||
public InternalSuccessorsVisitor(PathPredicates predicates) {
|
||||
public InternalSuccessorsRelativeVisitor(PathPredicates predicates) {
|
||||
this.predicates = predicates;
|
||||
}
|
||||
|
@ -125,6 +125,14 @@ public class TraceDomainObjectListener implements DomainObjectListener {
|
||||
typedMap.put(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the given event, taking the affected object, the old value, and the new value
|
||||
*
|
||||
* @param <T> the type of the affected object
|
||||
* @param <U> the type of the values
|
||||
* @param type the event type
|
||||
* @param handler the handler
|
||||
*/
|
||||
protected <T, U> void listenFor(TraceChangeType<T, U> type,
|
||||
AffectedAndValuesOnlyHandler<? super T, ? super U> handler) {
|
||||
typedMap.put(type, handler);
|
||||
|
@ -21,6 +21,7 @@ import java.util.stream.Stream;
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.RangeSet;
|
||||
|
||||
import ghidra.dbg.target.TargetMethod;
|
||||
import ghidra.dbg.target.TargetObject;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathPredicates;
|
||||
@ -38,6 +39,8 @@ import ghidra.trace.model.TraceUniqueObject;
|
||||
* In many cases, such interfaces are just wrappers.
|
||||
*/
|
||||
public interface TraceObject extends TraceUniqueObject {
|
||||
String EXTRA_INTERFACES_ATTRIBUTE_NAME = "_extra_ifs";
|
||||
|
||||
/**
|
||||
* Get the trace containing this object
|
||||
*
|
||||
@ -79,8 +82,9 @@ public interface TraceObject extends TraceUniqueObject {
|
||||
*
|
||||
* @param the minimum lifespan of edges from the root to this object
|
||||
* @param resolution the rule for handling duplicate keys when setting values.
|
||||
* @return the value path from root to the newly inserted object
|
||||
*/
|
||||
void insert(Range<Long> lifespan, ConflictResolution resolution);
|
||||
TraceObjectValPath insert(Range<Long> lifespan, ConflictResolution resolution);
|
||||
|
||||
/**
|
||||
* Remove this object from its canonical path for the given lifespan
|
||||
@ -97,9 +101,9 @@ public interface TraceObject extends TraceUniqueObject {
|
||||
* Remove this object and its successors from their canonical paths for the given span
|
||||
*
|
||||
* <p>
|
||||
* Truncate the lifespans of this object's canonical parent value and all canonical values
|
||||
* succeeding this object. If a truncated value's lifespan is contained in the given span, the
|
||||
* value will be deleted.
|
||||
* Truncate the lifespans of this object's parent values and all canonical values succeeding
|
||||
* this object. If a truncated value's lifespan is contained in the given span, the value will
|
||||
* be deleted.
|
||||
*
|
||||
* @param span the span during which this object and its canonical successors should be removed
|
||||
*/
|
||||
@ -282,9 +286,20 @@ public interface TraceObject extends TraceUniqueObject {
|
||||
* @param rootPredicates the predicates for matching path keys, relative to the root
|
||||
* @return the stream of matching paths to values
|
||||
*/
|
||||
Stream<? extends TraceObjectValPath> getAncestors(Range<Long> span,
|
||||
Stream<? extends TraceObjectValPath> getAncestorsRoot(Range<Long> span,
|
||||
PathPredicates rootPredicates);
|
||||
|
||||
/**
|
||||
* Stream all ancestor values of this object matching the given predicates, intersecting the
|
||||
* given span
|
||||
*
|
||||
* @param span a span which values along the path must intersect
|
||||
* @param relativePredicates the predicates for matching path keys, relative to this object
|
||||
* @return the stream of matching paths to values
|
||||
*/
|
||||
Stream<? extends TraceObjectValPath> getAncestors(Range<Long> span,
|
||||
PathPredicates relativePredicates);
|
||||
|
||||
/**
|
||||
* Stream all successor values of this object matching the given predicates, intersecting the
|
||||
* given span
|
||||
@ -466,4 +481,30 @@ public interface TraceObject extends TraceUniqueObject {
|
||||
*/
|
||||
@Override
|
||||
boolean isDeleted();
|
||||
|
||||
/**
|
||||
* Check if the child represents a method at the given snap
|
||||
*
|
||||
* @param snap the snap
|
||||
* @return true if a method
|
||||
*/
|
||||
default boolean isMethod(long snap) {
|
||||
if (getTargetSchema().getInterfaces().contains(TargetMethod.class)) {
|
||||
return true;
|
||||
}
|
||||
TraceObjectValue extras = getAttribute(snap, TraceObject.EXTRA_INTERFACES_ATTRIBUTE_NAME);
|
||||
if (extras == null) {
|
||||
return false;
|
||||
}
|
||||
Object val = extras.getValue();
|
||||
if (!(val instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
String valStr = (String) val;
|
||||
// Not ideal, but it's not a substring of any other schema interface....
|
||||
if (valStr.contains("Method")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -225,9 +225,26 @@ public final class TraceObjectKeyPath implements Comparable<TraceObjectKeyPath>
|
||||
if (!predicates.ancestorMatches(keyList, false)) {
|
||||
return Stream.of();
|
||||
}
|
||||
Stream<TraceObjectKeyPath> ancestry =
|
||||
isRoot() ? Stream.of() : parent().streamMatchingAncestry(predicates);
|
||||
if (predicates.matches(keyList)) {
|
||||
return Stream.concat(Stream.of(this), parent().streamMatchingAncestry(predicates));
|
||||
return Stream.concat(Stream.of(this), ancestry);
|
||||
}
|
||||
return parent().streamMatchingAncestry(predicates);
|
||||
return ancestry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this path is an ancestor of the given path
|
||||
*
|
||||
* <p>
|
||||
* Equivalently, check if the given path is a successor of this path. A path is considered an
|
||||
* ancestor of itself. To check for a strict ancestor, use
|
||||
* {@code this.isAncestor(that) && !this.equals(that)}.
|
||||
*
|
||||
* @param that the supposed successor to this path
|
||||
* @return true if the given path is in fact a successor
|
||||
*/
|
||||
public boolean isAncestor(TraceObjectKeyPath that) {
|
||||
return PathUtils.isAncestor(keyList, that.keyList);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package ghidra.trace.model.target;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObject.ConflictResolution;
|
||||
|
||||
@ -43,6 +44,17 @@ public interface TraceObjectValue {
|
||||
*/
|
||||
String getEntryKey();
|
||||
|
||||
/**
|
||||
* Get the "canonical path" of this value
|
||||
*
|
||||
* <p>
|
||||
* This is the parent's canonical path extended by this value's entry key. Note, in the case
|
||||
* this value has a child object, this is not necessarily its canonical path.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
TraceObjectKeyPath getCanonicalPath();
|
||||
|
||||
/**
|
||||
* Get the value
|
||||
*
|
||||
@ -58,6 +70,13 @@ public interface TraceObjectValue {
|
||||
*/
|
||||
TraceObject getChild();
|
||||
|
||||
/**
|
||||
* Check if the value is an object (i.e., {@link TraceObject})
|
||||
*
|
||||
* @return true if an object, false otherwise
|
||||
*/
|
||||
boolean isObject();
|
||||
|
||||
/**
|
||||
* Check if this value represents its child's canonical location
|
||||
*
|
||||
@ -69,6 +88,15 @@ public interface TraceObjectValue {
|
||||
*/
|
||||
boolean isCanonical();
|
||||
|
||||
/**
|
||||
* Get the (target) schema for the value
|
||||
*
|
||||
* @return the schema
|
||||
*/
|
||||
default TargetObjectSchema getTargetSchema() {
|
||||
return getParent().getTargetSchema().getChildSchema(getEntryKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lifespan of this entry, truncating duplicates
|
||||
*
|
||||
@ -157,4 +185,17 @@ public interface TraceObjectValue {
|
||||
* if a second is created.
|
||||
*/
|
||||
TraceObjectValue truncateOrDelete(Range<Long> span);
|
||||
|
||||
/**
|
||||
* Check if the schema designates this value as hidden
|
||||
*
|
||||
* @return true if hidden
|
||||
*/
|
||||
default boolean isHidden() {
|
||||
TraceObject parent = getParent();
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
return parent.getTargetSchema().isHidden(getEntryKey());
|
||||
}
|
||||
}
|
||||
|
@ -26,21 +26,24 @@ import javax.swing.table.*;
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
extends GTableHeaderRenderer {
|
||||
extends GTableHeaderRenderer implements RangedRenderer<N> {
|
||||
protected final static int ARROW_SIZE = 10;
|
||||
protected final static Polygon ARROW = new Polygon(
|
||||
new int[] { 0, -ARROW_SIZE, -ARROW_SIZE },
|
||||
new int[] { 0, ARROW_SIZE, -ARROW_SIZE }, 3);
|
||||
|
||||
protected Range<Double> fullRange = Range.closed(0d, 1d);
|
||||
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
|
||||
protected double span = 1;
|
||||
|
||||
protected Range<N> fullRange;
|
||||
|
||||
protected N pos;
|
||||
protected double doublePos;
|
||||
|
||||
@Override
|
||||
public void setFullRange(Range<N> fullRange) {
|
||||
this.fullRange = RangeTableCellRenderer.validateViewRange(fullRange);
|
||||
this.span = this.fullRange.upperEndpoint() - this.fullRange.lowerEndpoint();
|
||||
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
|
||||
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
|
||||
}
|
||||
|
||||
public void setCursorPosition(N pos) {
|
||||
@ -51,6 +54,7 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
@Override
|
||||
protected void paintChildren(Graphics g) {
|
||||
super.paintChildren(g);
|
||||
// The cursor should occlude the children
|
||||
paintCursor(g);
|
||||
}
|
||||
|
||||
@ -58,7 +62,7 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
Graphics2D g = (Graphics2D) parentG.create();
|
||||
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
double x = (doublePos - fullRange.lowerEndpoint()) / span * getWidth();
|
||||
double x = (doublePos - fullRangeDouble.lowerEndpoint()) / span * getWidth();
|
||||
g.translate(x, getHeight());
|
||||
g.rotate(Math.PI / 2);
|
||||
g.setColor(getForeground());
|
||||
@ -117,7 +121,8 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
}
|
||||
TableColumn col = colModel.getColumn(viewColIdx);
|
||||
|
||||
double pos = span * (e.getX() - colX) / col.getWidth() + fullRange.lowerEndpoint();
|
||||
double pos =
|
||||
span * (e.getX() - colX) / col.getWidth() + fullRangeDouble.lowerEndpoint();
|
||||
listener.accept(pos);
|
||||
}
|
||||
};
|
||||
@ -128,4 +133,19 @@ public class RangeCursorTableHeaderRenderer<N extends Number & Comparable<N>>
|
||||
public N getCursorPosition() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<N> getFullRange() {
|
||||
return fullRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<Double> getFullRangeDouble() {
|
||||
return fullRangeDouble;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSpan() {
|
||||
return span;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Graphics;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
import com.google.common.collect.RangeSet;
|
||||
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||
|
||||
public class RangeSetTableCellRenderer<N extends Number & Comparable<N>>
|
||||
extends AbstractGColumnRenderer<RangeSet<N>> implements RangedRenderer<N> {
|
||||
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
|
||||
protected double span = 1;
|
||||
|
||||
protected Range<N> fullRange;
|
||||
protected RangeSet<N> dataRangeSet;
|
||||
|
||||
@Override
|
||||
public void setFullRange(Range<N> fullRange) {
|
||||
this.fullRange = fullRange;
|
||||
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
|
||||
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterString(RangeSet<N> t, Settings settings) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
|
||||
this.dataRangeSet = (RangeSet<N>) data.getValue();
|
||||
super.getTableCellRendererComponent(data);
|
||||
setText("");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics parentG) {
|
||||
super.paintComponent(parentG);
|
||||
if (dataRangeSet == null || dataRangeSet.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Graphics g = parentG.create();
|
||||
g.setColor(getForeground());
|
||||
for (Range<N> range : dataRangeSet.asRanges()) {
|
||||
paintRange(g, range);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<N> getFullRange() {
|
||||
return fullRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<Double> getFullRangeDouble() {
|
||||
return fullRangeDouble;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSpan() {
|
||||
return span;
|
||||
}
|
||||
}
|
@ -24,27 +24,19 @@ import ghidra.docking.settings.Settings;
|
||||
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||
|
||||
public class RangeTableCellRenderer<N extends Number & Comparable<N>>
|
||||
extends AbstractGColumnRenderer<Range<N>> {
|
||||
extends AbstractGColumnRenderer<Range<N>> implements RangedRenderer<N> {
|
||||
|
||||
protected Range<Double> doubleFullRange = Range.closed(0d, 1d);
|
||||
protected Range<Double> fullRangeDouble = Range.closed(0d, 1d);
|
||||
protected double span = 1;
|
||||
|
||||
protected Range<N> fullRange;
|
||||
protected Range<N> dataRange;
|
||||
|
||||
public static Range<Double> validateViewRange(Range<? extends Number> fullRange) {
|
||||
if (!fullRange.hasLowerBound() || !fullRange.hasUpperBound()) {
|
||||
throw new IllegalArgumentException("Cannot have unbounded full range");
|
||||
}
|
||||
// I don't care to preserve open/closed, since it just specifies the view bounds
|
||||
return Range.closed(fullRange.lowerEndpoint().doubleValue(),
|
||||
fullRange.upperEndpoint().doubleValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFullRange(Range<N> fullRange) {
|
||||
this.fullRange = fullRange;
|
||||
this.doubleFullRange = validateViewRange(fullRange);
|
||||
this.span = this.doubleFullRange.upperEndpoint() - this.doubleFullRange.lowerEndpoint();
|
||||
this.fullRangeDouble = RangedRenderer.validateViewRange(fullRange);
|
||||
this.span = this.fullRangeDouble.upperEndpoint() - this.fullRangeDouble.lowerEndpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,38 +59,24 @@ public class RangeTableCellRenderer<N extends Number & Comparable<N>>
|
||||
if (dataRange == null) {
|
||||
return;
|
||||
}
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
int x1 = dataRange.hasLowerBound()
|
||||
? interpolate(width, dataRange.lowerEndpoint().doubleValue())
|
||||
: 0;
|
||||
int x2 = dataRange.hasUpperBound()
|
||||
? interpolate(width, dataRange.upperEndpoint().doubleValue())
|
||||
: width;
|
||||
|
||||
int y1 = height > 2 ? 1 : 0;
|
||||
int y2 = height > 2 ? height - 1 : height;
|
||||
|
||||
Graphics g = parentG.create();
|
||||
g.setColor(getForeground());
|
||||
|
||||
g.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
protected int interpolate(int w, double val) {
|
||||
double lower = doubleFullRange.lowerEndpoint();
|
||||
if (val <= lower) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= doubleFullRange.upperEndpoint()) {
|
||||
return w;
|
||||
}
|
||||
double dif = val - lower;
|
||||
return (int) (dif / span * w);
|
||||
paintRange(g, dataRange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<N> getFullRange() {
|
||||
return fullRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<Double> getFullRangeDouble() {
|
||||
return fullRangeDouble;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSpan() {
|
||||
return span;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.widgets.table;
|
||||
|
||||
import java.awt.Graphics;
|
||||
|
||||
import com.google.common.collect.Range;
|
||||
|
||||
public interface RangedRenderer<N extends Number & Comparable<N>> {
|
||||
|
||||
public static Range<Double> validateViewRange(Range<? extends Number> fullRange) {
|
||||
if (!fullRange.hasLowerBound() || !fullRange.hasUpperBound()) {
|
||||
throw new IllegalArgumentException("Cannot have unbounded full range");
|
||||
}
|
||||
// I don't care to preserve open/closed, since it just specifies the view bounds
|
||||
return Range.closed(fullRange.lowerEndpoint().doubleValue(),
|
||||
fullRange.upperEndpoint().doubleValue());
|
||||
}
|
||||
|
||||
void setFullRange(Range<N> fullRange);
|
||||
|
||||
Range<N> getFullRange();
|
||||
|
||||
Range<Double> getFullRangeDouble();
|
||||
|
||||
double getSpan();
|
||||
|
||||
default int interpolate(int w, double val) {
|
||||
Range<Double> fullRangeDouble = getFullRangeDouble();
|
||||
double span = getSpan();
|
||||
double lower = fullRangeDouble.lowerEndpoint();
|
||||
if (val <= lower) {
|
||||
return 0;
|
||||
}
|
||||
if (val >= fullRangeDouble.upperEndpoint()) {
|
||||
return w;
|
||||
}
|
||||
double dif = val - lower;
|
||||
return (int) (dif / span * w);
|
||||
}
|
||||
|
||||
int getWidth();
|
||||
|
||||
int getHeight();
|
||||
|
||||
default void paintRange(Graphics g, Range<N> range) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
int x1 = range.hasLowerBound()
|
||||
? interpolate(width, range.lowerEndpoint().doubleValue())
|
||||
: 0;
|
||||
int x2 = range.hasUpperBound()
|
||||
? interpolate(width, range.upperEndpoint().doubleValue())
|
||||
: width;
|
||||
|
||||
int y1 = height > 2 ? 1 : 0;
|
||||
int y2 = height > 2 ? height - 1 : height;
|
||||
|
||||
g.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
}
|
@ -24,18 +24,17 @@ import ghidra.util.table.column.GColumnRenderer;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* An Table Column is an interface that should be implemented by each class that provides
|
||||
* a field (column) of an object based table (each row relates to a particular type of object).
|
||||
* It determines the appropriate cell object for use by the table column this field represents.
|
||||
* It can then return the appropriate object to display in the table cell for the indicated
|
||||
* row object.
|
||||
* An Table Column is an interface that should be implemented by each class that provides a field
|
||||
* (column) of an object based table (each row relates to a particular type of object). It
|
||||
* determines the appropriate cell object for use by the table column this field represents. It can
|
||||
* then return the appropriate object to display in the table cell for the indicated row object.
|
||||
*
|
||||
* Implementations of this interface must provide a public default constructor.
|
||||
*
|
||||
* @param <ROW_TYPE> The row object class supported by this column
|
||||
* @param <COLUMN_TYPE> The column object class supported by this column
|
||||
* @param <DATA_SOURCE> The object class type that will be passed to
|
||||
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
|
||||
* @param <DATA_SOURCE> The object class type that will be passed to see
|
||||
* <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
|
||||
*/
|
||||
public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
|
||||
implements DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
@ -80,11 +79,16 @@ public abstract class AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOU
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<COLUMN_TYPE> getComparator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
|
||||
int columnIndex) {
|
||||
return getComparator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
// enforced by the compiler
|
||||
|
@ -24,48 +24,52 @@ import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.util.table.column.GColumnRenderer;
|
||||
|
||||
/**
|
||||
* The root interface for defining columns for {@link GDynamicColumnTableModel}s. The
|
||||
* class allows you to create objects for tables that know how to give a column value for a
|
||||
* given row.
|
||||
* The root interface for defining columns for {@link DynamicColumnTableModel}s. The class allows
|
||||
* you to create objects for tables that know how to give a column value for a given row.
|
||||
*
|
||||
* @param <ROW_TYPE> The row object class supported by this column
|
||||
* @param <COLUMN_TYPE> The column object class supported by this column
|
||||
* @param <DATA_SOURCE> The object class type that will be passed to
|
||||
* see <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
|
||||
* @param <DATA_SOURCE> The object class type that will be passed to see
|
||||
* <code>getValue(ROW_TYPE, Settings, DATA_SOURCE, ServiceProvider)</code>
|
||||
*/
|
||||
public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
|
||||
/**
|
||||
* Determines the unique column heading that may be used to identify a column instance.
|
||||
* This name must be non-changing and is used to save/restore state information.
|
||||
* Determines the unique column heading that may be used to identify a column instance. This
|
||||
* name must be non-changing and is used to save/restore state information.
|
||||
*
|
||||
* @return the field instance name.
|
||||
*/
|
||||
public String getColumnName();
|
||||
|
||||
/**
|
||||
* Determines the class of object that is associated with this field (column).
|
||||
*
|
||||
* @return the column class
|
||||
*/
|
||||
public Class<COLUMN_TYPE> getColumnClass();
|
||||
|
||||
/**
|
||||
* Returns the preferred width for this column. Column should either return a valid positive
|
||||
* Returns the preferred width for this column. Column should either return a valid positive
|
||||
* preferred size or -1.
|
||||
*
|
||||
* @return the preferred width for this column.
|
||||
*/
|
||||
public int getColumnPreferredWidth();
|
||||
|
||||
/**
|
||||
* Returns the single class type of the data that this table field can use to
|
||||
* generate columnar data.
|
||||
* @return the single class type of the data that this table field can use to
|
||||
* generate columnar data.
|
||||
* Returns the single class type of the data that this table field can use to generate columnar
|
||||
* data.
|
||||
*
|
||||
* @return the single class type of the data that this table field can use to generate columnar
|
||||
* data.
|
||||
*/
|
||||
public Class<ROW_TYPE> getSupportedRowType();
|
||||
|
||||
/**
|
||||
* Creates an object that is appropriate for this field (table column) and for the
|
||||
* object that is associated with this row of the table.
|
||||
* Creates an object that is appropriate for this field (table column) and for the object that
|
||||
* is associated with this row of the table.
|
||||
*
|
||||
* @param rowObject the object associated with the row in the table.
|
||||
* @param settings field settings
|
||||
* @param data the expected data object, as defined by the DATA_SOURCE type
|
||||
@ -79,13 +83,15 @@ public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
/**
|
||||
* Returns the optional cell renderer for this column; null if no renderer is used.
|
||||
*
|
||||
* <P>This method allows columns to define custom rendering. The interface returned here
|
||||
* ensures that the text used for filtering matches what the users sees (via the
|
||||
* <P>
|
||||
* This method allows columns to define custom rendering. The interface returned here ensures
|
||||
* that the text used for filtering matches what the users sees (via the
|
||||
* {@link GColumnRenderer#getFilterString(Object, Settings)} method).
|
||||
*
|
||||
* <P>Note: some types should not make use of the aforementioned filter string. These types
|
||||
* include the {@link Number} wrapper types, {@link Date} and {@link Enum}s. (This is
|
||||
* because the filtering system works naturally with these types.) See {@link GColumnRenderer}.
|
||||
* <P>
|
||||
* Note: some types should not make use of the aforementioned filter string. These types include
|
||||
* the {@link Number} wrapper types, {@link Date} and {@link Enum}s. (This is because the
|
||||
* filtering system works naturally with these types.) See {@link GColumnRenderer}.
|
||||
*
|
||||
* @return the renderer
|
||||
*/
|
||||
@ -93,13 +99,15 @@ public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
|
||||
/**
|
||||
* Returns a list of settings definitions for this field.
|
||||
*
|
||||
* @return list of settings definitions for this field.
|
||||
*/
|
||||
public SettingsDefinition[] getSettingsDefinitions();
|
||||
|
||||
/**
|
||||
* Gets the maximum number of text display lines needed for any given cell with the
|
||||
* specified settings.
|
||||
* Gets the maximum number of text display lines needed for any given cell with the specified
|
||||
* settings.
|
||||
*
|
||||
* @param settings field settings
|
||||
* @return maximum number of lines needed
|
||||
*/
|
||||
@ -107,28 +115,33 @@ public interface DynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
|
||||
/**
|
||||
* Determines the column heading that will be displayed.
|
||||
*
|
||||
* @param settings the settings
|
||||
* @return the field name to display as the column heading.
|
||||
*/
|
||||
public String getColumnDisplayName(Settings settings);
|
||||
|
||||
/**
|
||||
* Returns a description of this column. This may be used as a tooltip for the column header
|
||||
* @return a description of this column. This may be used as a tooltip for the column header.
|
||||
* Returns a description of this column. This may be used as a tooltip for the column header
|
||||
*
|
||||
* @return a description of this column. This may be used as a tooltip for the column header.
|
||||
*/
|
||||
public String getColumnDescription();
|
||||
|
||||
/**
|
||||
* Returns a value that is unique for this table column. This is different than getting
|
||||
* the display name, which may be shared by different columns.
|
||||
* Returns a value that is unique for this table column. This is different than getting the
|
||||
* display name, which may be shared by different columns.
|
||||
*
|
||||
* @return the identifier
|
||||
*/
|
||||
public String getUniqueIdentifier();
|
||||
|
||||
/**
|
||||
* If implemented, will return a comparator that knows how to sort values for this column.
|
||||
* If implemented, will return a comparator that knows how to sort values for this column.
|
||||
* Implementors should return <code>null</code> if they do not wish to provider a comparator
|
||||
*
|
||||
* @return the comparator
|
||||
*/
|
||||
public Comparator<COLUMN_TYPE> getComparator();
|
||||
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
|
||||
int columnIndex);
|
||||
}
|
||||
|
@ -31,31 +31,29 @@ import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* An abstract table model for showing DynamicTableColumns where each row is based on an
|
||||
* object of type ROW_TYPE. The client is responsible for implementing
|
||||
* {@link #createTableColumnDescriptor()}. This method specifies which default columns the
|
||||
* table should have and whether they should be visible or hidden. Hidden columns can be
|
||||
* made visible through the UI.
|
||||
* An abstract table model for showing DynamicTableColumns where each row is based on an object of
|
||||
* type ROW_TYPE. The client is responsible for implementing {@link #createTableColumnDescriptor()}.
|
||||
* This method specifies which default columns the table should have and whether they should be
|
||||
* visible or hidden. Hidden columns can be made visible through the UI.
|
||||
* <p>
|
||||
* This model will also discover other system columns that understand how to render
|
||||
* <code>ROW_TYPE</code> data directly. Also, if you create a {@link TableRowMapper mapper}(s) for
|
||||
* <code>ROW_TYPE</code> data directly. Also, if you create a {@link TableRowMapper mapper}(s) for
|
||||
* your row type, then this model will load columns for each type for which a mapper was created,
|
||||
* all as optional, hidden columns.
|
||||
* <p>
|
||||
* The various attributes of the columns of this model (visibility, position, size, etc) are
|
||||
* saved to disk as tool preferences when the user exits the tool.
|
||||
* The various attributes of the columns of this model (visibility, position, size, etc) are saved
|
||||
* to disk as tool preferences when the user exits the tool.
|
||||
* <p>
|
||||
* Implementation Note: this model loads all columns, specific and discovered, as being visible.
|
||||
* Then, during initialization, the {@link TableColumnModelState} class will
|
||||
* either hide all non-default columns, or reload the column state if any
|
||||
* previous saved state is found.
|
||||
* Then, during initialization, the {@link TableColumnModelState} class will either hide all
|
||||
* non-default columns, or reload the column state if any previous saved state is found.
|
||||
*
|
||||
* @param <ROW_TYPE> the row object class for this table model.
|
||||
* @param <DATA_SOURCE> the type of data that will be returned from {@link #getDataSource()}. This
|
||||
* object will be given to the {@link DynamicTableColumn} objects used by this
|
||||
* table model when
|
||||
* {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)}
|
||||
* is called.
|
||||
* @param <DATA_SOURCE> the type of data that will be returned from {@link #getDataSource()}. This
|
||||
* object will be given to the {@link DynamicTableColumn} objects used by this table
|
||||
* model when
|
||||
* {@link DynamicTableColumn#getValue(Object, ghidra.docking.settings.Settings, Object, ServiceProvider)}
|
||||
* is called.
|
||||
*/
|
||||
public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
extends AbstractSortedTableModel<ROW_TYPE>
|
||||
@ -122,10 +120,10 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows clients to defer column creation until after this parent class's constructor has
|
||||
* been called. This method will not restore any column settings that have been changed
|
||||
* after construction. Thus, this method is intended only to be called during the
|
||||
* construction process.
|
||||
* Allows clients to defer column creation until after this parent class's constructor has been
|
||||
* called. This method will not restore any column settings that have been changed after
|
||||
* construction. Thus, this method is intended only to be called during the construction
|
||||
* process.
|
||||
*/
|
||||
protected void reloadColumns() {
|
||||
|
||||
@ -201,9 +199,8 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* This differs from {@link #createSortComparator(int)} in that the other method
|
||||
* creates a comparator that operates on a full row value, whereas this method operates on
|
||||
* column values.
|
||||
* This differs from {@link #createSortComparator(int)} in that the other method creates a
|
||||
* comparator that operates on a full row value, whereas this method operates on column values.
|
||||
*
|
||||
* @param columnIndex the column index
|
||||
* @return a comparator for the specific column values
|
||||
@ -211,7 +208,8 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
@SuppressWarnings("unchecked") // the column provides the values itself; safe cast
|
||||
protected Comparator<Object> createSortComparatorForColumn(int columnIndex) {
|
||||
DynamicTableColumn<ROW_TYPE, ?, ?> column = getColumn(columnIndex);
|
||||
Comparator<Object> comparator = (Comparator<Object>) column.getComparator();
|
||||
Comparator<Object> comparator =
|
||||
(Comparator<Object>) column.getComparator(this, columnIndex);
|
||||
return comparator;
|
||||
}
|
||||
|
||||
@ -252,11 +250,13 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given column at the end of the list of columns. This method is intended for
|
||||
* Adds the given column at the end of the list of columns. This method is intended for
|
||||
* implementations to add custom column objects, rather than relying on generic, discovered
|
||||
* DynamicTableColumn implementations.
|
||||
*
|
||||
* <p><b>Note: this method assumes that the columns have already been sorted</b>
|
||||
* <p>
|
||||
* <b>Note: this method assumes that the columns have already been sorted</b>
|
||||
*
|
||||
* @param column The field to add
|
||||
*/
|
||||
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
|
||||
@ -264,11 +264,12 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given columns to the end of the list of columns. This method is intended for
|
||||
* Adds the given columns to the end of the list of columns. This method is intended for
|
||||
* implementations to add custom column objects, rather than relying on generic, discovered
|
||||
* DynamicTableColumn implementations.
|
||||
*
|
||||
* <p><b>Note: this method assumes that the columns have already been sorted.</b>
|
||||
* <p>
|
||||
* <b>Note: this method assumes that the columns have already been sorted.</b>
|
||||
*
|
||||
* @param columns The columns to add
|
||||
*/
|
||||
@ -280,15 +281,16 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given field at the given index to the list of fields in this class.
|
||||
* This method is intended for implementations to add custom column objects, rather than
|
||||
* relying on generic, discovered DynamicTableColumn implementations.
|
||||
* Adds the given field at the given index to the list of fields in this class. This method is
|
||||
* intended for implementations to add custom column objects, rather than relying on generic,
|
||||
* discovered DynamicTableColumn implementations.
|
||||
* <p>
|
||||
* <b>Note: this method assumes that the columns have already been sorted.</b>
|
||||
*
|
||||
* @param column The field to add.
|
||||
* @param index The index at which to add the field. If the index value is invalid (negative
|
||||
* or greater than the number of columns), then the column will be added to the
|
||||
* end of the columns list.
|
||||
* @param index The index at which to add the field. If the index value is invalid (negative or
|
||||
* greater than the number of columns), then the column will be added to the end of
|
||||
* the columns list.
|
||||
* @param isDefault true if this is a default column
|
||||
*/
|
||||
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column, int index,
|
||||
@ -324,8 +326,8 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given columns from this model. This method allows the client to remove
|
||||
* multiple columns at once, firing only one event when the work is finished.
|
||||
* Removes the given columns from this model. This method allows the client to remove multiple
|
||||
* columns at once, firing only one event when the work is finished.
|
||||
*
|
||||
* @param columns the columns to remove
|
||||
*/
|
||||
@ -363,6 +365,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
/**
|
||||
* Returns true if the column indicated by the index in the model is a default column (meaning
|
||||
* that it was specified by the model and not discovered).
|
||||
*
|
||||
* @param modelIndex the index of the column in the model.
|
||||
* @return true if the column is a default.
|
||||
*/
|
||||
@ -460,7 +463,8 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
|
||||
/**
|
||||
* Returns the table's context for the data.
|
||||
* @return the table's context for the data.
|
||||
*
|
||||
* @return the table's context for the data.
|
||||
*/
|
||||
public abstract DATA_SOURCE getDataSource();
|
||||
|
||||
@ -524,12 +528,12 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the special table cell renderer for the specified table field column.
|
||||
* A null value indicates that this field uses a default cell renderer.
|
||||
* Gets the special table cell renderer for the specified table field column. A null value
|
||||
* indicates that this field uses a default cell renderer.
|
||||
*
|
||||
* @param index the model column index
|
||||
* @return a table cell renderer for this field. Otherwise, null if a default
|
||||
* renderer should be used.
|
||||
* @return a table cell renderer for this field. Otherwise, null if a default renderer should be
|
||||
* used.
|
||||
*/
|
||||
@Override
|
||||
public TableCellRenderer getRenderer(int index) {
|
||||
@ -537,8 +541,9 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum number of text display lines needed for any given cell within the
|
||||
* specified column.
|
||||
* Gets the maximum number of text display lines needed for any given cell within the specified
|
||||
* column.
|
||||
*
|
||||
* @param index column field index
|
||||
* @return maximum number of lines needed for specified column
|
||||
*/
|
||||
|
@ -23,15 +23,15 @@ import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.util.table.column.GColumnRenderer;
|
||||
|
||||
/**
|
||||
* A class that is an Adapter in order to allow for the use of existing
|
||||
* {@link DynamicTableColumn}s when the actual row type of the table is
|
||||
* not the same as the row type that the {@link DynamicTableColumn} supports.
|
||||
* A class that is an Adapter in order to allow for the use of existing {@link DynamicTableColumn}s
|
||||
* when the actual row type of the table is not the same as the row type that the
|
||||
* {@link DynamicTableColumn} supports.
|
||||
*
|
||||
* @param <ROW_TYPE> The table's actual row type
|
||||
* @param <EXPECTED_ROW_TYPE> The row type expected by the given {@link DynamicTableColumn}
|
||||
* @param <COLUMN_TYPE> The column type provided by the given {@link DynamicTableColumn}
|
||||
* @param <DATA_SOURCE> the type of the data for each column; can be Object for columns that
|
||||
* do not have a data source
|
||||
* @param <DATA_SOURCE> the type of the data for each column; can be Object for columns that do not
|
||||
* have a data source
|
||||
*/
|
||||
public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SOURCE>
|
||||
extends AbstractDynamicTableColumn<ROW_TYPE, COLUMN_TYPE, DATA_SOURCE> {
|
||||
@ -110,8 +110,9 @@ public class MappedTableColumn<ROW_TYPE, EXPECTED_ROW_TYPE, COLUMN_TYPE, DATA_SO
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<COLUMN_TYPE> getComparator() {
|
||||
return tableColumn.getComparator();
|
||||
public Comparator<COLUMN_TYPE> getComparator(DynamicColumnTableModel<?> model,
|
||||
int columnIndex) {
|
||||
return tableColumn.getComparator(model, columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
Reference in New Issue
Block a user