GP-5523: Allow tool-wide configuration of radix for displaying trace times.

This commit is contained in:
Dan 2025-04-17 18:18:53 +00:00
parent 92e2b6b5d4
commit 004712026b
38 changed files with 751 additions and 246 deletions

View file

@ -39,6 +39,7 @@ import ghidra.trace.model.thread.TraceObjectThread;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.Msg;
import ghidra.util.NotOwnerException;
@ -712,7 +713,7 @@ public class DebuggerCoordinates {
coordState.putLong(KEY_THREAD_KEY, thread.getKey());
}
if (time != null) {
coordState.putString(KEY_TIME, time.toString());
coordState.putString(KEY_TIME, time.toString(TimeRadix.DEC));
}
if (frame != null) {
coordState.putInt(KEY_FRAME, frame);
@ -785,7 +786,7 @@ public class DebuggerCoordinates {
String timeSpec = coordState.getString(KEY_TIME, null);
TraceSchedule time;
try {
time = TraceSchedule.parse(timeSpec);
time = TraceSchedule.parse(timeSpec, TimeRadix.DEC);
}
catch (Exception e) {
Msg.error(DebuggerCoordinates.class,

View file

@ -63,6 +63,7 @@ import ghidra.trace.model.target.schema.TraceObjectSchema.SchemaName;
import ghidra.trace.model.target.schema.XmlSchemaContext;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.DuplicateFileException;
@ -1166,8 +1167,8 @@ public class TraceRmiHandler extends AbstractTraceRmiConnection {
TraceSnapshot snapshot = switch (req.getTimeCase()) {
case TIME_NOT_SET -> throw new TraceRmiError("snap or time required");
case SNAP -> open.createSnapshot(req.getSnap().getSnap());
case SCHEDULE -> open
.createSnapshot(TraceSchedule.parse(req.getSchedule().getSchedule()));
case SCHEDULE -> open.createSnapshot(
TraceSchedule.parse(req.getSchedule().getSchedule(), TimeRadix.DEC));
};
snapshot.setDescription(req.getDescription());
if (!"".equals(req.getDatetime())) {

View file

@ -101,5 +101,14 @@
emulated state into the trace's <EM>scratch space</EM>, which comprises all negative snaps.
Some time-travel capable back ends may also write into scratch space. When this toggle is
enabled, those scratch snapshots are hidden.</P>
<H3><A name="radix"></A>Set Time Radix</H3>
<P>These actions are available when a trace is active. It sets the display radix for snapshot
keys and time schedules <EM>throughout the tool</EM>. This is useful to match the display of
time coordinates with a back end that supports time travel. Notably, WinDbg TTD uses upper-case
hexadecimal for its event sequence numbers. Normally, the back end would set the UI's radix
automatically, but in case it does not, or if you'd like to override the radix, these actions
are available.</P>
</BODY>
</HTML>

View file

@ -15,7 +15,6 @@
*/
package ghidra.app.plugin.core.debug.gui.memview;
import java.awt.Color;
import java.util.*;
import ghidra.async.AsyncDebouncer;
@ -29,9 +28,11 @@ import ghidra.trace.model.*;
import ghidra.trace.model.breakpoint.*;
import ghidra.trace.model.memory.*;
import ghidra.trace.model.modules.*;
import ghidra.trace.model.stack.*;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.thread.*;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.util.TraceEvents;
import ghidra.util.Swing;
@ -84,6 +85,9 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
listenFor(TraceEvents.BYTES_CHANGED, this::bytesChanged);
listenFor(TraceEvents.VALUE_CREATED, this::valueCreated);
listenFor(TraceEvents.VALUE_DELETED, this::valueDeleted);
listenForUntyped(DomainObjectEvent.RESTORED, this::objectRestored);
}
@ -106,7 +110,7 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
AddressRange rng = rng(defaultSpace, threadId, threadId);
TraceObject obj = objThread.getObject();
obj.getCanonicalParents(Lifespan.ALL).forEach(p -> {
MemoryBox box = new MemoryBox("Thread " + thread.getName(p.getMinSnap()),
MemoryBox box = new MemoryBox(currentTrace, "Thread " + thread.getName(p.getMinSnap()),
MemviewBoxType.THREAD, rng, p.getLifespan());
updateList.add(box);
});
@ -124,7 +128,7 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
if (region.getName(v.getMinSnap()).equals("full memory")) {
return;
}
MemoryBox box = new MemoryBox("Region " + region.getName(v.getMinSnap()),
MemoryBox box = new MemoryBox(currentTrace, "Region " + region.getName(v.getMinSnap()),
MemviewBoxType.REGION, v.castValue(), v.getLifespan());
updateList.add(box);
});
@ -138,7 +142,7 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
TraceObject obj = objModule.getObject();
obj.getOrderedValues(Lifespan.ALL, TraceObjectModule.KEY_RANGE, true).forEach(v -> {
MemoryBox box = new MemoryBox("Module " + module.getName(v.getMinSnap()),
MemoryBox box = new MemoryBox(currentTrace, "Module " + module.getName(v.getMinSnap()),
MemviewBoxType.MODULE, v.castValue(), v.getLifespan());
updateList.add(box);
});
@ -152,7 +156,7 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
TraceObject obj = objSection.getObject();
obj.getOrderedValues(Lifespan.ALL, TraceObjectSection.KEY_RANGE, true).forEach(v -> {
MemoryBox box = new MemoryBox("Module " + section.getName(v.getMinSnap()),
MemoryBox box = new MemoryBox(currentTrace, "Module " + section.getName(v.getMinSnap()),
MemviewBoxType.IMAGE, v.castValue(), v.getLifespan());
updateList.add(box);
});
@ -168,8 +172,9 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
TraceObject obj = objBpt.getObject();
obj.getOrderedValues(Lifespan.ALL, TraceObjectBreakpointLocation.KEY_RANGE, true)
.forEach(v -> {
MemoryBox box = new MemoryBox("Module " + bpt.getName(v.getMinSnap()),
MemviewBoxType.BREAKPOINT, v.castValue(), v.getLifespan());
MemoryBox box =
new MemoryBox(currentTrace, "Module " + bpt.getName(v.getMinSnap()),
MemviewBoxType.BREAKPOINT, v.castValue(), v.getLifespan());
updateList.add(box);
});
updateLabelDebouncer.contact(null);
@ -180,12 +185,24 @@ public class DebuggerMemviewTraceListener extends TraceDomainObjectListener {
return;
}
Lifespan lifespan = range.getLifespan();
MemoryBox box = new MemoryBox("BytesChanged " + range.description(),
MemoryBox box = new MemoryBox(currentTrace, "BytesChanged " + range.description(),
MemviewBoxType.WRITE_MEMORY, range.getRange(), lifespan);
updateList.add(box);
updateLabelDebouncer.contact(null);
}
private void valueCreated(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
provider.fireTableDataChanged();
}
}
private void valueDeleted(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
provider.fireTableDataChanged();
}
}
private void objectRestored(DomainObjectChangeRecord domainObjectChangeRecord) {
if (!trackTrace) {
return;

View file

@ -23,9 +23,12 @@ import java.util.Map;
import generic.theme.GThemeDefaults.Colors;
import ghidra.program.model.address.AddressRange;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public class MemoryBox {
protected final Trace trace;
protected String id;
protected MemviewBoxType type;
protected AddressRange range;
@ -46,7 +49,9 @@ public class MemoryBox {
protected boolean current;
public MemoryBox(String id, MemviewBoxType type, AddressRange range, long tick, Color color) {
public MemoryBox(Trace trace, String id, MemviewBoxType type, AddressRange range, long tick,
Color color) {
this.trace = trace;
this.id = id;
this.type = type;
this.range = range;
@ -54,12 +59,13 @@ public class MemoryBox {
this.color = color;
}
public MemoryBox(String id, MemviewBoxType type, AddressRange range, long tick) {
this(id, type, range, tick, type.getColor());
public MemoryBox(Trace trace, String id, MemviewBoxType type, AddressRange range, long tick) {
this(trace, id, type, range, tick, type.getColor());
}
public MemoryBox(String id, MemviewBoxType type, AddressRange range, Lifespan trange) {
this(id, type, range, trange.lmin());
public MemoryBox(Trace trace, String id, MemviewBoxType type, AddressRange range,
Lifespan trange) {
this(trace, id, type, range, trange.lmin());
setEnd(trange.lmax());
}
@ -224,6 +230,18 @@ public class MemoryBox {
stopTime = val;
}
private TimeRadix getTimeRadix() {
return trace.getTimeManager().getTimeRadix();
}
public String formatStart() {
return getTimeRadix().format(start);
}
public String formatEnd() {
return getTimeRadix().format(stop);
}
public boolean inPixelRange(long pos) {
if (pos < pixTstart) {
return false;

View file

@ -38,7 +38,8 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
private Map<String, MemoryBox> memMap = new HashMap<>();
private MemviewProvider provider;
private final static String COLUMN_NAMES[] = { NAME_COL, ASTART_COL, ASTOP_COL, TSTART_COL, TSTOP_COL };
private final static String COLUMN_NAMES[] =
{ NAME_COL, ASTART_COL, ASTOP_COL, TSTART_COL, TSTOP_COL };
public MemviewMapModel(MemviewProvider provider) {
super(ASTART);
@ -108,9 +109,9 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
}
/**
* Convenience method for locating columns by name. Implementation is naive so
* this should be overridden if this method is to be called often. This method
* is not in the TableModel interface and is not used by the JTable.
* Convenience method for locating columns by name. Implementation is naive so this should be
* overridden if this method is to be called often. This method is not in the TableModel
* interface and is not used by the JTable.
*/
@Override
public int findColumn(String columnName) {
@ -142,10 +143,9 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
}
/**
* Returns the number of records managed by the data source object. A
* <B>JTable</B> uses this method to determine how many rows it should create
* and display. This method should be quick, as it is call by <B>JTable</B>
* quite frequently.
* Returns the number of records managed by the data source object. A <B>JTable</B> uses this
* method to determine how many rows it should create and display. This method should be quick,
* as it is call by <B>JTable</B> quite frequently.
*
* @return the number or rows in the model
* @see #getColumnCount
@ -165,7 +165,8 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
MemoryBox box = memList.get(rowIndex);
try {
box.getStart();
} catch (ConcurrentModificationException e) {
}
catch (ConcurrentModificationException e) {
update();
}
return memList.get(rowIndex);
@ -179,24 +180,25 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
public Object getColumnValueForRow(MemoryBox box, int columnIndex) {
try {
switch (columnIndex) {
case NAME:
return box.getId();
case ASTART:
return box.getRange().getMinAddress();
case ASTOP:
return box.getRange().getMaxAddress();
case TSTART:
return Long.toString(box.getStart(), 16);
case TSTOP:
long end = box.getEnd();
if (end == Long.MAX_VALUE) {
return "+" + '\u221e' + '\u2025';
}
return Long.toString(end, 16);
default:
return "UNKNOWN";
case NAME:
return box.getId();
case ASTART:
return box.getRange().getMinAddress();
case ASTOP:
return box.getRange().getMaxAddress();
case TSTART:
return box.formatStart();
case TSTOP:
long end = box.getEnd();
if (end == Long.MAX_VALUE) {
return "+" + '\u221e' + '\u2025';
}
return box.formatEnd();
default:
return "UNKNOWN";
}
} catch (ConcurrentModificationException e) {
}
catch (ConcurrentModificationException e) {
update();
}
return null;
@ -223,18 +225,18 @@ class MemviewMapModel extends AbstractSortedTableModel<MemoryBox> {
public int compare(MemoryBox b1, MemoryBox b2) {
switch (sortColumn) {
case NAME:
return b1.getId().compareToIgnoreCase(b2.getId());
case ASTART:
return (int) (b1.getStartAddress() - b2.getStartAddress());
case ASTOP:
return (int) (b1.getStopAddress() - b2.getStopAddress());
case TSTART:
return (int) (b1.getStartTime() - b2.getStartTime());
case TSTOP:
return (int) (b1.getStopTime() - b2.getStopTime());
default:
return 0;
case NAME:
return b1.getId().compareToIgnoreCase(b2.getId());
case ASTART:
return (int) (b1.getStartAddress() - b2.getStartAddress());
case ASTOP:
return (int) (b1.getStopAddress() - b2.getStopAddress());
case TSTART:
return (int) (b1.getStartTime() - b2.getStartTime());
case TSTOP:
return (int) (b1.getStopTime() - b2.getStopTime());
default:
return 0;
}
}
}

View file

@ -26,6 +26,7 @@ import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public class MemviewPanel extends JPanel implements MouseListener, MouseMotionListener {
private static final long serialVersionUID = 1L;
@ -484,11 +485,19 @@ public class MemviewPanel extends JPanel implements MouseListener, MouseMotionLi
return aval;
}
private TimeRadix getTimeRadix() {
if (boxList == null || boxList.isEmpty()) {
return TimeRadix.DEFAULT;
}
return boxList.get(0).trace.getTimeManager().getTimeRadix();
}
public String getTagForTick(long tick) {
TimeRadix radix = getTimeRadix();
String tval = "";
if (0 <= tick && tick < timesArray.length) {
Long time = timesArray[(int) tick];
tval = Long.toString(time, 16);
tval = radix.format(time);
}
return tval;
}

View file

@ -97,27 +97,27 @@ public class MemviewProvider extends ComponentProviderAdapter {
tool.addLocalAction(this, zoomOutTAction);
new ToggleActionBuilder("Toggle Layout", plugin.getName()) //
//.menuPath("&Toggle layout") //
.toolBarIcon(AbstractRefreshAction.ICON)
.helpLocation(new HelpLocation(plugin.getName(), "toggle_layout")) //
.onAction(ctx -> performToggleLayout(ctx))
.buildAndInstallLocal(this);
//.menuPath("&Toggle layout") //
.toolBarIcon(AbstractRefreshAction.ICON)
.helpLocation(new HelpLocation(plugin.getName(), "toggle_layout")) //
.onAction(ctx -> performToggleLayout(ctx))
.buildAndInstallLocal(this);
new ToggleActionBuilder("Toggle Process Trace", plugin.getName()) //
//.menuPath("&Toggle layout") //
.toolBarIcon(DebuggerResources.ICON_SYNC)
.helpLocation(new HelpLocation(plugin.getName(), "toggle_process_trace")) //
.onAction(ctx -> performToggleTrace(ctx))
.selected(false)
.buildAndInstallLocal(this);
//.menuPath("&Toggle layout") //
.toolBarIcon(DebuggerResources.ICON_SYNC)
.helpLocation(new HelpLocation(plugin.getName(), "toggle_process_trace")) //
.onAction(ctx -> performToggleTrace(ctx))
.selected(false)
.buildAndInstallLocal(this);
new ToggleActionBuilder("Apply Filter To Panel", plugin.getName()) //
//.menuPath("&Toggle layout") //
.toolBarIcon(DebuggerResources.ICON_FILTER)
.helpLocation(new HelpLocation(plugin.getName(), "apply_to_panel")) //
.onAction(ctx -> performApplyFilterToPanel(ctx))
.selected(true)
.buildAndInstallLocal(this);
//.menuPath("&Toggle layout") //
.toolBarIcon(DebuggerResources.ICON_FILTER)
.helpLocation(new HelpLocation(plugin.getName(), "apply_to_panel")) //
.onAction(ctx -> performApplyFilterToPanel(ctx))
.selected(true)
.buildAndInstallLocal(this);
}
@ -218,7 +218,7 @@ public class MemviewProvider extends ComponentProviderAdapter {
public void goTo(int x, int y) {
Rectangle bounds = scrollPane.getBounds();
scrollPane.getViewport()
.scrollRectToVisible(new Rectangle(x, y, bounds.width, bounds.height));
.scrollRectToVisible(new Rectangle(x, y, bounds.width, bounds.height));
scrollPane.getViewport().doLayout();
}
@ -306,4 +306,10 @@ public class MemviewProvider extends ComponentProviderAdapter {
memviewPanel.reset();
});
}
void fireTableDataChanged() {
Swing.runIfSwingOrRunLater(() -> {
memviewTable.fireTableDataChanged();
});
}
}

View file

@ -175,6 +175,10 @@ public class MemviewTable {
}
}
void fireTableDataChanged() {
model.fireTableDataChanged();
}
/*
private List<MemviewRow> generateRows(Collection<MemoryBox> changed) {
List<MemviewRow> list = new ArrayList<>();

View file

@ -28,6 +28,8 @@ import ghidra.framework.plugintool.Plugin;
import ghidra.trace.model.*;
import ghidra.trace.model.target.TraceObject;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.util.TraceEvents;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
@ -56,12 +58,20 @@ public abstract class AbstractQueryTableModel<T> extends ThreadedTableModel<T, T
if (query != null && query.involves(span, value)) {
reload(); // Can I be more surgical?
}
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
fireTableDataChanged();
}
}
protected void valueDeleted(TraceObjectValue value) {
if (query != null && query.involves(span, value)) {
reload(); // Can I be more surgical?
}
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
fireTableDataChanged();
}
}
protected void valueLifespanChanged(TraceObjectValue value, Lifespan oldSpan,

View file

@ -20,20 +20,34 @@ import java.awt.Component;
import docking.widgets.table.AbstractDynamicTableColumn;
import docking.widgets.table.GTableCellRenderingData;
import ghidra.app.plugin.core.debug.gui.model.PathTableModel.PathRow;
import ghidra.app.plugin.core.debug.gui.model.columns.TracePathLastLifespanColumn.SpanAndRadix;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
public class TracePathLastLifespanColumn
extends AbstractDynamicTableColumn<PathRow, Lifespan, Trace> {
extends AbstractDynamicTableColumn<PathRow, SpanAndRadix, Trace> {
private final class LastLifespanRenderer extends AbstractGColumnRenderer<Lifespan> {
record SpanAndRadix(Lifespan span, TimeRadix radix) implements Comparable<SpanAndRadix> {
@Override
public String getFilterString(Lifespan t, Settings settings) {
public final String toString() {
return span.toString(radix::format);
}
@Override
public int compareTo(SpanAndRadix that) {
return this.span.compareTo(that.span);
}
}
private final class LastLifespanRenderer extends AbstractGColumnRenderer<SpanAndRadix> {
@Override
public String getFilterString(SpanAndRadix t, Settings settings) {
return t == null ? "<null>" : t.toString();
}
@ -56,17 +70,18 @@ public class TracePathLastLifespanColumn
}
@Override
public GColumnRenderer<Lifespan> getColumnRenderer() {
public GColumnRenderer<SpanAndRadix> getColumnRenderer() {
return renderer;
}
@Override
public Lifespan getValue(PathRow rowObject, Settings settings, Trace data,
public SpanAndRadix getValue(PathRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
TraceObjectValue lastEntry = rowObject.getPath().getLastEntry();
TimeRadix radix = data.getTimeManager().getTimeRadix();
if (lastEntry == null) {
return Lifespan.ALL;
return new SpanAndRadix(Lifespan.ALL, radix);
}
return lastEntry.getLifespan();
return new SpanAndRadix(lastEntry.getLifespan(), radix);
}
}

View file

@ -17,15 +17,25 @@ 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.app.plugin.core.debug.gui.model.columns.TraceValueLifeColumn.SetAndRadix;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.trace.model.Lifespan.LifeSet;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.table.column.GColumnRenderer;
public class TraceValueLifeColumn
extends AbstractDynamicTableColumn<ValueRow, LifeSet, Trace> {
private final TraceValueColumnRenderer<LifeSet> renderer = new TraceValueColumnRenderer<>();
extends AbstractDynamicTableColumn<ValueRow, SetAndRadix, Trace> {
record SetAndRadix(LifeSet set, TimeRadix radix) {
@Override
public final String toString() {
return set.toString(radix::format);
}
}
private final TraceValueColumnRenderer<SetAndRadix> renderer = new TraceValueColumnRenderer<>();
@Override
public String getColumnName() {
@ -33,13 +43,13 @@ public class TraceValueLifeColumn
}
@Override
public GColumnRenderer<LifeSet> getColumnRenderer() {
public GColumnRenderer<SetAndRadix> getColumnRenderer() {
return renderer;
}
@Override
public LifeSet getValue(ValueRow rowObject, Settings settings, Trace data,
public SetAndRadix getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException {
return rowObject.getLife();
return new SetAndRadix(rowObject.getLife(), data.getTimeManager().getTimeRadix());
}
}

View file

@ -59,7 +59,13 @@ import ghidra.program.model.listing.Instruction;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.Varnode;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceDomainObjectListener;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.trace.util.TraceEvents;
import ghidra.util.*;
import ghidra.util.table.GhidraTable;
import ghidra.util.table.GhidraTableFilterPanel;
@ -505,6 +511,25 @@ public class DebuggerPcodeStepperProvider extends ComponentProviderAdapter {
}
}
class ForRadixTraceListener extends TraceDomainObjectListener {
{
listenFor(TraceEvents.VALUE_CREATED, this::valueCreated);
listenFor(TraceEvents.VALUE_DELETED, this::valueDeleted);
}
void valueCreated(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
updateSubTitle();
}
}
void valueDeleted(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
updateSubTitle();
}
}
}
protected static String createColoredStyle(String cls, Color color) {
if (color == null) {
return "";
@ -525,6 +550,8 @@ public class DebuggerPcodeStepperProvider extends ComponentProviderAdapter {
return true;
}
private final TraceDomainObjectListener forRadixTraceListener = new ForRadixTraceListener();
private final DebuggerPcodeStepperPlugin plugin;
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
@ -693,18 +720,36 @@ public class DebuggerPcodeStepperProvider extends ComponentProviderAdapter {
return mainPanel;
}
protected void removeTraceListener() {
if (current.getTrace() != null) {
current.getTrace().removeListener(forRadixTraceListener);
}
}
protected void addTraceListener() {
if (current.getTrace() != null) {
current.getTrace().addListener(forRadixTraceListener);
}
}
protected void updateSubTitle() {
TimeRadix radix = current.getTrace().getTimeManager().getTimeRadix();
setSubTitle(current.getTime().toString(radix));
}
public void coordinatesActivated(DebuggerCoordinates coordinates) {
if (sameCoordinates(current, coordinates)) {
current = coordinates;
return;
}
previous = current;
removeTraceListener();
current = coordinates;
addTraceListener();
doLoadPcodeFrame();
setSubTitle(current.getTime().toString());
updateSubTitle();
contextChanged();
}

View file

@ -33,10 +33,14 @@ import ghidra.framework.model.DomainObjectEvent;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceDomainObjectListener;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.trace.util.TraceEvents;
import ghidra.util.DateUtils;
import ghidra.util.table.GhidraTableFilterPanel;
import ghidra.util.table.column.AbstractGColumnRenderer;
@ -46,10 +50,11 @@ public class DebuggerSnapshotTablePanel extends JPanel {
implements EnumeratedTableColumn<SnapshotTableColumns, SnapshotRow> {
SNAP("Snap", Long.class, SnapshotRow::getSnap, false),
TIME("Time", TraceSchedule.class, SnapshotRow::getTime, true),
TIMESTAMP("Timestamp", String.class, SnapshotRow::getTimeStamp, true), // TODO: Use Date type here
TIMESTAMP("Timestamp", Date.class, SnapshotRow::getTimeStamp, true),
EVENT_THREAD("Event Thread", String.class, SnapshotRow::getEventThreadName, true),
SCHEDULE("Schedule", String.class, SnapshotRow::getSchedule, false),
DESCRIPTION("Description", String.class, SnapshotRow::getDescription, SnapshotRow::setDescription, true);
SCHEDULE("Schedule", TraceSchedule.class, SnapshotRow::getSchedule, false),
DESCRIPTION("Description", String.class, SnapshotRow::getDescription, //
SnapshotRow::setDescription, true);
private final String header;
private final Function<SnapshotRow, ?> getter;
@ -122,6 +127,9 @@ public class DebuggerSnapshotTablePanel extends JPanel {
listenFor(TraceEvents.SNAPSHOT_ADDED, this::snapAdded);
listenFor(TraceEvents.SNAPSHOT_CHANGED, this::snapChanged);
listenFor(TraceEvents.SNAPSHOT_DELETED, this::snapDeleted);
listenFor(TraceEvents.VALUE_CREATED, this::valueCreated);
listenFor(TraceEvents.VALUE_DELETED, this::valueDeleted);
}
private void objectRestored() {
@ -132,7 +140,7 @@ public class DebuggerSnapshotTablePanel extends JPanel {
if (snapshot.getKey() < 0 && hideScratch) {
return;
}
SnapshotRow row = new SnapshotRow(currentTrace, snapshot);
SnapshotRow row = new SnapshotRow(snapshot);
snapshotTableModel.add(row);
}
@ -149,12 +157,50 @@ public class DebuggerSnapshotTablePanel extends JPanel {
}
snapshotTableModel.deleteWith(row -> row.getSnapshot() == snapshot);
}
private void valueCreated(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
snapshotTableModel.fireTableDataChanged();
}
}
private void valueDeleted(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
snapshotTableModel.fireTableDataChanged();
}
}
}
final TableCellRenderer styleCurrentRenderer = new AbstractGColumnRenderer<Object>() {
@Override
protected String formatNumber(Number value, Settings settings) {
return switch (value) {
case null -> "<null>";
// SNAP is the only column with Long type
case Long snap -> getTimeRadix().format(snap);
default -> super.formatNumber(value, settings);
};
}
@Override
protected String getText(Object value) {
return switch (value) {
case null -> "<null>";
case Date date -> DateUtils.formatDateTimestamp(date);
case TraceSchedule schedule -> schedule.toString(getTimeRadix());
default -> value.toString();
};
}
@Override
public String getFilterString(Object t, Settings settings) {
return t == null ? "<null>" : t.toString();
return switch (t) {
case null -> "<null>";
// SNAP is the only column with Long type
case Long snap -> getTimeRadix().format(snap);
case Number n -> formatNumber(n, settings);
default -> getText(t);
};
}
@Override
@ -216,6 +262,11 @@ public class DebuggerSnapshotTablePanel extends JPanel {
descCol.setCellRenderer(styleCurrentRenderer);
}
protected TimeRadix getTimeRadix() {
return currentTrace == null ? TimeRadix.DEFAULT
: currentTrace.getTimeManager().getTimeRadix();
}
private void addNewListeners() {
if (currentTrace == null) {
return;
@ -268,7 +319,7 @@ public class DebuggerSnapshotTablePanel extends JPanel {
for (TraceSnapshot snapshot : hideScratch
? manager.getSnapshots(0, true, Long.MAX_VALUE, true)
: manager.getAllSnapshots()) {
SnapshotRow row = new SnapshotRow(currentTrace, snapshot);
SnapshotRow row = new SnapshotRow(snapshot);
toAdd.add(row);
if (current != DebuggerCoordinates.NOWHERE &&
snapshot.getKey() == current.getViewSnap()) {
@ -289,7 +340,7 @@ public class DebuggerSnapshotTablePanel extends JPanel {
Collection<? extends TraceSnapshot> sratch =
manager.getSnapshots(Long.MIN_VALUE, true, 0, false);
snapshotTableModel.addAll(sratch.stream()
.map(s -> new SnapshotRow(currentTrace, s))
.map(s -> new SnapshotRow(s))
.collect(Collectors.toList()));
}

View file

@ -23,9 +23,11 @@ import java.lang.invoke.MethodHandles;
import javax.swing.Icon;
import javax.swing.JComponent;
import db.Transaction;
import docking.ActionContext;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerSnapActionContext;
@ -37,8 +39,14 @@ import ghidra.framework.plugintool.AutoService.Wiring;
import ghidra.framework.plugintool.annotation.AutoConfigStateField;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.trace.model.Trace;
import ghidra.trace.model.TraceDomainObjectListener;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.target.path.KeyPath;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.trace.util.TraceEvents;
import ghidra.util.HelpLocation;
public class DebuggerTimeProvider extends ComponentProviderAdapter {
@ -54,7 +62,8 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
static ActionBuilder builder(Plugin owner) {
String ownerName = owner.getName();
return new ActionBuilder(NAME, ownerName).description(DESCRIPTION)
return new ActionBuilder(NAME, ownerName)
.description(DESCRIPTION)
.menuPath(DebuggerPluginPackage.NAME, NAME)
.menuGroup(GROUP)
.menuIcon(ICON)
@ -63,6 +72,43 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
}
}
interface SetTimeRadixAction {
String NAME = "Set Time Radix";
String DESCRIPTION = "Change the time radix for this trace / target";
String GROUP = GROUP_TRACE;
String HELP_ANCHOR = "radix";
static ToggleActionBuilder builder(String title, Plugin owner) {
String ownerName = owner.getName();
return new ToggleActionBuilder(NAME + " - " + title, ownerName)
.description(DESCRIPTION)
.menuPath(DebuggerPluginPackage.NAME, NAME, title)
.menuGroup(GROUP)
.helpLocation(new HelpLocation(ownerName, HELP_ANCHOR));
}
}
protected class ForRadixTraceListener extends TraceDomainObjectListener {
{
listenFor(TraceEvents.VALUE_CREATED, this::valueCreated);
listenFor(TraceEvents.VALUE_DELETED, this::valueDeleted);
}
private void valueCreated(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
refreshRadixSelection();
}
}
private void valueDeleted(TraceObjectValue value) {
if (value.getCanonicalPath().equals(KeyPath.of(TraceTimeManager.KEY_TIME_RADIX))) {
refreshRadixSelection();
}
}
}
private final TraceDomainObjectListener forRadixTraceListener = new ForRadixTraceListener();
protected final DebuggerTimePlugin plugin;
DebuggerCoordinates current = DebuggerCoordinates.NOWHERE;
@ -78,6 +124,9 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
DockingAction actionGoToTime;
ToggleDockingAction actionHideScratch;
ToggleDockingAction actionSetRadixDec;
ToggleDockingAction actionSetRadixHexUpper;
ToggleDockingAction actionSetRadixHexLower;
private DebuggerSnapActionContext myActionContext;
@ -202,6 +251,21 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
.selected(hideScratch)
.onAction(this::activatedHideScratch)
.buildAndInstallLocal(this);
actionSetRadixDec = SetTimeRadixAction.builder("Decimal", plugin)
.enabledWhen(c -> current.getTrace() != null &&
current.getTrace().getObjectManager().getRootObject() != null)
.onAction(c -> activatedSetRadix(TimeRadix.DEC))
.buildAndInstall(tool);
actionSetRadixHexUpper = SetTimeRadixAction.builder("Upper Hex", plugin)
.enabledWhen(c -> current.getTrace() != null &&
current.getTrace().getObjectManager().getRootObject() != null)
.onAction(c -> activatedSetRadix(TimeRadix.HEX_UPPER))
.buildAndInstall(tool);
actionSetRadixHexLower = SetTimeRadixAction.builder("Lower Hex", plugin)
.enabledWhen(c -> current.getTrace() != null &&
current.getTrace().getObjectManager().getRootObject() != null)
.onAction(c -> activatedSetRadix(TimeRadix.HEX_LOWER))
.buildAndInstall(tool);
}
private void activatedGoToTime() {
@ -218,10 +282,42 @@ public class DebuggerTimeProvider extends ComponentProviderAdapter {
mainPanel.setHideScratchSnapshots(hideScratch);
}
private void activatedSetRadix(TimeRadix radix) {
try (Transaction tx = current.getTrace().openTransaction("Set Time Radix")) {
current.getTrace().getTimeManager().setTimeRadix(radix);
}
// NOTE: refreshRadixSelection() should happen via listener
}
protected void removeTraceListener() {
if (current.getTrace() != null) {
current.getTrace().removeListener(forRadixTraceListener);
}
}
protected void addTraceListener() {
if (current.getTrace() != null) {
current.getTrace().addListener(forRadixTraceListener);
}
}
public void coordinatesActivated(DebuggerCoordinates coordinates) {
removeTraceListener();
current = coordinates;
addTraceListener();
mainPanel.setTrace(current.getTrace());
mainPanel.setCurrent(current);
refreshRadixSelection();
}
private void refreshRadixSelection() {
TimeRadix radix = current.getTrace() == null ? TimeRadix.DEFAULT
: current.getTrace().getTimeManager().getTimeRadix();
actionSetRadixHexLower.setSelected(radix == TimeRadix.HEX_LOWER);
actionSetRadixHexUpper.setSelected(radix == TimeRadix.HEX_UPPER);
actionSetRadixDec.setSelected(radix == TimeRadix.DEC);
}
public void writeConfigState(SaveState saveState) {

View file

@ -28,6 +28,7 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.MessageType;
import ghidra.util.Msg;
@ -37,6 +38,7 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
DebuggerSnapshotTablePanel snapshotPanel;
JTextField scheduleText;
TimeRadix radix = TimeRadix.DEFAULT;
TraceSchedule schedule;
JButton tickStep;
@ -56,7 +58,7 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
if (stepped == null) {
return;
}
setScheduleText(stepped.toString());
setScheduleText(stepped.toString(radix));
}
catch (Throwable e) {
Msg.warn(this, e.getMessage());
@ -102,7 +104,7 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
if (schedule.getSnap() == snap.longValue()) {
return;
}
scheduleText.setText(snap.toString());
scheduleText.setText(radix.format(snap));
});
scheduleText.getDocument().addDocumentListener(new DocumentListener() {
@ -132,7 +134,7 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
protected void scheduleTextChanged() {
schedule = null;
try {
schedule = TraceSchedule.parse(scheduleText.getText());
schedule = TraceSchedule.parse(scheduleText.getText(), radix);
snapshotPanel.setSelectedSnapshot(schedule.getSnap());
schedule.validate(getTrace());
setStatusText("");
@ -169,6 +171,7 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
public void close() {
super.close();
snapshotPanel.setTrace(null);
radix = TimeRadix.DEFAULT;
snapshotPanel.setSelectedSnapshot(null);
}
@ -176,13 +179,14 @@ public class DebuggerTimeSelectionDialog extends DialogComponentProvider {
* Prompts the user to select a snapshot and optionally specify a full schedule
*
* @param trace the trace from whose snapshots to select
* @param defaultTime, optionally the time to select initially
* @param defaultTime optionally the time to select initially
* @return the schedule, likely specifying just the snapshot selection
*/
public TraceSchedule promptTime(Trace trace, TraceSchedule defaultTime) {
snapshotPanel.setTrace(trace);
radix = trace.getTimeManager().getTimeRadix();
schedule = defaultTime;
scheduleText.setText(defaultTime.toString());
scheduleText.setText(defaultTime.toString(radix));
tool.showDialog(this);
return schedule;
}

View file

@ -22,17 +22,14 @@ import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.util.DateUtils;
public class SnapshotRow {
//private static final DateFormat FORMAT = DateFormat.getDateTimeInstance();
private final Trace trace;
private final TraceSnapshot snapshot;
private final Trace trace;
public SnapshotRow(Trace trace, TraceSnapshot snapshot) {
this.trace = snapshot.getTrace();
public SnapshotRow(TraceSnapshot snapshot) {
this.snapshot = snapshot;
this.trace = snapshot.getTrace();
}
public TraceSnapshot getSnapshot() {
@ -51,8 +48,8 @@ public class SnapshotRow {
return snapshot.getKey();
}
public String getTimeStamp() {
return DateUtils.formatDateTimestamp(new Date(snapshot.getRealTime()));
public Date getTimeStamp() {
return new Date(snapshot.getRealTime());
}
public String getEventThreadName() {
@ -60,8 +57,8 @@ public class SnapshotRow {
return thread == null ? "" : thread.getName(snapshot.getKey());
}
public String getSchedule() {
return snapshot.getScheduleString();
public TraceSchedule getSchedule() {
return snapshot.getSchedule();
}
public String getDescription() {

View file

@ -27,11 +27,11 @@ import docking.action.builder.ActionBuilder;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.debug.gui.timeoverview.*;
import ghidra.app.plugin.core.overview.OverviewColorLegendDialog;
import ghidra.app.plugin.core.overview.OverviewColorPlugin;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.*;
public class TimeTypeOverviewColorService implements TimeOverviewColorService {
@ -50,8 +50,8 @@ public class TimeTypeOverviewColorService implements TimeOverviewColorService {
private TimeTypeOverviewLegendPanel legendPanel;
private TimeOverviewColorPlugin plugin;
protected Map<Integer,Long> indexToSnap = new HashMap<>();
protected Map<Long,Integer> snapToIndex = new HashMap<>();
protected Map<Integer, Long> indexToSnap = new HashMap<>();
protected Map<Long, Integer> snapToIndex = new HashMap<>();
protected Lifespan bounds;
@Override
@ -82,12 +82,13 @@ public class TimeTypeOverviewColorService implements TimeOverviewColorService {
if (snap == null) {
return "";
}
TimeRadix radix = trace == null ? TimeRadix.DEFAULT : trace.getTimeManager().getTimeRadix();
Set<Pair<TimeType, String>> types = plugin.getTypes(snap);
StringBuffer buffer = new StringBuffer();
buffer.append("<b>");
buffer.append(HTMLUtilities.escapeHTML(getName()));
buffer.append(" (");
buffer.append(Long.toHexString(snap));
buffer.append(radix.format(snap));
buffer.append(")");
buffer.append("</b>\n");
for (Pair<TimeType, String> pair : types) {

View file

@ -35,6 +35,7 @@ import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginEventListener;
import ghidra.trace.model.Trace;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.Swing;
import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression;
@ -123,10 +124,11 @@ public class DebuggerTraceTabPanel extends GTabPanel<Trace>
DebuggerCoordinates current =
traceManager == null ? DebuggerCoordinates.NOWHERE : traceManager.getCurrentFor(trace);
if (current == DebuggerCoordinates.NOWHERE) {
// TODO: Could use view's snap and time table's schedule
// NOTE: Could use view's snap and time table's schedule, but not worth it.
return name + " (?)";
}
String schedule = current.getTime().toString();
TimeRadix radix = trace.getTimeManager().getTimeRadix();
String schedule = current.getTime().toString(radix);
if (schedule.length() > 15) {
schedule = "..." + schedule.substring(schedule.length() - 12);
}

View file

@ -27,6 +27,7 @@ import ghidra.trace.model.thread.TraceObjectThread;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.LockHold;
import ghidra.util.Msg;
import ghidra.util.database.*;
@ -85,7 +86,7 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot
eventThread = manager.threadManager.getThread(threadKey);
if (!"".equals(scheduleStr)) {
try {
schedule = TraceSchedule.parse(scheduleStr);
schedule = TraceSchedule.parse(scheduleStr, TimeRadix.DEC);
}
catch (IllegalArgumentException e) {
Msg.error(this, "Could not parse schedule: " + schedule, e);
@ -205,7 +206,7 @@ public class DBTraceSnapshot extends DBAnnotatedObject implements TraceSnapshot
public void setSchedule(TraceSchedule schedule) {
try (LockHold hold = LockHold.lock(manager.lock.writeLock())) {
this.schedule = schedule;
this.scheduleStr = schedule == null ? "" : schedule.toString();
this.scheduleStr = schedule == null ? "" : schedule.toString(TimeRadix.DEC);
update(SCHEDULE_COLUMN);
manager.notifySnapshotChanged(this);
}

View file

@ -25,10 +25,14 @@ import db.DBHandle;
import ghidra.framework.data.OpenMode;
import ghidra.trace.database.DBTrace;
import ghidra.trace.database.DBTraceManager;
import ghidra.trace.database.target.DBTraceObject;
import ghidra.trace.database.thread.DBTraceThreadManager;
import ghidra.trace.model.Lifespan;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.model.time.TraceSnapshot;
import ghidra.trace.model.time.TraceTimeManager;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.trace.util.TraceChangeRecord;
import ghidra.trace.util.TraceEvents;
import ghidra.util.LockHold;
@ -181,4 +185,34 @@ public class DBTraceTimeManager implements TraceTimeManager, DBTraceManager {
notifySnapshotDeleted(snapshot);
}
}
@Override
public void setTimeRadix(TimeRadix radix) {
DBTraceObject root = trace.getObjectManager().getRootObject();
if (root == null) {
throw new IllegalStateException(
"There must be a root object in the ObjectManager before setting the TimeRadix");
}
root.setAttribute(Lifespan.ALL, KEY_TIME_RADIX, switch (radix) {
case DEC -> "dec";
case HEX_UPPER -> "HEX";
case HEX_LOWER -> "hex";
});
}
@Override
public TimeRadix getTimeRadix() {
DBTraceObject root = trace.getObjectManager().getRootObject();
if (root == null) {
return TimeRadix.DEFAULT;
}
TraceObjectValue attribute = root.getAttribute(0, KEY_TIME_RADIX);
if (attribute == null) {
return TimeRadix.DEFAULT;
}
return switch (attribute.getValue()) {
case String s -> TimeRadix.fromStr(s);
default -> TimeRadix.DEFAULT;
};
}
}

View file

@ -281,7 +281,7 @@ public sealed interface Lifespan extends Span<Long, Lifespan>, Iterable<Long> {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -329,7 +329,7 @@ public sealed interface Lifespan extends Span<Long, Lifespan>, Iterable<Long> {
public record Impl(long lmin, long lmax) implements Lifespan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override

View file

@ -18,8 +18,12 @@ package ghidra.trace.model.time;
import java.util.Collection;
import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public interface TraceTimeManager {
/** The attribute key for controlling the time radix */
String KEY_TIME_RADIX = "_time_radix";
/**
* Create a new snapshot after the latest
*
@ -32,6 +36,7 @@ public interface TraceTimeManager {
* Get the snapshot with the given key, optionally creating it
*
* @param snap the snapshot key
* @param createIfAbsent create the snapshot if it's missing
* @return the snapshot or {@code null}
*/
TraceSnapshot getSnapshot(long snap, boolean createIfAbsent);
@ -105,4 +110,24 @@ public interface TraceTimeManager {
* @return the count
*/
long getSnapshotCount();
/**
* Set the radix for displaying and parsing time (snapshots and step counts)
*
* <p>
* This only affects the GUI, but storing it in the trace gives the back end a means of
* controlling it.
*
* @param radix the radix
*/
void setTimeRadix(TimeRadix radix);
/**
* Get the radix for displaying and parsing time (snapshots and step counts)
*
* @see #setTimeRadix(TimeRadix)
* @return radix the radix
*/
TimeRadix getTimeRadix();
}

View file

@ -18,6 +18,7 @@ package ghidra.trace.model.time.schedule;
import java.util.List;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
public abstract class AbstractStep implements Step {
protected final long threadKey;
@ -34,16 +35,22 @@ public abstract class AbstractStep implements Step {
/**
* Return the step portion of {@link #toString()}
*
* @param radix the radix
* @return the string
*/
protected abstract String toStringStepPart();
protected abstract String toStringStepPart(TimeRadix radix);
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
@Override
public String toString(TimeRadix radix) {
if (threadKey == -1) {
return toStringStepPart();
return toStringStepPart(radix);
}
return String.format("t%d-", threadKey) + toStringStepPart();
return String.format("t%d-%s", threadKey, toStringStepPart(radix));
}
@Override

View file

@ -34,6 +34,7 @@ import ghidra.program.model.lang.Language;
import ghidra.program.model.lang.Register;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.Varnode;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -108,7 +109,7 @@ public class PatchStep implements Step {
protected static void generateSleigh(List<String> result, Language language, Address address,
byte[] data) {
SemisparseByteArray array = new SemisparseByteArray(); // TODO: Seems heavy-handed
SemisparseByteArray array = new SemisparseByteArray(); // Seems heavy-handed
array.putData(address.getOffset(), data);
generateSleigh(result, language, address.getAddressSpace(), array);
}
@ -190,7 +191,7 @@ public class PatchStep implements Step {
}
public static PatchStep parse(long threadKey, String stepSpec) {
// TODO: Can I parse and validate the sleigh here?
// Would be nice to parse and validate the sleigh here, but need a language.
if (!stepSpec.startsWith("{") || !stepSpec.endsWith("}")) {
throw new IllegalArgumentException("Cannot parse step: '" + stepSpec + "'");
}
@ -200,7 +201,7 @@ public class PatchStep implements Step {
public PatchStep(long threadKey, String sleigh) {
this.threadKey = threadKey;
this.sleigh = Objects.requireNonNull(sleigh);
this.hashCode = Objects.hash(threadKey, sleigh); // TODO: May become mutable
this.hashCode = Objects.hash(threadKey, sleigh);
}
private void setSleigh(String sleigh) {
@ -233,6 +234,11 @@ public class PatchStep implements Step {
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
@Override
public String toString(TimeRadix radix) {
if (threadKey == -1) {
return "{" + sleigh + "}";
}
@ -251,7 +257,6 @@ public class PatchStep implements Step {
@Override
public boolean isNop() {
// TODO: If parsing beforehand, base on number of ops
return sleigh.length() == 0;
}
@ -272,7 +277,7 @@ public class PatchStep implements Step {
@Override
public boolean isCompatible(Step step) {
// TODO: Can we combine ops?
// Can we combine ops?
return false; // For now, never combine sleigh steps
}
@ -314,7 +319,7 @@ public class PatchStep implements Step {
return result;
}
// TODO: Compare ops, if/when we pre-compile
// Compare ops, if/when we pre-compile?
result = CompareResult.unrelated(this.sleigh.compareTo(that.sleigh));
if (result != CompareResult.EQUALS) {
return result;

View file

@ -18,14 +18,13 @@ package ghidra.trace.model.time.schedule;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import ghidra.pcode.emu.PcodeMachine;
import ghidra.pcode.emu.PcodeThread;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -33,27 +32,28 @@ import ghidra.util.task.TaskMonitor;
* A sequence of thread steps, each repeated some number of times
*/
public class Sequence implements Comparable<Sequence> {
public static final String SEP = ";";
private static final String SEP = ";";
/**
* Parse (and normalize) a sequence of steps
*
* <p>
* This takes a semicolon-separated list of steps in the form specified by
* {@link Step#parse(String)}. Each step may or may not specify a thread, but it's uncommon for
* any but the first step to omit the thread. The sequence is normalized as it is parsed, so any
* step after the first that omits a thread will be combined with the previous step. When the
* first step applies to the "last thread," it typically means the "event thread" of the source
* trace snapshot.
* {@link Step#parse(String, TimeRadix)}. Each step may or may not specify a thread, but it's
* uncommon for any but the first step to omit the thread. The sequence is normalized as it is
* parsed, so any step after the first that omits a thread will be combined with the previous
* step. When the first step applies to the "last thread," it typically means the "event thread"
* of the source trace snapshot.
*
* @param seqSpec the string specification of the sequence
* @param radix the radix
* @return the parsed sequence
* @throws IllegalArgumentException if the specification is of the wrong form
*/
public static Sequence parse(String seqSpec) {
public static Sequence parse(String seqSpec, TimeRadix radix) {
Sequence result = new Sequence();
for (String stepSpec : seqSpec.split(SEP)) {
Step step = Step.parse(stepSpec);
Step step = Step.parse(stepSpec, radix);
result.advance(step);
}
return result;
@ -109,7 +109,11 @@ public class Sequence implements Comparable<Sequence> {
@Override
public String toString() {
return StringUtils.join(steps, SEP);
return toString(TimeRadix.DEFAULT);
}
public String toString(TimeRadix radix) {
return steps.stream().map(s -> s.toString(radix)).collect(Collectors.joining(SEP));
}
/**

View file

@ -16,17 +16,18 @@
package ghidra.trace.model.time.schedule;
import ghidra.pcode.emu.PcodeThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
public class SkipStep extends AbstractStep {
public static SkipStep parse(long threadKey, String stepSpec) {
public static SkipStep parse(long threadKey, String stepSpec, TimeRadix radix) {
if (!stepSpec.startsWith("s")) {
throw new IllegalArgumentException("Cannot parse skip step: '" + stepSpec + "'");
}
try {
return new SkipStep(threadKey, Long.parseLong(stepSpec.substring(1)));
return new SkipStep(threadKey, radix.decode(stepSpec.substring(1)));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse skip step: '" + stepSpec + "'");
@ -54,8 +55,8 @@ public class SkipStep extends AbstractStep {
}
@Override
protected String toStringStepPart() {
return String.format("s%d", tickCount);
protected String toStringStepPart(TimeRadix radix) {
return "s" + radix.format(tickCount);
}
@Override

View file

@ -22,6 +22,7 @@ import ghidra.pcode.emu.PcodeThread;
import ghidra.program.model.lang.Language;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -40,21 +41,22 @@ public interface Step extends Comparable<Step> {
* applies to the last thread or the event thread.
*
* @param stepSpec the string specification
* @param radix the radix
* @return the parsed step
* @throws IllegalArgumentException if the specification is of the wrong form
*/
static Step parse(String stepSpec) {
static Step parse(String stepSpec, TimeRadix radix) {
if ("".equals(stepSpec)) {
return nop();
}
String[] parts = stepSpec.split("-");
if (parts.length == 1) {
return parse(-1, parts[0].trim());
return parse(-1, parts[0].trim(), radix);
}
if (parts.length == 2) {
String tPart = parts[0].trim();
if (tPart.startsWith("t")) {
return parse(Long.parseLong(tPart.substring(1)), parts[1].trim());
return parse(Long.parseLong(tPart.substring(1)), parts[1].trim(), radix);
}
}
throw new IllegalArgumentException("Cannot parse step: '" + stepSpec + "'");
@ -70,23 +72,26 @@ public interface Step extends Comparable<Step> {
*
* @param threadKey the thread to step, or -1 for the last thread or event thread
* @param stepSpec the string specification
* @param radix the radix
* @return the parsed step
* @throws IllegalArgumentException if the specification is of the wrong form
*/
static Step parse(long threadKey, String stepSpec) {
static Step parse(long threadKey, String stepSpec, TimeRadix radix) {
if (stepSpec.startsWith("s")) {
return SkipStep.parse(threadKey, stepSpec);
return SkipStep.parse(threadKey, stepSpec, radix);
}
if (stepSpec.startsWith("{")) {
return PatchStep.parse(threadKey, stepSpec);
}
return TickStep.parse(threadKey, stepSpec);
return TickStep.parse(threadKey, stepSpec, radix);
}
static TickStep nop() {
return new TickStep(-1, 0);
}
String toString(TimeRadix radix);
StepType getType();
default int getTypeOrder() {

View file

@ -16,6 +16,7 @@
package ghidra.trace.model.time.schedule;
import ghidra.pcode.emu.PcodeThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
@ -24,9 +25,9 @@ import ghidra.util.task.TaskMonitor;
*/
public class TickStep extends AbstractStep {
public static TickStep parse(long threadKey, String stepSpec) {
public static TickStep parse(long threadKey, String stepSpec, TimeRadix radix) {
try {
return new TickStep(threadKey, Long.parseLong(stepSpec));
return new TickStep(threadKey, radix.decode(stepSpec));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException("Cannot parse tick step: '" + stepSpec + "'");
@ -54,8 +55,8 @@ public class TickStep extends AbstractStep {
}
@Override
protected String toStringStepPart() {
return Long.toString(tickCount);
protected String toStringStepPart(TimeRadix radix) {
return radix.format(tickCount);
}
@Override

View file

@ -35,6 +35,62 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
*/
public static final TraceSchedule ZERO = TraceSchedule.snap(0);
/**
* Format for rendering and parsing snaps and step counts
*/
public enum TimeRadix {
/** Use decimal (default) */
DEC("dec", 10, "%d"),
/** Use upper-case hexadecimal */
HEX_UPPER("HEX", 16, "%X"),
/** Use lower-case hexadecimal */
HEX_LOWER("hex", 16, "%x");
/** The default radix (decimal) */
public static final TimeRadix DEFAULT = DEC;
/**
* Get the radix specified by the given string
*
* @param s the name of the specified radix
* @return the radix
*/
public static TimeRadix fromStr(String s) {
return switch (s) {
case "dec" -> DEC;
case "HEX" -> HEX_UPPER;
case "hex" -> HEX_LOWER;
default -> DEFAULT;
};
}
public final String name;
public final int n;
public final String fmt;
private TimeRadix(String name, int n, String fmt) {
this.name = name;
this.n = n;
this.fmt = fmt;
}
public String format(long time) {
return fmt.formatted(time);
}
public long decode(String nm) {
if (nm.startsWith("0x") || nm.startsWith("0X") ||
nm.startsWith("-0x") || nm.startsWith("-0X")) {
return Long.parseLong(nm, 16);
}
if (nm.startsWith("0n") || nm.startsWith("0N") ||
nm.startsWith("-0n") || nm.startsWith("-0N")) {
return Long.parseLong(nm, 10);
}
return Long.parseLong(nm, n);
}
}
/**
* Specifies forms of a stepping schedule.
*
@ -196,14 +252,15 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
* <p>
* A schedule consists of a snap, a optional {@link Sequence} of thread instruction-level steps,
* and optional p-code-level steps (pSteps). The form of {@code steps} and {@code pSteps} is
* specified by {@link Sequence#parse(String)}. Each sequence consists of stepping selected
* threads forward, and/or patching machine state.
* specified by {@link Sequence#parse(String, TimeRadix)}. Each sequence consists of stepping
* selected threads forward, and/or patching machine state.
*
* @param spec the string specification
* @param source the presumed source of the schedule
* @param radix the radix
* @return the parsed schedule
*/
public static TraceSchedule parse(String spec, Source source) {
public static TraceSchedule parse(String spec, Source source, TimeRadix radix) {
String[] parts = spec.split(":", 2);
if (parts.length > 2) {
throw new AssertionError();
@ -212,7 +269,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
final Sequence ticks;
final Sequence pTicks;
try {
snap = Long.decode(parts[0]);
snap = radix.decode(parts[0]);
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -220,7 +277,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
if (parts.length > 1) {
String[] subs = parts[1].split("\\.");
try {
ticks = Sequence.parse(subs[0]);
ticks = Sequence.parse(subs[0], radix);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -230,7 +287,7 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
}
else if (subs.length == 2) {
try {
pTicks = Sequence.parse(subs[1]);
pTicks = Sequence.parse(subs[1], radix);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(PARSE_ERR_MSG, e);
@ -248,13 +305,24 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
}
/**
* As in {@link #parse(String, Source)}, but assumed abnormal
* As in {@link #parse(String, Source, TimeRadix)}, but assumed abnormal
*
* @param spec the string specification
* @param radix the radix
* @return the parsed schedule
*/
public static TraceSchedule parse(String spec, TimeRadix radix) {
return parse(spec, Source.INPUT, radix);
}
/**
* As in {@link #parse(String, TimeRadix)}, but with the {@link TimeRadix#DEFAULT} radix.
*
* @param spec the string specification
* @return the parse sequence
*/
public static TraceSchedule parse(String spec) {
return parse(spec, Source.INPUT);
return parse(spec, TimeRadix.DEFAULT);
}
public enum Source {
@ -318,13 +386,18 @@ public class TraceSchedule implements Comparable<TraceSchedule> {
@Override
public String toString() {
return toString(TimeRadix.DEFAULT);
}
public String toString(TimeRadix radix) {
if (pSteps.isNop()) {
if (steps.isNop()) {
return Long.toString(snap);
return radix.format(snap);
}
return String.format("%d:%s", snap, steps);
return String.format("%s:%s", radix.format(snap), steps.toString(radix));
}
return String.format("%d:%s.%s", snap, steps, pSteps);
return String.format("%s:%s.%s", radix.format(snap), steps.toString(radix),
pSteps.toString(radix));
}
/**

View file

@ -30,6 +30,7 @@ import ghidra.test.AbstractGhidraHeadlessIntegrationTest;
import ghidra.test.ToyProgramBuilder;
import ghidra.trace.database.ToyDBTraceBuilder;
import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.time.schedule.TraceSchedule.TimeRadix;
import ghidra.util.task.TaskMonitor;
public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@ -161,7 +162,7 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@Test
public void testRewind() {
Sequence seq = Sequence.parse("10;t1-20;t2-30");
Sequence seq = Sequence.parse("10;t1-20;t2-30", TimeRadix.DEC);
assertEquals(0, seq.rewind(5));
assertEquals("10;t1-20;t2-25", seq.toString());
@ -181,7 +182,7 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
@Test(expected = IllegalArgumentException.class)
public void testRewindNegativeErr() {
Sequence seq = Sequence.parse("10;t1-20;t2-30");
Sequence seq = Sequence.parse("10;t1-20;t2-30", TimeRadix.DEC);
seq.rewind(-1);
}
@ -251,7 +252,8 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
}
public String strRelativize(String fromSpec, String toSpec) {
Sequence seq = Sequence.parse(toSpec).relativize(Sequence.parse(fromSpec));
Sequence seq = Sequence.parse(toSpec, TimeRadix.DEC)
.relativize(Sequence.parse(fromSpec, TimeRadix.DEC));
return seq == null ? null : seq.toString();
}
@ -503,4 +505,13 @@ public class TraceScheduleTest extends AbstractGhidraHeadlessIntegrationTest {
assertFalse(
TraceSchedule.parse("1:1.1;{r0=1}").differsOnlyByPatch(TraceSchedule.parse("1:1.1")));
}
@Test
public void testTimeRadix() throws Exception {
TraceSchedule schedule = TraceSchedule.parse("A:t10-B.C", TimeRadix.HEX_UPPER);
assertEquals("10:t10-11.12", schedule.toString(TimeRadix.DEC));
assertEquals("A:t10-B.C", schedule.toString(TimeRadix.HEX_UPPER));
assertEquals("a:t10-b.c", schedule.toString(TimeRadix.HEX_LOWER));
}
}

View file

@ -16,6 +16,7 @@
package generic;
import java.util.Comparator;
import java.util.function.Function;
import generic.Span.Domain;
@ -107,12 +108,12 @@ public interface End<T> {
}
@Override
public String toMinString() {
public String toMinString(Function<? super End<Void>, String> nToString) {
return "(-inf";
}
@Override
public String toMaxString() {
public String toMaxString(Function<? super End<Void>, String> nToString) {
return "#ERROR-inf)";
}
@ -138,12 +139,12 @@ public interface End<T> {
}
@Override
public String toMinString() {
public String toMinString(Function<? super End<Void>, String> nToString) {
return "(#ERROR+inf";
}
@Override
public String toMaxString() {
public String toMaxString(Function<? super End<Void>, String> nToString) {
return "+inf)";
}
@ -235,31 +236,33 @@ public interface End<T> {
/**
* An endpoint representing a bound
*
* @param val the value of the endpoint
* @param epsilon determines whether the endpoint is included
* @param <T> the type of values
*/
record Point<T> (T val, Epsilon epsilon) implements End<T> {
record Point<T>(T val, Epsilon epsilon) implements End<T> {
@Override
public String toMinString() {
public String toMinString(Function<? super End<T>, String> nToString) {
switch (epsilon) {
case NEGATIVE:
return "(#ERROR" + val;
return "(#ERROR" + nToString.apply(this);
case ZERO:
return "[" + val;
return "[" + nToString.apply(this);
case POSITIVE:
return "(" + val;
return "(" + nToString.apply(this);
}
throw new AssertionError();
}
@Override
public String toMaxString() {
public String toMaxString(Function<? super End<T>, String> nToString) {
switch (epsilon) {
case NEGATIVE:
return val + ")";
return nToString.apply(this) + ")";
case ZERO:
return val + "]";
return nToString.apply(this) + "]";
case POSITIVE:
return "#ERROR" + val + ")";
return "#ERROR" + nToString.apply(this) + ")";
}
throw new AssertionError();
}
@ -358,13 +361,13 @@ public interface End<T> {
}
@Override
public String toMinString(End<N> min) {
return min.toMinString();
public String toMinString(End<N> min, Function<? super End<N>, String> nToString) {
return min.toMinString(nToString);
}
@Override
public String toMaxString(End<N> max) {
return max.toMaxString();
public String toMaxString(End<N> max, Function<? super End<N>, String> nToString) {
return max.toMaxString(nToString);
}
@Override
@ -394,16 +397,18 @@ public interface End<T> {
}
/**
* @see Domain#toMinString(Object)
* @see Domain#toMinString(Object, Function)
* @param nToString the endpoint-to-string function
* @return the string
*/
String toMinString();
String toMinString(Function<? super End<T>, String> nToString);
/**
* @see Domain#toMaxString(Object)
* @see Domain#toMaxString(Object, Function)
* @param nToString the endpoint-to-string function
* @return the string
*/
String toMaxString();
String toMaxString(Function<? super End<T>, String> nToString);
/**
* Increment this endpoint, only by changing the coefficient of epsilon

View file

@ -17,7 +17,8 @@ package ghidra.util.database;
import db.Field;
import generic.End;
import generic.End.*;
import generic.End.EndDomain;
import generic.End.EndSpan;
import generic.Span;
import ghidra.util.database.DirectedIterator.Direction;
@ -148,7 +149,7 @@ public sealed interface FieldSpan extends EndSpan<Field, FieldSpan> {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -163,7 +164,7 @@ public sealed interface FieldSpan extends EndSpan<Field, FieldSpan> {
record Impl(End<Field> min, End<Field> max) implements FieldSpan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override

View file

@ -165,7 +165,7 @@ public sealed interface KeySpan extends Span<Long, KeySpan> {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -180,7 +180,7 @@ public sealed interface KeySpan extends Span<Long, KeySpan> {
record Impl(Long min, Long max) implements KeySpan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override

View file

@ -172,7 +172,7 @@ public class DemoSpanCellRendererTest extends AbstractGhidraHeadedIntegrationTes
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -184,7 +184,7 @@ public class DemoSpanCellRendererTest extends AbstractGhidraHeadedIntegrationTes
record Impl(Integer min, Integer max) implements IntSpan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override

View file

@ -161,9 +161,9 @@ public class GTableCellRenderer extends AbstractGCellRenderer implements TableCe
boolean hasFocus = data.hasFocus();
Settings settings = data.getColumnSettings();
if (value instanceof Number) {
if (value instanceof Number n) {
setHorizontalAlignment(SwingConstants.RIGHT);
setText(formatNumber((Number) value, settings));
setText(formatNumber(n, settings));
}
else {
setText(getText(value));

View file

@ -17,6 +17,7 @@ package generic;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import generic.ULongSpan.*;
@ -146,30 +147,45 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
* Render the given span as a string
*
* @param s the span
* @param nToString a function to convert n to a string
* @return the string
*/
default String toString(S s) {
return s.isEmpty() ? "(empty)" : toMinString(s.min()) + ".." + toMaxString(s.max());
return toString(s, this::toString);
}
/**
* Render the given span as a string
*
* @param s the span
* @param nToString a function to convert n to a string
* @return the string
*/
default String toString(S s, Function<? super N, String> nToString) {
return s.isEmpty() ? "(empty)"
: toMinString(s.min(), nToString) + ".." + toMaxString(s.max(), nToString);
}
/**
* Render the lower bound of a span
*
* @param min the lower bound
* @param nToString a function to convert n to a string
* @return the string
*/
default String toMinString(N min) {
return min().equals(min) ? "(-inf" : ("[" + toString(min));
default String toMinString(N min, Function<? super N, String> nToString) {
return min().equals(min) ? "(-inf" : ("[" + nToString.apply(min));
}
/**
* Render the upper bound of a span
*
* @param max the upper bound
* @param nToString a function to convert n to a string
* @return the string
*/
default String toMaxString(N max) {
return max().equals(max) ? "+inf)" : (toString(max) + "]");
default String toMaxString(N max, Function<? super N, String> nToString) {
return max().equals(max) ? "+inf)" : (nToString.apply(max) + "]");
}
/**
@ -558,6 +574,14 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
*/
boolean isEmpty();
/**
* Render this set as a string, using the given endpoint-to-string function
*
* @param nToString the endpoint-to-string function
* @return the string
*/
String toString(Function<? super N, String> nToString);
/**
* Iterate the spans in this set
*
@ -847,13 +871,16 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
return true;
}
public String toString(Function<? super N, String> nToString) {
return spanTree.values()
.stream()
.map(e -> domain.toString(e.getKey(), nToString) + '=' + e.getValue())
.collect(Collectors.joining(",", "{", "}"));
}
@Override
public String toString() {
return "{" + spanTree.values()
.stream()
.map(e -> domain.toString(e.getKey()) + '=' + e.getValue())
.collect(Collectors.joining(",")) +
"}";
return toString(domain::toString);
}
@Override
@ -863,7 +890,7 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
@Override
public NavigableSet<S> spans() {
// TODO: Make this a view?
// Make this a view?
return spanTree.values()
.stream()
.map(e -> e.getKey())
@ -1007,11 +1034,17 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
return true;
}
@Override
public String toString(Function<? super N, String> nToString) {
return map.spans()
.stream()
.map(s -> domain.toString(s, nToString))
.collect(Collectors.joining(",", "[", "]"));
}
@Override
public String toString() {
return '[' +
map.spans().stream().map(s -> domain.toString(s)).collect(Collectors.joining(",")) +
']';
return toString(domain::toString);
}
/**
@ -1087,11 +1120,12 @@ public interface Span<N, S extends Span<N, S>> extends Comparable<S> {
/**
* Provides a default {@link Object#toString} implementation
*
* @param nToString the endpoint-to-string function
* @return the string
*/
@SuppressWarnings("unchecked")
default String doToString() {
return domain().toString((S) this);
default String toString(Function<? super N, String> nToString) {
return domain().toString((S) this, nToString);
}
/**

View file

@ -131,7 +131,7 @@ public interface ULongSpan extends Span<Long, ULongSpan> {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override
@ -151,7 +151,7 @@ public interface ULongSpan extends Span<Long, ULongSpan> {
record Impl(Long min, Long max) implements ULongSpan {
@Override
public String toString() {
return doToString();
return toString(DOMAIN::toString);
}
@Override