mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-27 23:17:03 +08:00
GP-2555: Refactor StackProvider for object-mode trace.
This commit is contained in:
+1
-1
@@ -120,7 +120,7 @@ public class GdbModelTargetStackFrame
|
||||
return impl.gateFuture(frame.setActive(false));
|
||||
}
|
||||
|
||||
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME)
|
||||
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME, hidden = true)
|
||||
public String getFunction() {
|
||||
return func;
|
||||
}
|
||||
|
||||
+5
-1
@@ -107,7 +107,11 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
|
||||
if (object != null) {
|
||||
return iface.cast(recorder.getTargetObject(object));
|
||||
}
|
||||
return recorder.getFocus().getCachedSuitable(iface);
|
||||
TargetObject focus = recorder.getFocus();
|
||||
if (focus == null) {
|
||||
return null;
|
||||
}
|
||||
return focus.getCachedSuitable(iface);
|
||||
}
|
||||
|
||||
abstract boolean isEnabledForObject(T t);
|
||||
|
||||
+4
-4
@@ -45,14 +45,14 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
|
||||
}
|
||||
|
||||
protected void valueCreated(TraceObjectValue value) {
|
||||
if (query != null && query.includes(span, value)) {
|
||||
if (query != null && query.involves(span, value)) {
|
||||
reload(); // Can I be more surgical?
|
||||
}
|
||||
}
|
||||
|
||||
protected void valueDeleted(TraceObjectValue value) {
|
||||
if (query != null && query.includes(span, value)) {
|
||||
reload();
|
||||
if (query != null && query.involves(span, value)) {
|
||||
reload(); // Can I be more surgical?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
|
||||
}
|
||||
boolean inOld = span.intersects(oldSpan);
|
||||
boolean inNew = span.intersects(newSpan);
|
||||
boolean queryIncludes = query.includes(Lifespan.ALL, value);
|
||||
boolean queryIncludes = query.involves(Lifespan.ALL, value);
|
||||
if (queryIncludes) {
|
||||
if (inOld != inNew) {
|
||||
reload();
|
||||
|
||||
+26
-3
@@ -25,17 +25,20 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
|
||||
import docking.widgets.table.DynamicTableColumn;
|
||||
import docking.widgets.table.RangeCursorTableHeaderRenderer.SeekListener;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.trace.model.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
import ghidra.util.table.GhidraTable;
|
||||
import ghidra.util.table.GhidraTableFilterPanel;
|
||||
|
||||
public abstract class AbstractQueryTablePanel<T> extends JPanel {
|
||||
public abstract class AbstractQueryTablePanel<T, M extends AbstractQueryTableModel<T>>
|
||||
extends JPanel {
|
||||
|
||||
protected final AbstractQueryTableModel<T> tableModel;
|
||||
protected final M tableModel;
|
||||
protected final GhidraTable table;
|
||||
protected final GhidraTableFilterPanel<T> filterPanel;
|
||||
|
||||
@@ -53,7 +56,7 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
|
||||
add(filterPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
protected abstract AbstractQueryTableModel<T> createModel(Plugin plugin);
|
||||
protected abstract M createModel(Plugin plugin);
|
||||
|
||||
public void goToCoordinates(DebuggerCoordinates coords) {
|
||||
if (DebuggerCoordinates.equalsIgnoreRecorderAndView(current, coords)) {
|
||||
@@ -184,6 +187,26 @@ public abstract class AbstractQueryTablePanel<T> extends JPanel {
|
||||
return filterPanel.getSelectedItem();
|
||||
}
|
||||
|
||||
public List<T> getAllItems() {
|
||||
return List.copyOf(tableModel.getModelData());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <V> DynamicTableColumn<T, V, Trace> getColumnByNameAndType(String name, Class<V> type) {
|
||||
int count = tableModel.getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<T, ?, ?> column = tableModel.getColumn(i);
|
||||
if (!name.equals(column.getColumnName())) {
|
||||
continue;
|
||||
}
|
||||
if (column.getColumnClass() != type) {
|
||||
continue;
|
||||
}
|
||||
return (DynamicTableColumn<T, V, Trace>) column;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setDiffColor(Color diffColor) {
|
||||
tableModel.setDiffColor(diffColor);
|
||||
}
|
||||
|
||||
+85
-15
@@ -15,19 +15,20 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import ghidra.dbg.target.schema.EnumerableTargetObjectSchema;
|
||||
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.Lifespan;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.*;
|
||||
|
||||
public class ModelQuery {
|
||||
public static final ModelQuery EMPTY = new ModelQuery(PathPredicates.EMPTY);
|
||||
// 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.
|
||||
@@ -112,6 +113,23 @@ public class ModelQuery {
|
||||
return trace.getObjectManager().getValuePaths(span, predicates).map(p -> p);
|
||||
}
|
||||
|
||||
public List<TargetObjectSchema> computeSchemas(Trace trace) {
|
||||
TargetObjectSchema rootSchema = trace.getObjectManager().getRootSchema();
|
||||
return predicates.getPatterns()
|
||||
.stream()
|
||||
.map(p -> rootSchema.getSuccessorSchema(p.asPath()))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public TargetObjectSchema computeSingleSchema(Trace trace) {
|
||||
List<TargetObjectSchema> schemas = computeSchemas(trace);
|
||||
if (schemas.size() != 1) {
|
||||
return EnumerableTargetObjectSchema.OBJECT;
|
||||
}
|
||||
return schemas.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the named attributes for resulting objects, according to the schema
|
||||
*
|
||||
@@ -122,15 +140,31 @@ public class ModelQuery {
|
||||
* @return the list of attributes
|
||||
*/
|
||||
public Stream<AttributeSchema> computeAttributes(Trace trace) {
|
||||
TraceObjectManager objects = trace.getObjectManager();
|
||||
TargetObjectSchema schema =
|
||||
objects.getRootSchema().getSuccessorSchema(predicates.getSingletonPattern().asPath());
|
||||
TargetObjectSchema schema = computeSingleSchema(trace);
|
||||
return schema.getAttributeSchemas()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(as -> !"".equals(as.getName()));
|
||||
}
|
||||
|
||||
protected static boolean includes(Lifespan span, PathPattern pattern, TraceObjectValue value) {
|
||||
List<String> asPath = pattern.asPath();
|
||||
if (asPath.isEmpty()) {
|
||||
// If the pattern is the root, then only match the "root value"
|
||||
return value.getParent() == null;
|
||||
}
|
||||
if (!PathPredicates.keyMatches(PathUtils.getKey(asPath), value.getEntryKey())) {
|
||||
return false;
|
||||
}
|
||||
TraceObject parent = value.getParent();
|
||||
if (parent == null) {
|
||||
// Value is the root. We would already have matched above
|
||||
return false;
|
||||
}
|
||||
return parent.getAncestors(span, pattern.removeRight(1))
|
||||
.anyMatch(v -> v.getSource(parent).isRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether this query would include the given value in its result
|
||||
*
|
||||
@@ -144,21 +178,57 @@ public class ModelQuery {
|
||||
* @return true if the value would be accepted
|
||||
*/
|
||||
public boolean includes(Lifespan 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 (!span.intersects(value.getLifespan())) {
|
||||
return false;
|
||||
}
|
||||
for (PathPattern pattern : predicates.getPatterns()) {
|
||||
if (includes(span, pattern, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static boolean involves(Lifespan span, PathPattern pattern, TraceObjectValue value) {
|
||||
TraceObject parent = value.getParent();
|
||||
// Every query involves the root
|
||||
if (parent == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any of the value's paths could be an ancestor of a result
|
||||
List<String> asPath = new ArrayList<>(pattern.asPath());
|
||||
// Destroy the pattern from the right, thus iterating each ancestor
|
||||
while (!asPath.isEmpty()) {
|
||||
// The value's key much match somewhere in the pattern to be involved
|
||||
if (!PathPredicates.keyMatches(PathUtils.getKey(asPath), value.getEntryKey())) {
|
||||
asPath.remove(asPath.size() - 1);
|
||||
continue;
|
||||
}
|
||||
// If it does, then check if any path to the value's parent matches the rest
|
||||
asPath.remove(asPath.size() - 1);
|
||||
if (parent.getAncestors(span, new PathPattern(asPath))
|
||||
.anyMatch(v -> v.getSource(parent).isRoot())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the query results could depend on the given value
|
||||
*
|
||||
* @return true if the query results depend on the given value
|
||||
*/
|
||||
public boolean involves(Lifespan span, TraceObjectValue value) {
|
||||
if (!span.intersects(value.getLifespan())) {
|
||||
return false;
|
||||
}
|
||||
return parent.getAncestors(span, predicates.removeRight(1))
|
||||
.anyMatch(v -> v.getSource(parent).isRoot());
|
||||
for (PathPattern pattern : predicates.getPatterns()) {
|
||||
if (involves(span, pattern, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+92
-17
@@ -37,9 +37,6 @@ 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) {
|
||||
@@ -272,6 +269,19 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
}
|
||||
}
|
||||
|
||||
static class AutoAttributeColumn extends TraceValueObjectAttributeColumn {
|
||||
public static TraceValueObjectAttributeColumn fromSchema(SchemaContext ctx,
|
||||
AttributeSchema attributeSchema) {
|
||||
String name = attributeSchema.getName();
|
||||
Class<?> type = computeColumnType(ctx, attributeSchema);
|
||||
return new AutoAttributeColumn(name, type);
|
||||
}
|
||||
|
||||
public AutoAttributeColumn(String attributeName, Class<?> attributeType) {
|
||||
super(attributeName, attributeType);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Save and restore these between sessions, esp., their settings
|
||||
private Map<ColKey, TraceValueObjectAttributeColumn> columnCache = new HashMap<>();
|
||||
|
||||
@@ -307,7 +317,13 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
protected void updateTimelineMax() {
|
||||
Long max = getTrace() == null ? null : getTrace().getTimeManager().getMaxSnap();
|
||||
Lifespan fullRange = Lifespan.span(0L, max == null ? 1 : max + 1);
|
||||
lifePlotColumn.setFullRange(fullRange);
|
||||
int count = getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
|
||||
if (column instanceof TraceValueLifePlotColumn plotCol) {
|
||||
plotCol.setFullRange(fullRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected List<AttributeSchema> computeAttributeSchemas() {
|
||||
@@ -337,7 +353,6 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
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());
|
||||
}
|
||||
@@ -346,6 +361,9 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
|
||||
protected Set<DynamicTableColumn<ValueRow, ?, ?>> computeAttributeColumns(
|
||||
Collection<AttributeSchema> attributes) {
|
||||
if (attributes == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Trace trace = getTrace();
|
||||
if (trace == null) {
|
||||
return Set.of();
|
||||
@@ -357,25 +375,31 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
SchemaContext ctx = rootSchema.getContext();
|
||||
return attributes.stream()
|
||||
.map(as -> columnCache.computeIfAbsent(ColKey.fromSchema(ctx, as),
|
||||
ck -> TraceValueObjectAttributeColumn.fromSchema(ctx, as)))
|
||||
ck -> AutoAttributeColumn.fromSchema(ctx, as)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
protected void resyncAttributeColumns(Collection<AttributeSchema> attributes) {
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> columns =
|
||||
new HashSet<>(computeAttributeColumns(attributes));
|
||||
Map<Boolean, List<AttributeSchema>> byVisible = attributes == null ? Map.of()
|
||||
: attributes.stream()
|
||||
.collect(Collectors.groupingBy(a -> !a.isHidden() || isShowHidden()));
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> visibleColumns =
|
||||
new HashSet<>(computeAttributeColumns(byVisible.get(true)));
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> hiddenColumns =
|
||||
new HashSet<>(computeAttributeColumns(byVisible.get(false)));
|
||||
Set<DynamicTableColumn<ValueRow, ?, ?>> toRemove = new HashSet<>();
|
||||
for (int i = 0; i < getColumnCount(); i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> exists = getColumn(i);
|
||||
if (!(exists instanceof TraceValueObjectAttributeColumn)) {
|
||||
if (!(exists instanceof AutoAttributeColumn)) {
|
||||
continue;
|
||||
}
|
||||
if (!columns.remove(exists)) {
|
||||
if (!visibleColumns.remove(exists) && !hiddenColumns.remove(exists)) {
|
||||
toRemove.add(exists);
|
||||
}
|
||||
}
|
||||
removeTableColumns(toRemove);
|
||||
addTableColumns(columns);
|
||||
addTableColumns(visibleColumns, true);
|
||||
addTableColumns(hiddenColumns, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -389,9 +413,9 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<ValueRow> descriptor = new TableColumnDescriptor<>();
|
||||
descriptor.addVisibleColumn(new TraceValueKeyColumn());
|
||||
descriptor.addVisibleColumn(valueColumn = new TraceValueValColumn());
|
||||
descriptor.addVisibleColumn(new TraceValueValColumn());
|
||||
descriptor.addVisibleColumn(new TraceValueLifeColumn());
|
||||
descriptor.addHiddenColumn(lifePlotColumn = new TraceValueLifePlotColumn());
|
||||
descriptor.addHiddenColumn(new TraceValueLifePlotColumn());
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@@ -405,9 +429,38 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the row whose object is the canonical ancestor to the given object
|
||||
*
|
||||
* @param successor the given object
|
||||
* @return the row or null
|
||||
*/
|
||||
public ValueRow findTraceObjectAncestor(TraceObject successor) {
|
||||
for (ValueRow row : getModelData()) {
|
||||
TraceObjectValue value = row.getValue();
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
if (!value.getChild().getCanonicalPath().isAncestor(successor.getCanonicalPath())) {
|
||||
continue;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDiffColor(Color diffColor) {
|
||||
valueColumn.setDiffColor(diffColor);
|
||||
int count = getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
|
||||
if (column instanceof TraceValueObjectAttributeColumn attrCol) {
|
||||
attrCol.setDiffColor(diffColor);
|
||||
}
|
||||
else if (column instanceof TraceValueValColumn valCol) {
|
||||
valCol.setDiffColor(diffColor);
|
||||
}
|
||||
}
|
||||
for (TraceValueObjectAttributeColumn column : columnCache.values()) {
|
||||
column.setDiffColor(diffColor);
|
||||
}
|
||||
@@ -415,7 +468,16 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
|
||||
@Override
|
||||
public void setDiffColorSel(Color diffColorSel) {
|
||||
valueColumn.setDiffColorSel(diffColorSel);
|
||||
int count = getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
|
||||
if (column instanceof TraceValueObjectAttributeColumn attrCol) {
|
||||
attrCol.setDiffColorSel(diffColorSel);
|
||||
}
|
||||
else if (column instanceof TraceValueValColumn valCol) {
|
||||
valCol.setDiffColorSel(diffColorSel);
|
||||
}
|
||||
}
|
||||
for (TraceValueObjectAttributeColumn column : columnCache.values()) {
|
||||
column.setDiffColorSel(diffColorSel);
|
||||
}
|
||||
@@ -424,11 +486,24 @@ public class ObjectTableModel extends AbstractQueryTableModel<ValueRow> {
|
||||
@Override
|
||||
protected void snapChanged() {
|
||||
super.snapChanged();
|
||||
lifePlotColumn.setSnap(getSnap());
|
||||
long snap = getSnap();
|
||||
int count = getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
|
||||
if (column instanceof TraceValueLifePlotColumn plotCol) {
|
||||
plotCol.setSnap(snap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSeekListener(SeekListener listener) {
|
||||
lifePlotColumn.addSeekListener(listener);
|
||||
int count = getColumnCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
DynamicTableColumn<ValueRow, ?, ?> column = getColumn(i);
|
||||
if (column instanceof TraceValueLifePlotColumn plotCol) {
|
||||
plotCol.addSeekListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-3
@@ -24,6 +24,7 @@ import docking.widgets.tree.GTreeLazyNode;
|
||||
import docking.widgets.tree.GTreeNode;
|
||||
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
|
||||
import ghidra.dbg.target.*;
|
||||
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
||||
import ghidra.framework.model.DomainObjectClosedListener;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.Trace.TraceObjectChangeType;
|
||||
@@ -189,6 +190,19 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
public abstract class AbstractNode extends GTreeLazyNode {
|
||||
public abstract TraceObjectValue getValue();
|
||||
|
||||
@Override
|
||||
public int compareTo(GTreeNode node) {
|
||||
return TargetObjectKeyComparator.CHILD.compare(this.getName(), node.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getValue().getEntryKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract String getDisplayText();
|
||||
|
||||
protected void childCreated(TraceObjectValue value) {
|
||||
if (getParent() == null || !isLoaded()) {
|
||||
return;
|
||||
@@ -249,6 +263,11 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
if (trace == null) {
|
||||
return "<html><em>No trace is active</em>";
|
||||
}
|
||||
@@ -332,7 +351,7 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
public String getDisplayText() {
|
||||
String html = HTMLUtilities.escapeHTML(
|
||||
value.getEntryKey() + ": " + display.getPrimitiveValueDisplay(value.getValue()));
|
||||
return "<html>" + html;
|
||||
@@ -380,7 +399,7 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
public String getDisplayText() {
|
||||
return "<html>" + HTMLUtilities.escapeHTML(value.getEntryKey()) + ": <em>" +
|
||||
HTMLUtilities.escapeHTML(display.getObjectLinkDisplay(value)) + "</em>";
|
||||
}
|
||||
@@ -422,7 +441,7 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
public String getDisplayText() {
|
||||
return "<html>" + HTMLUtilities.escapeHTML(display.getObjectDisplay(value));
|
||||
}
|
||||
|
||||
@@ -606,6 +625,7 @@ public class ObjectTreeModel implements DisplaysModified {
|
||||
List<GTreeNode> result = ObjectTableModel
|
||||
.distinctCanonical(object.getValues().stream().filter(this::isValueVisible))
|
||||
.map(v -> nodeCache.getOrCreateNode(v))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
return result;
|
||||
}
|
||||
|
||||
+12
-2
@@ -17,14 +17,24 @@ package ghidra.app.plugin.core.debug.gui.model;
|
||||
|
||||
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.trace.model.target.TraceObject;
|
||||
|
||||
public class ObjectsTablePanel extends AbstractQueryTablePanel<ValueRow> {
|
||||
public class ObjectsTablePanel extends AbstractQueryTablePanel<ValueRow, ObjectTableModel> {
|
||||
public ObjectsTablePanel(Plugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractQueryTableModel<ValueRow> createModel(Plugin plugin) {
|
||||
protected ObjectTableModel createModel(Plugin plugin) {
|
||||
return new ObjectTableModel(plugin);
|
||||
}
|
||||
|
||||
public boolean trySelectAncestor(TraceObject successor) {
|
||||
ValueRow row = tableModel.findTraceObjectAncestor(successor);
|
||||
if (row == null) {
|
||||
return false;
|
||||
}
|
||||
setSelectedItem(row);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,13 +18,13 @@ 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 class PathsTablePanel extends AbstractQueryTablePanel<PathRow, PathTableModel> {
|
||||
public PathsTablePanel(Plugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractQueryTableModel<PathRow> createModel(Plugin plugin) {
|
||||
protected PathTableModel createModel(Plugin plugin) {
|
||||
return new PathTableModel(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -15,8 +15,11 @@
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.model.columns;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
|
||||
import ghidra.dbg.util.PathUtils.TargetObjectKeyComparator;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.trace.model.Trace;
|
||||
@@ -32,4 +35,9 @@ public class TraceValueKeyColumn extends AbstractDynamicTableColumn<ValueRow, St
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
return rowObject.getKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<String> getComparator() {
|
||||
return TargetObjectKeyComparator.CHILD;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -105,13 +105,6 @@ public class TraceValueObjectAttributeColumn
|
||||
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();
|
||||
@@ -155,7 +148,7 @@ public class TraceValueObjectAttributeColumn
|
||||
new ColumnRenderedValueBackupComparator<>(model, columnIndex));
|
||||
}
|
||||
|
||||
protected Object getAttributeValue(ValueRow row) {
|
||||
public Object getAttributeValue(ValueRow row) {
|
||||
TraceObjectValue edge = row.getAttribute(attributeName);
|
||||
return edge == null ? null : edge.getValue();
|
||||
}
|
||||
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
/* ###
|
||||
* 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.stack;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.table.TableColumn;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
|
||||
import docking.widgets.table.CustomToStringCellRenderer;
|
||||
import docking.widgets.table.DefaultEnumeratedColumnTableModel;
|
||||
import docking.widgets.table.DefaultEnumeratedColumnTableModel.EnumeratedTableColumn;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.services.*;
|
||||
import ghidra.dbg.DebugModelConventions;
|
||||
import ghidra.dbg.target.TargetStackFrame;
|
||||
import ghidra.framework.plugintool.AutoService;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.lang.Register;
|
||||
import ghidra.program.model.lang.RegisterValue;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.Trace.TraceMemoryBytesChangeType;
|
||||
import ghidra.trace.model.Trace.TraceStackChangeType;
|
||||
import ghidra.trace.model.memory.TraceMemorySpace;
|
||||
import ghidra.trace.model.stack.TraceStack;
|
||||
import ghidra.trace.model.stack.TraceStackFrame;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
import ghidra.trace.util.TraceAddressSpace;
|
||||
import ghidra.trace.util.TraceRegisterUtils;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.table.GhidraTable;
|
||||
import ghidra.util.table.GhidraTableFilterPanel;
|
||||
import utilities.util.SuppressableCallback;
|
||||
import utilities.util.SuppressableCallback.Suppression;
|
||||
|
||||
public class DebuggerLegacyStackPanel extends JPanel {
|
||||
|
||||
protected enum StackTableColumns
|
||||
implements EnumeratedTableColumn<StackTableColumns, StackFrameRow> {
|
||||
LEVEL("Level", Integer.class, StackFrameRow::getFrameLevel),
|
||||
PC("PC", Address.class, StackFrameRow::getProgramCounter),
|
||||
FUNCTION("Function", ghidra.program.model.listing.Function.class, StackFrameRow::getFunction),
|
||||
COMMENT("Comment", String.class, StackFrameRow::getComment, StackFrameRow::setComment, StackFrameRow::isCommentable);
|
||||
|
||||
private final String header;
|
||||
private final Function<StackFrameRow, ?> getter;
|
||||
private final BiConsumer<StackFrameRow, Object> setter;
|
||||
private final Predicate<StackFrameRow> editable;
|
||||
private final Class<?> cls;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T> StackTableColumns(String header, Class<T> cls, Function<StackFrameRow, T> getter,
|
||||
BiConsumer<StackFrameRow, T> setter, Predicate<StackFrameRow> editable) {
|
||||
this.header = header;
|
||||
this.cls = cls;
|
||||
this.getter = getter;
|
||||
this.setter = (BiConsumer<StackFrameRow, Object>) setter;
|
||||
this.editable = editable;
|
||||
}
|
||||
|
||||
<T> StackTableColumns(String header, Class<T> cls, Function<StackFrameRow, T> getter) {
|
||||
this(header, cls, getter, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getValueClass() {
|
||||
return cls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueOf(StackFrameRow row) {
|
||||
return getter.apply(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueOf(StackFrameRow row, Object value) {
|
||||
setter.accept(row, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEditable(StackFrameRow row) {
|
||||
return setter != null && editable.test(row);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class StackTableModel
|
||||
extends DefaultEnumeratedColumnTableModel<StackTableColumns, StackFrameRow> {
|
||||
|
||||
public StackTableModel(PluginTool tool) {
|
||||
super(tool, "Stack", StackTableColumns.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StackTableColumns> defaultSortOrder() {
|
||||
return List.of(StackTableColumns.LEVEL);
|
||||
}
|
||||
}
|
||||
|
||||
class ForStackListener extends TraceDomainObjectListener {
|
||||
public ForStackListener() {
|
||||
listenFor(TraceStackChangeType.ADDED, this::stackAdded);
|
||||
listenFor(TraceStackChangeType.CHANGED, this::stackChanged);
|
||||
listenFor(TraceStackChangeType.DELETED, this::stackDeleted);
|
||||
|
||||
listenFor(TraceMemoryBytesChangeType.CHANGED, this::bytesChanged);
|
||||
}
|
||||
|
||||
private void stackAdded(TraceStack stack) {
|
||||
if (stack.getSnap() != current.getViewSnap()) {
|
||||
return;
|
||||
}
|
||||
TraceThread curThread = current.getThread();
|
||||
if (curThread != stack.getThread()) {
|
||||
return;
|
||||
}
|
||||
loadStack();
|
||||
}
|
||||
|
||||
private void stackChanged(TraceStack stack) {
|
||||
if (currentStack != stack) {
|
||||
return;
|
||||
}
|
||||
updateStack();
|
||||
}
|
||||
|
||||
private void stackDeleted(TraceStack stack) {
|
||||
if (currentStack != stack) {
|
||||
return;
|
||||
}
|
||||
loadStack();
|
||||
}
|
||||
|
||||
// For updating a synthetic frame
|
||||
private void bytesChanged(TraceAddressSpace space, TraceAddressSnapRange range) {
|
||||
TraceThread curThread = current.getThread();
|
||||
if (space.getThread() != curThread || space.getFrameLevel() != 0) {
|
||||
return;
|
||||
}
|
||||
if (!current.getView().getViewport().containsAnyUpper(range.getLifespan())) {
|
||||
return;
|
||||
}
|
||||
List<StackFrameRow> stackData = stackTableModel.getModelData();
|
||||
if (stackData.isEmpty() || !(stackData.get(0) instanceof StackFrameRow.Synthetic)) {
|
||||
return;
|
||||
}
|
||||
StackFrameRow.Synthetic frameRow = (StackFrameRow.Synthetic) stackData.get(0);
|
||||
Trace trace = current.getTrace();
|
||||
Register pc = trace.getBaseLanguage().getProgramCounter();
|
||||
if (!TraceRegisterUtils.rangeForRegister(pc).intersects(range.getRange())) {
|
||||
return;
|
||||
}
|
||||
TraceMemorySpace regs =
|
||||
trace.getMemoryManager().getMemoryRegisterSpace(curThread, false);
|
||||
RegisterValue value = regs.getViewValue(current.getViewSnap(), pc);
|
||||
Address address = trace.getBaseLanguage()
|
||||
.getDefaultSpace()
|
||||
.getAddress(value.getUnsignedValue().longValue());
|
||||
frameRow.updateProgramCounter(address);
|
||||
stackTableModel.fireTableDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
class ForFunctionsListener implements DebuggerStaticMappingChangeListener {
|
||||
@Override
|
||||
public void mappingsChanged(Set<Trace> affectedTraces, Set<Program> affectedPrograms) {
|
||||
Trace curTrace = current.getTrace();
|
||||
if (curTrace == null || !affectedTraces.contains(curTrace)) {
|
||||
return;
|
||||
}
|
||||
Swing.runIfSwingOrRunLater(() -> stackTableModel.fireTableDataChanged());
|
||||
}
|
||||
}
|
||||
|
||||
final DebuggerStackProvider provider;
|
||||
|
||||
@AutoServiceConsumed
|
||||
private DebuggerTraceManagerService traceManager;
|
||||
// @AutoServiceConsumed by method
|
||||
private DebuggerModelService modelService;
|
||||
// @AutoServiceConsumed via method
|
||||
DebuggerStaticMappingService mappingService;
|
||||
@AutoServiceConsumed
|
||||
private DebuggerListingService listingService; // TODO: Goto pc on double-click
|
||||
@AutoServiceConsumed
|
||||
private MarkerService markerService; // TODO: Mark non-current frame PCs, too. (separate plugin?)
|
||||
@SuppressWarnings("unused")
|
||||
private final AutoService.Wiring autoServiceWiring;
|
||||
|
||||
// Table rows access this for function name resolution
|
||||
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
|
||||
private Trace currentTrace; // Copy for transition
|
||||
|
||||
private TraceStack currentStack;
|
||||
|
||||
private ForStackListener forStackListener = new ForStackListener();
|
||||
private ForFunctionsListener forFunctionsListener = new ForFunctionsListener();
|
||||
|
||||
private final SuppressableCallback<Void> cbFrameSelected = new SuppressableCallback<>();
|
||||
|
||||
protected final StackTableModel stackTableModel;
|
||||
protected GhidraTable stackTable;
|
||||
protected GhidraTableFilterPanel<StackFrameRow> stackFilterPanel;
|
||||
|
||||
private DebuggerStackActionContext myActionContext;
|
||||
|
||||
public DebuggerLegacyStackPanel(DebuggerStackPlugin plugin, DebuggerStackProvider provider) {
|
||||
super(new BorderLayout());
|
||||
this.provider = provider;
|
||||
stackTableModel = new StackTableModel(provider.getTool());
|
||||
|
||||
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
|
||||
|
||||
stackTable = new GhidraTable(stackTableModel);
|
||||
add(new JScrollPane(stackTable));
|
||||
stackFilterPanel = new GhidraTableFilterPanel<>(stackTable, stackTableModel);
|
||||
add(stackFilterPanel, BorderLayout.SOUTH);
|
||||
|
||||
stackTable.getSelectionModel().addListSelectionListener(evt -> {
|
||||
if (evt.getValueIsAdjusting()) {
|
||||
return;
|
||||
}
|
||||
contextChanged();
|
||||
activateSelectedFrame();
|
||||
});
|
||||
|
||||
stackTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() < 2 || e.getButton() != MouseEvent.BUTTON1) {
|
||||
return;
|
||||
}
|
||||
if (listingService == null) {
|
||||
return;
|
||||
}
|
||||
if (myActionContext == null) {
|
||||
return;
|
||||
}
|
||||
Address pc = myActionContext.getFrame().getProgramCounter();
|
||||
if (pc == null) {
|
||||
return;
|
||||
}
|
||||
listingService.goTo(pc, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
int selectedRow = stackTable.getSelectedRow();
|
||||
StackFrameRow row = stackTableModel.getRowObject(selectedRow);
|
||||
rowActivated(row);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Adjust default column widths?
|
||||
TableColumnModel columnModel = stackTable.getColumnModel();
|
||||
|
||||
TableColumn levelCol = columnModel.getColumn(StackTableColumns.LEVEL.ordinal());
|
||||
levelCol.setPreferredWidth(25);
|
||||
TableColumn baseCol = columnModel.getColumn(StackTableColumns.PC.ordinal());
|
||||
baseCol.setCellRenderer(CustomToStringCellRenderer.MONO_OBJECT);
|
||||
}
|
||||
|
||||
protected void contextChanged() {
|
||||
StackFrameRow row = stackFilterPanel.getSelectedItem();
|
||||
myActionContext =
|
||||
row == null ? null : new DebuggerStackActionContext(provider, row, stackTable);
|
||||
provider.contextChanged();
|
||||
}
|
||||
|
||||
protected void activateSelectedFrame() {
|
||||
// TODO: Action to toggle sync?
|
||||
if (myActionContext == null) {
|
||||
return;
|
||||
}
|
||||
if (traceManager == null) {
|
||||
return;
|
||||
}
|
||||
cbFrameSelected.invoke(() -> {
|
||||
traceManager.activateFrame(myActionContext.getFrame().getFrameLevel());
|
||||
});
|
||||
}
|
||||
|
||||
private void rowActivated(StackFrameRow row) {
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
TraceStackFrame frame = row.frame;
|
||||
if (frame == null) {
|
||||
return;
|
||||
}
|
||||
TraceThread thread = frame.getStack().getThread();
|
||||
Trace trace = thread.getTrace();
|
||||
TraceRecorder recorder = modelService.getRecorder(trace);
|
||||
if (recorder == null) {
|
||||
return;
|
||||
}
|
||||
TargetStackFrame targetFrame = recorder.getTargetStackFrame(thread, frame.getLevel());
|
||||
if (targetFrame == null || !targetFrame.isValid()) {
|
||||
return;
|
||||
}
|
||||
DebugModelConventions.requestActivation(targetFrame);
|
||||
}
|
||||
|
||||
protected void updateStack() {
|
||||
Set<TraceStackFrame> toAdd = new LinkedHashSet<>(currentStack.getFrames(current.getSnap()));
|
||||
for (Iterator<StackFrameRow> it = stackTableModel.getModelData().iterator(); it
|
||||
.hasNext();) {
|
||||
StackFrameRow row = it.next();
|
||||
if (!toAdd.remove(row.frame)) {
|
||||
it.remove();
|
||||
}
|
||||
else {
|
||||
row.update();
|
||||
}
|
||||
}
|
||||
|
||||
for (TraceStackFrame frame : toAdd) {
|
||||
stackTableModel.add(new StackFrameRow(this, frame));
|
||||
}
|
||||
|
||||
stackTableModel.fireTableDataChanged();
|
||||
|
||||
selectCurrentFrame();
|
||||
}
|
||||
|
||||
protected void doSetCurrentStack(TraceStack stack) {
|
||||
if (stack == null) {
|
||||
currentStack = null;
|
||||
stackTableModel.clear();
|
||||
contextChanged();
|
||||
return;
|
||||
}
|
||||
if (currentStack == stack && stack.hasFixedFrames()) {
|
||||
stackTableModel.fireTableDataChanged();
|
||||
return;
|
||||
}
|
||||
currentStack = stack;
|
||||
stackTableModel.clear();
|
||||
for (TraceStackFrame frame : currentStack.getFrames(current.getSnap())) {
|
||||
stackTableModel.add(new StackFrameRow(this, frame));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize a stack with only one frame, taking PC from the registers
|
||||
*/
|
||||
protected void doSetSyntheticStack() {
|
||||
stackTableModel.clear();
|
||||
currentStack = null;
|
||||
|
||||
Trace curTrace = current.getTrace();
|
||||
TraceMemorySpace regs =
|
||||
curTrace.getMemoryManager().getMemoryRegisterSpace(current.getThread(), false);
|
||||
if (regs == null) {
|
||||
contextChanged();
|
||||
return;
|
||||
}
|
||||
Register pc = curTrace.getBaseLanguage().getProgramCounter();
|
||||
if (pc == null) {
|
||||
contextChanged();
|
||||
return;
|
||||
}
|
||||
RegisterValue value = regs.getViewValue(current.getViewSnap(), pc);
|
||||
if (value == null) {
|
||||
contextChanged();
|
||||
return;
|
||||
}
|
||||
Address address = curTrace.getBaseLanguage()
|
||||
.getDefaultSpace()
|
||||
.getAddress(value.getUnsignedValue().longValue(), true);
|
||||
stackTableModel.add(new StackFrameRow.Synthetic(this, address));
|
||||
}
|
||||
|
||||
protected void loadStack() {
|
||||
TraceThread curThread = current.getThread();
|
||||
if (curThread == null) {
|
||||
doSetCurrentStack(null);
|
||||
return;
|
||||
}
|
||||
// TODO: getLatestViewStack? Conventionally, I don't expect any scratch stacks, yet.
|
||||
TraceStack stack =
|
||||
current.getTrace().getStackManager().getLatestStack(curThread, current.getViewSnap());
|
||||
if (stack == null) {
|
||||
doSetSyntheticStack();
|
||||
}
|
||||
else {
|
||||
doSetCurrentStack(stack);
|
||||
}
|
||||
selectCurrentFrame();
|
||||
}
|
||||
|
||||
private void removeOldListeners() {
|
||||
if (currentTrace == null) {
|
||||
return;
|
||||
}
|
||||
currentTrace.removeListener(forStackListener);
|
||||
}
|
||||
|
||||
private void addNewListeners() {
|
||||
if (currentTrace == null) {
|
||||
return;
|
||||
}
|
||||
currentTrace.addListener(forStackListener);
|
||||
}
|
||||
|
||||
private void doSetTrace(Trace trace) {
|
||||
if (currentTrace == trace) {
|
||||
return;
|
||||
}
|
||||
removeOldListeners();
|
||||
this.currentTrace = trace;
|
||||
addNewListeners();
|
||||
}
|
||||
|
||||
public void coordinatesActivated(DebuggerCoordinates coordinates) {
|
||||
current = coordinates;
|
||||
doSetTrace(current.getTrace());
|
||||
loadStack();
|
||||
}
|
||||
|
||||
protected void selectCurrentFrame() {
|
||||
try (Suppression supp = cbFrameSelected.suppress(null)) {
|
||||
StackFrameRow row =
|
||||
stackTableModel.findFirst(r -> r.getFrameLevel() == current.getFrame());
|
||||
if (row == null) {
|
||||
// Strange
|
||||
stackTable.clearSelection();
|
||||
}
|
||||
else {
|
||||
stackFilterPanel.setSelectedItem(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DebuggerStackActionContext getActionContext() {
|
||||
return myActionContext;
|
||||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
public void setModelService(DebuggerModelService modelService) {
|
||||
this.modelService = modelService;
|
||||
}
|
||||
|
||||
@AutoServiceConsumed
|
||||
private void setMappingService(DebuggerStaticMappingService mappingService) {
|
||||
if (this.mappingService != null) {
|
||||
this.mappingService.removeChangeListener(forFunctionsListener);
|
||||
}
|
||||
this.mappingService = mappingService;
|
||||
if (this.mappingService != null) {
|
||||
this.mappingService.addChangeListener(forFunctionsListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/* ###
|
||||
* 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.stack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||
import docking.widgets.table.TableColumnDescriptor;
|
||||
import ghidra.app.plugin.core.debug.DebuggerCoordinates;
|
||||
import ghidra.app.plugin.core.debug.gui.model.*;
|
||||
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.ValueRow;
|
||||
import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueKeyColumn;
|
||||
import ghidra.app.plugin.core.debug.gui.model.columns.TraceValueObjectAttributeColumn;
|
||||
import ghidra.app.services.DebuggerTraceManagerService;
|
||||
import ghidra.dbg.target.TargetStack;
|
||||
import ghidra.dbg.target.TargetStackFrame;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema;
|
||||
import ghidra.dbg.util.PathMatcher;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.*;
|
||||
import utilities.util.SuppressableCallback;
|
||||
import utilities.util.SuppressableCallback.Suppression;
|
||||
|
||||
public class DebuggerStackPanel extends ObjectsTablePanel implements ListSelectionListener {
|
||||
|
||||
private static class FrameLevelColumn extends TraceValueKeyColumn {
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Level";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 48;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FramePcColumn extends TraceValueObjectAttributeColumn {
|
||||
public FramePcColumn() {
|
||||
super(TargetStackFrame.PC_ATTRIBUTE_NAME, Address.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "PC";
|
||||
}
|
||||
}
|
||||
|
||||
private class FrameFunctionColumn
|
||||
extends AbstractDynamicTableColumn<ValueRow, Function, Trace> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Function";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getValue(ValueRow rowObject, Settings settings, Trace data,
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
TraceObjectValue value = rowObject.getAttribute(TargetStackFrame.PC_ATTRIBUTE_NAME);
|
||||
return value == null ? null : provider.getFunction((Address) value.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private class StackTableModel extends ObjectTableModel {
|
||||
protected StackTableModel(Plugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<ValueRow> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<ValueRow> descriptor = new TableColumnDescriptor<>();
|
||||
descriptor.addVisibleColumn(new FrameLevelColumn());
|
||||
descriptor.addVisibleColumn(new FramePcColumn());
|
||||
descriptor.addVisibleColumn(new FrameFunctionColumn());
|
||||
// TODO: Comment column?
|
||||
return descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
private final DebuggerStackProvider provider;
|
||||
|
||||
@AutoServiceConsumed
|
||||
protected DebuggerTraceManagerService traceManager;
|
||||
@SuppressWarnings("unused")
|
||||
private final AutoService.Wiring autoServiceWiring;
|
||||
|
||||
private final SuppressableCallback<Void> cbFrameSelected = new SuppressableCallback<>();
|
||||
|
||||
private DebuggerObjectActionContext myActionContext;
|
||||
|
||||
public DebuggerStackPanel(Plugin plugin, DebuggerStackProvider provider) {
|
||||
super(plugin);
|
||||
this.provider = provider;
|
||||
|
||||
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
|
||||
|
||||
setLimitToSnap(true);
|
||||
setShowHidden(false);
|
||||
|
||||
addSelectionListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObjectTableModel createModel(Plugin plugin) {
|
||||
return new StackTableModel(plugin);
|
||||
}
|
||||
|
||||
public ActionContext getActionContext() {
|
||||
return myActionContext;
|
||||
}
|
||||
|
||||
protected ModelQuery computeQuery(TraceObject object) {
|
||||
if (object == null) {
|
||||
return ModelQuery.EMPTY;
|
||||
}
|
||||
TargetObjectSchema rootSchema = object.getRoot()
|
||||
.getTargetSchema();
|
||||
List<String> stackPath = rootSchema
|
||||
.searchForSuitable(TargetStack.class, object.getCanonicalPath().getKeyList());
|
||||
if (stackPath == null) {
|
||||
return ModelQuery.EMPTY;
|
||||
}
|
||||
TargetObjectSchema stackSchema = rootSchema.getSuccessorSchema(stackPath);
|
||||
PathMatcher matcher = stackSchema.searchFor(TargetStackFrame.class, stackPath, true);
|
||||
return new ModelQuery(matcher);
|
||||
}
|
||||
|
||||
public void coordinatesActivated(DebuggerCoordinates coordinates) {
|
||||
TraceObject object = coordinates.getObject();
|
||||
setQuery(computeQuery(object));
|
||||
goToCoordinates(coordinates);
|
||||
|
||||
if (object != null) {
|
||||
try (Suppression supp = cbFrameSelected.suppress(null)) {
|
||||
trySelectAncestor(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent e) {
|
||||
if (e.getValueIsAdjusting()) {
|
||||
return;
|
||||
}
|
||||
List<ValueRow> sel = getSelectedItems();
|
||||
if (!sel.isEmpty()) {
|
||||
myActionContext = new DebuggerObjectActionContext(
|
||||
sel.stream().map(r -> r.getValue()).collect(Collectors.toList()), provider, this);
|
||||
}
|
||||
ValueRow item = getSelectedItem();
|
||||
if (item != null) {
|
||||
cbFrameSelected.invoke(() -> traceManager.activateObject(item.getValue().getChild()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-15
@@ -22,20 +22,24 @@ import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
|
||||
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
|
||||
@PluginInfo( //
|
||||
shortDescription = "Debugger stack view", //
|
||||
description = "GUI to navigate the stack", //
|
||||
category = PluginCategoryNames.DEBUGGER, //
|
||||
packageName = DebuggerPluginPackage.NAME, //
|
||||
status = PluginStatus.RELEASED, //
|
||||
eventsConsumed = {
|
||||
TraceActivatedPluginEvent.class, //
|
||||
TraceClosedPluginEvent.class, //
|
||||
}, //
|
||||
servicesRequired = { //
|
||||
} //
|
||||
)
|
||||
@PluginInfo(
|
||||
shortDescription = "Debugger stack view",
|
||||
description = "GUI to navigate the stack",
|
||||
category = PluginCategoryNames.DEBUGGER,
|
||||
packageName = DebuggerPluginPackage.NAME,
|
||||
status = PluginStatus.RELEASED,
|
||||
eventsConsumed = {
|
||||
TraceActivatedPluginEvent.class,
|
||||
TraceClosedPluginEvent.class,
|
||||
},
|
||||
servicesRequired = {
|
||||
})
|
||||
public class DebuggerStackPlugin extends AbstractDebuggerPlugin {
|
||||
|
||||
protected DebuggerStackProvider provider;
|
||||
@@ -58,9 +62,11 @@ public class DebuggerStackPlugin extends AbstractDebuggerPlugin {
|
||||
@Override
|
||||
public void processEvent(PluginEvent event) {
|
||||
super.processEvent(event);
|
||||
if (event instanceof TraceActivatedPluginEvent) {
|
||||
TraceActivatedPluginEvent ev = (TraceActivatedPluginEvent) event;
|
||||
if (event instanceof TraceActivatedPluginEvent ev) {
|
||||
provider.coordinatesActivated(ev.getActiveCoordinates());
|
||||
}
|
||||
else if (event instanceof TraceClosedPluginEvent ev) {
|
||||
provider.traceClosed(ev.getTrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-425
File diff suppressed because it is too large
Load Diff
+9
-29
@@ -17,18 +17,15 @@ package ghidra.app.plugin.core.debug.gui.stack;
|
||||
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.trace.model.*;
|
||||
import ghidra.trace.model.stack.TraceStackFrame;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
|
||||
public class StackFrameRow {
|
||||
public static class Synthetic extends StackFrameRow {
|
||||
private Address pc;
|
||||
|
||||
public Synthetic(DebuggerStackProvider provider, Address pc) {
|
||||
super(provider);
|
||||
public Synthetic(DebuggerLegacyStackPanel panel, Address pc) {
|
||||
super(panel);
|
||||
this.pc = pc;
|
||||
}
|
||||
|
||||
@@ -42,19 +39,19 @@ public class StackFrameRow {
|
||||
}
|
||||
}
|
||||
|
||||
private final DebuggerStackProvider provider;
|
||||
private final DebuggerLegacyStackPanel panel;
|
||||
|
||||
final TraceStackFrame frame;
|
||||
private int level;
|
||||
|
||||
public StackFrameRow(DebuggerStackProvider provider, TraceStackFrame frame) {
|
||||
this.provider = provider;
|
||||
public StackFrameRow(DebuggerLegacyStackPanel panel, TraceStackFrame frame) {
|
||||
this.panel = panel;
|
||||
this.frame = frame;
|
||||
this.level = frame.getLevel();
|
||||
}
|
||||
|
||||
private StackFrameRow(DebuggerStackProvider provider) {
|
||||
this.provider = provider;
|
||||
private StackFrameRow(DebuggerLegacyStackPanel panel) {
|
||||
this.panel = panel;
|
||||
this.frame = null;
|
||||
this.level = 0;
|
||||
}
|
||||
@@ -64,7 +61,7 @@ public class StackFrameRow {
|
||||
}
|
||||
|
||||
public long getSnap() {
|
||||
return provider.current.getSnap();
|
||||
return panel.current.getSnap();
|
||||
}
|
||||
|
||||
public Address getProgramCounter() {
|
||||
@@ -87,24 +84,7 @@ public class StackFrameRow {
|
||||
}
|
||||
|
||||
public Function getFunction() {
|
||||
if (provider.mappingService == null) {
|
||||
return null;
|
||||
}
|
||||
TraceThread curThread = provider.current.getThread();
|
||||
if (curThread == null) {
|
||||
return null;
|
||||
}
|
||||
Address pc = getProgramCounter();
|
||||
if (pc == null) {
|
||||
return null;
|
||||
}
|
||||
TraceLocation dloc = new DefaultTraceLocation(curThread.getTrace(),
|
||||
curThread, Lifespan.at(getSnap()), pc);
|
||||
ProgramLocation sloc = provider.mappingService.getOpenMappedLocation(dloc);
|
||||
if (sloc == null) {
|
||||
return null;
|
||||
}
|
||||
return sloc.getProgram().getFunctionManager().getFunctionContaining(sloc.getAddress());
|
||||
return panel.provider.getFunction(getProgramCounter());
|
||||
}
|
||||
|
||||
protected void update() {
|
||||
|
||||
+27
@@ -57,4 +57,31 @@ public class ModelQueryTest extends AbstractGhidraHeadedDebuggerGUITest {
|
||||
assertFalse(threadQuery.includes(Lifespan.before(0), thread0Val));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvolves() throws Throwable {
|
||||
createTrace();
|
||||
|
||||
ModelQuery rootQuery = ModelQuery.parse("");
|
||||
ModelQuery threadQuery = ModelQuery.parse("Processes[].Threads[]");
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
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(Lifespan.nowOn(0), ConflictResolution.DENY)
|
||||
.getLastEntry();
|
||||
|
||||
assertTrue(rootQuery.involves(Lifespan.ALL, rootVal));
|
||||
assertFalse(rootQuery.involves(Lifespan.ALL, thread0Val));
|
||||
|
||||
assertTrue(threadQuery.involves(Lifespan.ALL, rootVal));
|
||||
assertTrue(threadQuery.involves(Lifespan.ALL, thread0Val));
|
||||
assertFalse(threadQuery.involves(Lifespan.before(0), thread0Val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+508
File diff suppressed because it is too large
Load Diff
-124
@@ -1,124 +0,0 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.app.plugin.core.debug.gui.stack;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.dbg.target.schema.SchemaContext;
|
||||
import ghidra.dbg.target.schema.TargetObjectSchema.SchemaName;
|
||||
import ghidra.dbg.target.schema.XmlSchemaContext;
|
||||
import ghidra.trace.model.Trace;
|
||||
import ghidra.trace.model.target.TraceObjectKeyPath;
|
||||
import ghidra.trace.model.target.TraceObject.ConflictResolution;
|
||||
import ghidra.trace.model.thread.TraceObjectThread;
|
||||
import ghidra.trace.model.thread.TraceThread;
|
||||
import ghidra.util.database.UndoableTransaction;
|
||||
import ghidra.util.exception.DuplicateNameException;
|
||||
|
||||
public class DebuggerStackProviderObjectTest extends DebuggerStackProviderTest {
|
||||
|
||||
protected SchemaContext ctx;
|
||||
|
||||
@Override
|
||||
protected void createTrace(String langID) throws IOException {
|
||||
super.createTrace(langID);
|
||||
try {
|
||||
activateObjectsMode();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void useTrace(Trace trace) {
|
||||
super.useTrace(trace);
|
||||
try {
|
||||
activateObjectsMode();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void activateObjectsMode() throws Exception {
|
||||
// NOTE the use of index='1' allowing object-based managers to ID unique path
|
||||
ctx = XmlSchemaContext.deserialize("""
|
||||
<context>
|
||||
<schema name='Session' elementResync='NEVER' attributeResync='ONCE'>
|
||||
<attribute name='Processes' schema='ProcessContainer' />
|
||||
</schema>
|
||||
<schema name='ProcessContainer' canonical='yes' elementResync='NEVER'
|
||||
attributeResync='ONCE'>
|
||||
<element index='1' schema='Process' />
|
||||
</schema>
|
||||
<schema name='Process' elementResync='NEVER' attributeResync='ONCE'>
|
||||
<attribute name='Threads' schema='ThreadContainer' />
|
||||
<attribute name='Memory' schema='RegionContainer' />
|
||||
</schema>
|
||||
<schema name='ThreadContainer' canonical='yes' elementResync='NEVER'
|
||||
attributeResync='ONCE'>
|
||||
<element schema='Thread' />
|
||||
</schema>
|
||||
<schema name='Thread' elementResync='NEVER' attributeResync='NEVER'>
|
||||
<interface name='Thread' />
|
||||
<interface name='Aggregate' />
|
||||
<attribute name='Stack' schema='Stack' />
|
||||
<attribute name='Registers' schema='RegisterContainer' />
|
||||
</schema>
|
||||
<schema name='Stack' canonical='yes' elementResync='NEVER'
|
||||
attributeResync='ONCE'>
|
||||
<interface name='Stack' />
|
||||
<element schema='Frame' />
|
||||
</schema>
|
||||
<schema name='Frame' elementResync='NEVER' attributeResync='NEVER'>
|
||||
<interface name='StackFrame' />
|
||||
</schema>
|
||||
<schema name='RegisterContainer' canonical='yes' elementResync='NEVER'
|
||||
attributeResync='NEVER'>
|
||||
<interface name='RegisterContainer' />
|
||||
<element schema='Register' />
|
||||
</schema>
|
||||
<schema name='Register' elementResync='NEVER' attributeResync='NEVER'>
|
||||
<interface name='Register' />
|
||||
</schema>
|
||||
<schema name='RegionContainer' canonical='yes' elementResync='NEVER'
|
||||
attributeResync='ONCE'>
|
||||
<element schema='Region' />
|
||||
</schema>
|
||||
<schema name='Region' elementResync='NEVER' attributeResync='NEVER'>
|
||||
<interface name='MemoryRegion' />
|
||||
</schema>
|
||||
</context>
|
||||
""");
|
||||
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
tb.trace.getObjectManager().createRootObject(ctx.getSchema(new SchemaName("Session")));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TraceThread addThread(String n) throws DuplicateNameException {
|
||||
try (UndoableTransaction tid = tb.startTransaction()) {
|
||||
TraceObjectThread thread = (TraceObjectThread) super.addThread(n);
|
||||
TraceObjectKeyPath regsPath = thread.getObject().getCanonicalPath().extend("Registers");
|
||||
tb.trace.getObjectManager()
|
||||
.createObject(regsPath)
|
||||
.insert(thread.getLifespan(), ConflictResolution.DENY);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
+335
-231
File diff suppressed because it is too large
Load Diff
+7
@@ -696,6 +696,13 @@ public interface TargetObjectSchema {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a suitable object with this schema at the given path
|
||||
*
|
||||
* @param type the type of object sought
|
||||
* @param path the path of a seed object
|
||||
* @return the expected path of the suitable object, or null
|
||||
*/
|
||||
default List<String> searchForSuitable(Class<? extends TargetObject> type, List<String> path) {
|
||||
for (; path != null; path = PathUtils.parent(path)) {
|
||||
TargetObjectSchema schema = getSuccessorSchema(path);
|
||||
|
||||
+18
-2
@@ -260,7 +260,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
* @param column The field to add
|
||||
*/
|
||||
protected void addTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column) {
|
||||
addTableColumns(CollectionUtils.asSet(column));
|
||||
addTableColumns(CollectionUtils.asSet(column), true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,8 +274,24 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
|
||||
* @param columns The columns to add
|
||||
*/
|
||||
protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> columns) {
|
||||
addTableColumns(columns, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @param columns The columns to add
|
||||
* @param isDefault true if these are default columns
|
||||
*/
|
||||
protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> columns,
|
||||
boolean isDefault) {
|
||||
for (DynamicTableColumn<ROW_TYPE, ?, ?> column : columns) {
|
||||
doAddTableColumn(column, getDefaultTableColumns().size(), true);
|
||||
doAddTableColumn(column, getDefaultTableColumns().size(), isDefault);
|
||||
}
|
||||
fireTableStructureChanged();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user