Merge remote-tracking branch 'origin/GP-1608_refactorTraceTabs--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-04-04 08:32:05 -04:00
commit f963f23a8f
26 changed files with 579 additions and 653 deletions

View file

@ -135,6 +135,13 @@ public interface Target {
} }
} }
/**
* Describe the target for display in the UI
*
* @return the description
*/
String describe();
/** /**
* Check if the target is still valid * Check if the target is still valid
* *

View file

@ -45,6 +45,16 @@ import ghidra.util.NotOwnerException;
public class DebuggerCoordinates { public class DebuggerCoordinates {
/**
* Coordinates that indicate no trace is active in the Debugger UI.
*
* <p>
* Typically, that only happens when no trace is open. Telling the trace manager to activate
* {@code NOWHERE} will cause it to instead activate the most recently active trace, which may
* very well be the current trace, resulting in no change. Internally, the trace manager will
* activate {@code NOWHERE} whenever the current trace is closed, effectively activating the
* most recent trace other than the one just closed.
*/
public static final DebuggerCoordinates NOWHERE = public static final DebuggerCoordinates NOWHERE =
new DebuggerCoordinates(null, null, null, null, null, null, null, null); new DebuggerCoordinates(null, null, null, null, null, null, null, null);

View file

@ -82,6 +82,12 @@ public class TraceRmiTarget extends AbstractTarget {
this.supportedBreakpointKinds = computeSupportedBreakpointKinds(); this.supportedBreakpointKinds = computeSupportedBreakpointKinds();
} }
@Override
public String describe() {
return "%s in %s at %s (rmi)".formatted(getTrace().getDomainFile().getName(),
connection.getDescription(), connection.getRemoteAddress());
}
@Override @Override
public boolean isValid() { public boolean isValid() {
return !connection.isClosed() && connection.isTarget(trace); return !connection.isClosed() && connection.isTarget(trace);

View file

@ -1614,7 +1614,7 @@ public interface DebuggerResources {
} }
interface CloseAllTracesAction extends CloseTraceAction { interface CloseAllTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " All Traces"; String NAME = NAME_PREFIX + "All Traces";
String DESCRIPTION = "Close all traces"; String DESCRIPTION = "Close all traces";
String HELP_ANCHOR = "close_all_traces"; String HELP_ANCHOR = "close_all_traces";
@ -1641,7 +1641,7 @@ public interface DebuggerResources {
} }
interface CloseOtherTracesAction extends CloseTraceAction { interface CloseOtherTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " Other Traces"; String NAME = NAME_PREFIX + "Other Traces";
String DESCRIPTION = "Close all traces except the current one"; String DESCRIPTION = "Close all traces except the current one";
String HELP_ANCHOR = "close_other_traces"; String HELP_ANCHOR = "close_other_traces";
@ -1668,7 +1668,7 @@ public interface DebuggerResources {
} }
interface CloseDeadTracesAction extends CloseTraceAction { interface CloseDeadTracesAction extends CloseTraceAction {
String NAME = NAME_PREFIX + " Dead Traces"; String NAME = NAME_PREFIX + "Dead Traces";
String DESCRIPTION = "Close all traces not being recorded"; String DESCRIPTION = "Close all traces not being recorded";
String HELP_ANCHOR = "close_dead_traces"; String HELP_ANCHOR = "close_dead_traces";

View file

@ -55,9 +55,6 @@ import ghidra.trace.model.time.schedule.TraceSchedule;
import ghidra.trace.util.TraceEvents; import ghidra.trace.util.TraceEvents;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
@PluginInfo( @PluginInfo(
shortDescription = "Debugger global controls", shortDescription = "Debugger global controls",
@ -357,18 +354,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin
if (target == null) { if (target == null) {
return; return;
} }
TargetActionTask.executeTask(tool, new Task("Disconnect", false, false, false) { TargetActionTask.executeTask(tool, new DisconnectTask(tool, List.of(target)));
@Override
public void run(TaskMonitor monitor) throws CancelledException {
try {
target.disconnect();
}
catch (Exception e) {
tool.setStatusInfo("Disconnect failed: " + e, true);
Msg.error(this, "Disconnect failed: " + e, e);
}
}
});
} }
private boolean haveEmuAndTrace() { private boolean haveEmuAndTrace() {

View file

@ -0,0 +1,53 @@
/* ###
* 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.control;
import java.util.Collection;
import java.util.List;
import ghidra.debug.api.target.Target;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.Task;
import ghidra.util.task.TaskMonitor;
public class DisconnectTask extends Task {
private final PluginTool tool;
private final List<Target> targets;
public DisconnectTask(PluginTool tool, Collection<Target> targets) {
super("Disconnect", false, true, false);
this.tool = tool;
this.targets = List.copyOf(targets);
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
monitor.initialize(targets.size(), "Disconnecting...");
for (Target target : targets) {
try {
monitor.setMessage("Disconnecting " + target.describe());
target.disconnect();
monitor.increment();
}
catch (Exception e) {
tool.setStatusInfo("Disconnect failed: " + e, true);
Msg.error(this, "Disconnect failed: " + e, e);
}
}
}
}

View file

@ -57,6 +57,8 @@ import ghidra.app.plugin.core.debug.gui.DebuggerResources.FollowsCurrentThreadAc
import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction; import ghidra.app.plugin.core.debug.gui.DebuggerResources.OpenProgramAction;
import ghidra.app.plugin.core.debug.gui.action.*; import ghidra.app.plugin.core.debug.gui.action.*;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext; import ghidra.app.plugin.core.debug.gui.modules.DebuggerMissingModuleActionContext;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerTraceFileActionContext;
import ghidra.app.plugin.core.debug.gui.trace.DebuggerTraceTabPanel;
import ghidra.app.plugin.core.debug.utils.ProgramLocationUtils; import ghidra.app.plugin.core.debug.utils.ProgramLocationUtils;
import ghidra.app.plugin.core.debug.utils.ProgramURLUtils; import ghidra.app.plugin.core.debug.utils.ProgramURLUtils;
import ghidra.app.plugin.core.marker.MarkerMarginProvider; import ghidra.app.plugin.core.marker.MarkerMarginProvider;
@ -342,6 +344,7 @@ public class DebuggerListingProvider extends CodeViewerProvider {
protected final ListenerSet<LocationTrackingSpecChangeListener> trackingSpecChangeListeners = protected final ListenerSet<LocationTrackingSpecChangeListener> trackingSpecChangeListeners =
new ListenerSet<>(LocationTrackingSpecChangeListener.class, true); new ListenerSet<>(LocationTrackingSpecChangeListener.class, true);
protected final DebuggerTraceTabPanel traceTabs;
protected final DebuggerLocationLabel locationLabel = new DebuggerLocationLabel(); protected final DebuggerLocationLabel locationLabel = new DebuggerLocationLabel();
protected final JLabel trackingLabel = new JLabel(); protected final JLabel trackingLabel = new JLabel();
@ -372,6 +375,8 @@ public class DebuggerListingProvider extends CodeViewerProvider {
this.plugin = plugin; this.plugin = plugin;
this.isMainListing = isConnected; this.isMainListing = isConnected;
// TODO: An icon to distinguish dynamic from static
syncTrait = new ForListingSyncTrait(); syncTrait = new ForListingSyncTrait();
goToTrait = new ForListingGoToTrait(); goToTrait = new ForListingGoToTrait();
trackingTrait = new ForListingTrackingTrait(); trackingTrait = new ForListingTrackingTrait();
@ -394,13 +399,21 @@ public class DebuggerListingProvider extends CodeViewerProvider {
readsMemTrait.goToCoordinates(current); readsMemTrait.goToCoordinates(current);
locationLabel.goToCoordinates(current); locationLabel.goToCoordinates(current);
// TODO: An icon to distinguish dynamic from static if (isConnected) {
traceTabs = new DebuggerTraceTabPanel(plugin);
}
else {
traceTabs = null;
}
addDisplayListener(readsMemTrait.getDisplayListener()); addDisplayListener(readsMemTrait.getDisplayListener());
JPanel northPanel = new JPanel(new BorderLayout()); JPanel northPanel = new JPanel(new BorderLayout());
northPanel.add(locationLabel); northPanel.add(locationLabel);
northPanel.add(trackingLabel, BorderLayout.EAST); northPanel.add(trackingLabel, BorderLayout.EAST);
if (traceTabs != null) {
northPanel.add(traceTabs, BorderLayout.NORTH);
}
this.setNorthComponent(northPanel); this.setNorthComponent(northPanel);
if (isConnected) { if (isConnected) {
setTitle(DebuggerResources.TITLE_PROVIDER_LISTING); setTitle(DebuggerResources.TITLE_PROVIDER_LISTING);
@ -929,6 +942,12 @@ public class DebuggerListingProvider extends CodeViewerProvider {
@Override @Override
public ActionContext getActionContext(MouseEvent event) { public ActionContext getActionContext(MouseEvent event) {
if (traceTabs != null) {
DebuggerTraceFileActionContext traceCtx = traceTabs.getActionContext(event);
if (traceCtx != null) {
return traceCtx;
}
}
if (event == null || event.getSource() != locationLabel) { if (event == null || event.getSource() != locationLabel) {
return super.getActionContext(event); return super.getActionContext(event);
} }
@ -1036,7 +1055,6 @@ public class DebuggerListingProvider extends CodeViewerProvider {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
// Attempt to open probable matches. All others, list to import // Attempt to open probable matches. All others, list to import
// TODO: What if sections are not presented?
for (TraceModule mod : modules) { for (TraceModule mod : modules) {
DomainFile match = mappingService.findBestModuleProgram(space, mod); DomainFile match = mappingService.findBestModuleProgram(space, mod);
if (match == null) { if (match == null) {
@ -1064,8 +1082,9 @@ public class DebuggerListingProvider extends CodeViewerProvider {
new DebuggerMissingModuleActionContext(mod)); new DebuggerMissingModuleActionContext(mod));
} }
/** /**
* Once the programs are opened, including those which are successfully imported, the mapper * Once the programs are opened, including those which are successfully imported, the
* bot should take over, eventually invoking callbacks to our mapping change listener. * automatic mapper should take effect, eventually invoking callbacks to our mapping change
* listener.
*/ */
} }

View file

@ -76,6 +76,14 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
coords); coords);
} }
DebuggerCoordinates coordsForObject(TraceObject object) {
if (provider.current.getTrace() != object.getTrace()) {
// This can happen transiently, so just find something graceful
return DebuggerCoordinates.NOWHERE.object(object);
}
return provider.current.object(object);
}
private class ThreadPcColumn extends TraceValueObjectPropertyColumn<Address> { private class ThreadPcColumn extends TraceValueObjectPropertyColumn<Address> {
public ThreadPcColumn() { public ThreadPcColumn() {
super(Address.class); super(Address.class);
@ -84,7 +92,8 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
@Override @Override
public ValueProperty<Address> getProperty(ValueRow row) { public ValueProperty<Address> getProperty(ValueRow row) {
TraceObject obj = row.getValue().getChild(); TraceObject obj = row.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj);
DebuggerCoordinates coords = coordsForObject(obj);
return new ValueAddressProperty(row) { return new ValueAddressProperty(row) {
@Override @Override
public Address getValue() { public Address getValue() {
@ -110,7 +119,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
public Function getValue(ValueRow rowObject, Settings settings, Trace data, public Function getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException { ServiceProvider serviceProvider) throws IllegalArgumentException {
TraceObject obj = rowObject.getValue().getChild(); TraceObject obj = rowObject.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj); DebuggerCoordinates coords = coordsForObject(obj);
Address pc = computeProgramCounter(coords); Address pc = computeProgramCounter(coords);
if (pc == null) { if (pc == null) {
return null; return null;
@ -129,7 +138,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
public String getValue(ValueRow rowObject, Settings settings, Trace data, public String getValue(ValueRow rowObject, Settings settings, Trace data,
ServiceProvider serviceProvider) throws IllegalArgumentException { ServiceProvider serviceProvider) throws IllegalArgumentException {
TraceObject obj = rowObject.getValue().getChild(); TraceObject obj = rowObject.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj); DebuggerCoordinates coords = coordsForObject(obj);
Address pc = computeProgramCounter(coords); Address pc = computeProgramCounter(coords);
if (pc == null) { if (pc == null) {
return null; return null;
@ -146,7 +155,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel<TraceOb
@Override @Override
public ValueProperty<Address> getProperty(ValueRow row) { public ValueProperty<Address> getProperty(ValueRow row) {
TraceObject obj = row.getValue().getChild(); TraceObject obj = row.getValue().getChild();
DebuggerCoordinates coords = provider.current.object(obj); DebuggerCoordinates coords = coordsForObject(obj);
return new ValueAddressProperty(row) { return new ValueAddressProperty(row) {
@Override @Override
public Address getValue() { public Address getValue() {

View file

@ -28,7 +28,8 @@ import docking.WindowPosition;
import docking.action.DockingActionIf; import docking.action.DockingActionIf;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.services.*; import ghidra.app.services.DebuggerEmulationService;
import ghidra.app.services.DebuggerTargetService;
import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.debug.api.tracemgr.DebuggerCoordinates;
import ghidra.framework.model.DomainObjectChangeRecord; import ghidra.framework.model.DomainObjectChangeRecord;
import ghidra.framework.model.DomainObjectEvent; import ghidra.framework.model.DomainObjectEvent;
@ -97,8 +98,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
@AutoServiceConsumed @AutoServiceConsumed
DebuggerTargetService targetService; DebuggerTargetService targetService;
// @AutoServiceConsumed // via method
private DebuggerTraceManagerService traceManager;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private final AutoService.Wiring autoServiceWiring; private final AutoService.Wiring autoServiceWiring;
@ -106,7 +105,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
private JPanel mainPanel; private JPanel mainPanel;
DebuggerTraceTabPanel traceTabs;
JPopupMenu traceTabPopupMenu; JPopupMenu traceTabPopupMenu;
DebuggerThreadsPanel panel; DebuggerThreadsPanel panel;
DebuggerLegacyThreadsPanel legacyPanel; DebuggerLegacyThreadsPanel legacyPanel;
@ -133,12 +131,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
setVisible(true); setVisible(true);
} }
@AutoServiceConsumed
public void setTraceManager(DebuggerTraceManagerService traceManager) {
this.traceManager = traceManager;
contextChanged();
}
@AutoServiceConsumed @AutoServiceConsumed
public void setEmulationService(DebuggerEmulationService emulationService) { public void setEmulationService(DebuggerEmulationService emulationService) {
contextChanged(); contextChanged();
@ -152,7 +144,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
current = coordinates; current = coordinates;
traceTabs.coordinatesActivated(coordinates);
if (Trace.isLegacy(coordinates.getTrace())) { if (Trace.isLegacy(coordinates.getTrace())) {
panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); panel.coordinatesActivated(DebuggerCoordinates.NOWHERE);
legacyPanel.coordinatesActivated(coordinates); legacyPanel.coordinatesActivated(coordinates);
@ -191,10 +182,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
myActionContext = legacyPanel.getActionContext(); myActionContext = legacyPanel.getActionContext();
} }
void traceTabsContextChanged() {
myActionContext = traceTabs.getActionContext();
}
@Override @Override
public ActionContext getActionContext(MouseEvent event) { public ActionContext getActionContext(MouseEvent event) {
if (myActionContext == null) { if (myActionContext == null) {
@ -211,10 +198,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter {
panel = new DebuggerThreadsPanel(this); panel = new DebuggerThreadsPanel(this);
legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this); legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this);
mainPanel.add(panel); mainPanel.add(panel);
traceTabs = new DebuggerTraceTabPanel(this);
mainPanel.add(traceTabs, BorderLayout.NORTH);
} }
protected void createActions() { protected void createActions() {

View file

@ -13,27 +13,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package ghidra.app.plugin.core.debug.gui.thread; package ghidra.app.plugin.core.debug.gui.trace;
import java.awt.Rectangle; import java.awt.event.MouseEvent;
import java.awt.event.*;
import java.util.Objects;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.JList;
import javax.swing.event.ListSelectionEvent;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.widgets.HorizontalTabPanel; import docking.widgets.tab.GTabPanel;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; import ghidra.app.plugin.core.debug.event.*;
import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent;
import ghidra.app.plugin.core.debug.gui.DebuggerResources; import ghidra.app.plugin.core.debug.gui.DebuggerResources;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerTraceFileActionContext;
import ghidra.app.plugin.core.progmgr.MultiTabPlugin;
import ghidra.app.services.DebuggerTargetService; import ghidra.app.services.DebuggerTargetService;
import ghidra.app.services.DebuggerTraceManagerService; import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.target.Target; import ghidra.debug.api.target.Target;
import ghidra.debug.api.target.TargetPublicationListener; import ghidra.debug.api.target.TargetPublicationListener;
import ghidra.debug.api.tracemgr.DebuggerCoordinates; import ghidra.framework.model.*;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.framework.plugintool.util.PluginEventListener; import ghidra.framework.plugintool.util.PluginEventListener;
@ -42,26 +39,21 @@ import ghidra.util.Swing;
import utilities.util.SuppressableCallback; import utilities.util.SuppressableCallback;
import utilities.util.SuppressableCallback.Suppression; import utilities.util.SuppressableCallback.Suppression;
public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace> public class DebuggerTraceTabPanel extends GTabPanel<Trace>
implements PluginEventListener { implements PluginEventListener, DomainObjectListener {
private class TargetsChangeListener implements TargetPublicationListener { private class TargetsChangeListener implements TargetPublicationListener {
@Override @Override
public void targetPublished(Target target) { public void targetPublished(Target target) {
Swing.runIfSwingOrRunLater(() -> repaint()); Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace()));
} }
@Override @Override
public void targetWithdrawn(Target target) { public void targetWithdrawn(Target target) {
Swing.runIfSwingOrRunLater(() -> repaint()); Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace()));
} }
} }
private final DebuggerThreadsPlugin plugin;
private final DebuggerThreadsProvider provider;
// @AutoServiceConsumed by method // @AutoServiceConsumed by method
DebuggerTargetService targetService; DebuggerTargetService targetService;
@AutoServiceConsumed @AutoServiceConsumed
@ -78,101 +70,80 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
private final SuppressableCallback<Void> cbCoordinateActivation = new SuppressableCallback<>(); private final SuppressableCallback<Void> cbCoordinateActivation = new SuppressableCallback<>();
private DebuggerTraceFileActionContext myActionContext; public DebuggerTraceTabPanel(Plugin plugin) {
super("Trace");
public DebuggerTraceTabPanel(DebuggerThreadsProvider provider) {
this.plugin = provider.plugin;
this.provider = provider;
this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this);
PluginTool tool = plugin.getTool(); PluginTool tool = plugin.getTool();
tool.addEventListener(TraceOpenedPluginEvent.class, this); tool.addEventListener(TraceOpenedPluginEvent.class, this);
tool.addEventListener(TraceActivatedPluginEvent.class, this);
tool.addEventListener(TraceClosedPluginEvent.class, this); tool.addEventListener(TraceClosedPluginEvent.class, this);
list.setCellRenderer(new TabListCellRenderer<>() { setNameFunction(this::getNameForTrace);
protected String getText(Trace value) { setIconFunction(this::getIconForTrace);
return value.getName(); setToolTipFunction(this::getTipForTrace);
} setSelectedTabConsumer(this::traceTabSelected);
// Cannot use method ref here, since traceManager is still null
protected Icon getIcon(Trace value) { setCloseTabConsumer(t -> traceManager.closeTrace(t));
if (targetService == null) {
return super.getIcon(value);
}
Target target = targetService.getTarget(value);
if (target == null || !target.isValid()) {
return super.getIcon(value);
}
return DebuggerResources.ICON_RECORD;
}
});
list.getSelectionModel().addListSelectionListener(this::traceTabSelected);
list.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
setTraceTabActionContext(null);
}
});
list.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
setTraceTabActionContext(e);
}
});
actionCloseTrace = CloseTraceAction.builderPopup(plugin) actionCloseTrace = CloseTraceAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class) .withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> c.getTrace() != null) .popupWhen(c -> {
Trace trace = c.getTrace();
if (trace == null) {
return false;
}
actionCloseTrace.getPopupMenuData()
.setMenuItemName(CloseTraceAction.NAME_PREFIX + getNameForTrace(trace));
return true;
})
.onAction(c -> traceManager.closeTrace(c.getTrace())) .onAction(c -> traceManager.closeTrace(c.getTrace()))
.buildAndInstallLocal(provider); .buildAndInstall(tool);
actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin) actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class) .withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> !traceManager.getOpenTraces().isEmpty()) .popupWhen(c -> !traceManager.getOpenTraces().isEmpty())
.onAction(c -> traceManager.closeAllTraces()) .onAction(c -> traceManager.closeAllTraces())
.buildAndInstallLocal(provider); .buildAndInstall(tool);
actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin) actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class) .withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null) .popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null)
.onAction(c -> traceManager.closeOtherTraces(c.getTrace())) .onAction(c -> traceManager.closeOtherTraces(c.getTrace()))
.buildAndInstallLocal(provider); .buildAndInstall(tool);
actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin) actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin)
.withContext(DebuggerTraceFileActionContext.class) .withContext(DebuggerTraceFileActionContext.class)
.popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && targetService != null) .popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && targetService != null)
.onAction(c -> traceManager.closeDeadTraces()) .onAction(c -> traceManager.closeDeadTraces())
.buildAndInstallLocal(provider); .buildAndInstall(tool);
} }
private Trace computeClickedTraceTab(MouseEvent e) { private String getNameForTrace(Trace trace) {
JList<Trace> list = getList(); return DomainObjectDisplayUtils.getTabText(trace);
int i = list.locationToIndex(e.getPoint()); }
if (i < 0) {
private Icon getIconForTrace(Trace trace) {
if (targetService == null) {
return null; return null;
} }
Rectangle cell = list.getCellBounds(i, i); Target target = targetService.getTarget(trace);
if (!cell.contains(e.getPoint())) { if (target == null || !target.isValid()) {
return null; return null;
} }
return getItem(i); return DebuggerResources.ICON_RECORD;
} }
private Trace setTraceTabActionContext(MouseEvent e) { private String getTipForTrace(Trace trace) {
Trace newTrace = e == null ? getSelectedItem() : computeClickedTraceTab(e); return DomainObjectDisplayUtils.getToolTip(trace);
actionCloseTrace.getPopupMenuData()
.setMenuItemName(
CloseTraceAction.NAME_PREFIX + (newTrace == null ? "..." : newTrace.getName()));
myActionContext = new DebuggerTraceFileActionContext(newTrace);
provider.traceTabsContextChanged();
return newTrace;
} }
public DebuggerTraceFileActionContext getActionContext() { public DebuggerTraceFileActionContext getActionContext(MouseEvent e) {
return myActionContext; if (e == null) {
return null;
} }
Trace trace = getValueFor(e);
public void coordinatesActivated(DebuggerCoordinates coordinates) { if (trace == null) {
try (Suppression supp = cbCoordinateActivation.suppress(null)) { return null;
setSelectedItem(coordinates.getTrace());
} }
return new DebuggerTraceFileActionContext(trace);
} }
@AutoServiceConsumed @AutoServiceConsumed
@ -186,28 +157,46 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel<Trace>
} }
} }
protected void add(Trace trace) {
addTab(trace);
trace.removeListener(this);
trace.addListener(this);
}
protected void remove(Trace trace) {
trace.removeListener(this);
removeTab(trace);
}
@Override @Override
public void eventSent(PluginEvent event) { public void eventSent(PluginEvent event) {
if (Objects.equals(event.getSourceName(), plugin.getName())) {
return;
}
if (event instanceof TraceOpenedPluginEvent evt) { if (event instanceof TraceOpenedPluginEvent evt) {
try (Suppression supp = cbCoordinateActivation.suppress(null)) { try (Suppression supp = cbCoordinateActivation.suppress(null)) {
addItem(evt.getTrace()); add(evt.getTrace());
}
}
else if (event instanceof TraceActivatedPluginEvent evt) {
Trace trace = evt.getActiveCoordinates().getTrace();
try (Suppression supp = cbCoordinateActivation.suppress(null)) {
selectTab(trace);
} }
} }
else if (event instanceof TraceClosedPluginEvent evt) { else if (event instanceof TraceClosedPluginEvent evt) {
Trace trace = evt.getTrace();
try (Suppression supp = cbCoordinateActivation.suppress(null)) { try (Suppression supp = cbCoordinateActivation.suppress(null)) {
removeItem(evt.getTrace()); remove(trace);
} }
} }
} }
private void traceTabSelected(ListSelectionEvent e) { @Override
if (e.getValueIsAdjusting()) { public void domainObjectChanged(DomainObjectChangedEvent ev) {
return; if (ev.getSource() instanceof Trace trace) {
refreshTab(trace);
} }
Trace newTrace = setTraceTabActionContext(null); }
private void traceTabSelected(Trace newTrace) {
cbCoordinateActivation.invoke(() -> { cbCoordinateActivation.invoke(() -> {
traceManager.activateTrace(newTrace); traceManager.activateTrace(newTrace);
}); });

View file

@ -90,6 +90,12 @@ public class TraceRecorderTarget extends AbstractTarget {
this.recorder = recorder; this.recorder = recorder;
} }
@Override
public String describe() {
return "%s in %s (recorder)".formatted(getTrace().getDomainFile().getName(),
recorder.getTarget().getModel().getBrief());
}
@Override @Override
public boolean isValid() { public boolean isValid() {
return recorder.isRecording(); return recorder.isRecording();

View file

@ -410,7 +410,7 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener {
continue; continue;
} }
try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(df)) { try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(df)) {
if (programs.contains(peek.object)) { if (peek.object != null && programs.contains(peek.object)) {
result.add(e); result.add(e);
} }
} }

View file

@ -26,10 +26,13 @@ import java.util.stream.Stream;
import docking.ActionContext; import docking.ActionContext;
import docking.action.DockingAction; import docking.action.DockingAction;
import docking.action.ToggleDockingAction; import docking.action.ToggleDockingAction;
import docking.widgets.OptionDialog;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.*; import ghidra.app.plugin.core.debug.event.*;
import ghidra.app.plugin.core.debug.gui.DebuggerResources.*; import ghidra.app.plugin.core.debug.gui.DebuggerResources.*;
import ghidra.app.plugin.core.debug.gui.control.DisconnectTask;
import ghidra.app.plugin.core.debug.gui.control.TargetActionTask;
import ghidra.app.services.*; import ghidra.app.services.*;
import ghidra.app.services.DebuggerControlService.ControlModeChangeListener; import ghidra.app.services.DebuggerControlService.ControlModeChangeListener;
import ghidra.async.*; import ghidra.async.*;
@ -135,8 +138,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
private void threadDeleted(TraceThread thread) { private void threadDeleted(TraceThread thread) {
synchronized (listenersByTrace) { synchronized (listenersByTrace) {
DebuggerCoordinates last = lastCoordsByTrace.get(trace); LastCoords last = lastCoordsByTrace.get(trace);
if (last != null && last.getThread() == thread) { if (last != null && last.coords.getThread() == thread) {
lastCoordsByTrace.remove(trace); lastCoordsByTrace.remove(trace);
} }
} }
@ -261,7 +264,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
} }
} }
protected final Map<Trace, DebuggerCoordinates> lastCoordsByTrace = new WeakHashMap<>(); protected record LastCoords(Long time, DebuggerCoordinates coords) {
public static final LastCoords NEVER = new LastCoords(null, DebuggerCoordinates.NOWHERE);
public LastCoords(DebuggerCoordinates coords) {
this(System.currentTimeMillis(), coords);
}
public LastCoords keepTime(DebuggerCoordinates adjusted) {
return new LastCoords(time, adjusted);
}
}
protected final Map<Trace, LastCoords> lastCoordsByTrace = new WeakHashMap<>();
protected final Map<Trace, ListenerForTraceChanges> listenersByTrace = new WeakHashMap<>(); protected final Map<Trace, ListenerForTraceChanges> listenersByTrace = new WeakHashMap<>();
protected final Set<Trace> tracesView = Collections.unmodifiableSet(listenersByTrace.keySet()); protected final Set<Trace> tracesView = Collections.unmodifiableSet(listenersByTrace.keySet());
@ -275,6 +291,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
protected final AsyncReference<Boolean, Void> saveTracesByDefault = new AsyncReference<>(true); protected final AsyncReference<Boolean, Void> saveTracesByDefault = new AsyncReference<>(true);
@AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class) @AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class)
protected final AsyncReference<Boolean, Void> autoCloseOnTerminate = new AsyncReference<>(true); protected final AsyncReference<Boolean, Void> autoCloseOnTerminate = new AsyncReference<>(true);
// Do not save this one, it's for testing only
protected boolean ensureActiveTrace = true;
// @AutoServiceConsumed via method // @AutoServiceConsumed via method
private DebuggerTargetService targetService; private DebuggerTargetService targetService;
@ -431,37 +449,21 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
@Override @Override
public void closeAllTraces() { public void closeAllTraces() {
Swing.runIfSwingOrRunLater(() -> { checkCloseTraces(getOpenTraces());
for (Trace trace : getOpenTraces()) {
closeTrace(trace);
}
});
} }
@Override @Override
public void closeOtherTraces(Trace keep) { public void closeOtherTraces(Trace keep) {
Swing.runIfSwingOrRunLater(() -> { checkCloseTraces(getOpenTraces().stream().filter(t -> t != keep).toList());
for (Trace trace : getOpenTraces()) {
if (trace != keep) {
closeTrace(trace);
}
}
});
} }
@Override @Override
public void closeDeadTraces() { public void closeDeadTraces() {
Swing.runIfSwingOrRunLater(() -> { checkCloseTraces(targetService == null
if (targetService == null) { ? getOpenTraces()
return; : getOpenTraces().stream()
} .filter(t -> targetService.getTarget(t) == null)
for (Trace trace : getOpenTraces()) { .toList());
Target target = targetService.getTarget(trace);
if (target == null) {
closeTrace(trace);
}
}
});
} }
@AutoServiceConsumed @AutoServiceConsumed
@ -549,7 +551,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
} }
current = newCurrent; current = newCurrent;
if (newCurrent.getTrace() != null) { if (newCurrent.getTrace() != null) {
lastCoordsByTrace.put(newCurrent.getTrace(), newCurrent); lastCoordsByTrace.put(newCurrent.getTrace(), new LastCoords(newCurrent));
} }
} }
contextChanged(); contextChanged();
@ -610,11 +612,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
if (!listenersByTrace.containsKey(trace)) { if (!listenersByTrace.containsKey(trace)) {
return; return;
} }
DebuggerCoordinates cur = LastCoords cur = lastCoordsByTrace.getOrDefault(trace, LastCoords.NEVER);
lastCoordsByTrace.getOrDefault(trace, DebuggerCoordinates.NOWHERE);
DebuggerCoordinates adj = DebuggerCoordinates adj =
cur.platform(getPlatformForMapper(trace, cur.getObject(), mapper)); cur.coords.platform(getPlatformForMapper(trace, cur.coords.getObject(), mapper));
lastCoordsByTrace.put(trace, adj); lastCoordsByTrace.put(trace, cur.keepTime(adj));
if (trace == current.getTrace()) { if (trace == current.getTrace()) {
current = adj; current = adj;
fireLocationEvent(adj, ActivationCause.MAPPER_CHANGED); fireLocationEvent(adj, ActivationCause.MAPPER_CHANGED);
@ -670,7 +671,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
synchronized (listenersByTrace) { synchronized (listenersByTrace) {
// If known, fill in target ASAP, so it determines the time // If known, fill in target ASAP, so it determines the time
return fillInTarget(trace, return fillInTarget(trace,
lastCoordsByTrace.getOrDefault(trace, DebuggerCoordinates.NOWHERE)); lastCoordsByTrace.getOrDefault(trace, LastCoords.NEVER).coords);
} }
} }
@ -917,6 +918,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
} }
new TaskLauncher(new Task("Save New Trace", true, true, true) { new TaskLauncher(new Task("Save New Trace", true, true, true) {
@Override @Override
public void run(TaskMonitor monitor) throws CancelledException { public void run(TaskMonitor monitor) throws CancelledException {
String filename = trace.getName(); String filename = trace.getName();
@ -958,6 +960,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
future.completeExceptionally(e); future.completeExceptionally(e);
} }
} }
}); });
} }
return future; return future;
@ -994,20 +997,64 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
} }
} }
@Override protected void doCloseTraces(Collection<Trace> traces, Collection<Target> targets) {
public void closeTrace(Trace trace) { for (Trace t : traces) {
if (t.getConsumerList().contains(this)) {
firePluginEvent(new TraceClosedPluginEvent(getName(), t));
doTraceClosed(t);
}
}
TargetActionTask.executeTask(tool, new DisconnectTask(tool, targets));
}
protected static final String MSGPAT_TERMINATE = """
<html>
<body width="300px">
<p>This will terminate the following targets:</p>
<ul>
%s
</ul>
<p>Proceed?</p>
</body>
</html>
""";
protected static String formatTargets(Collection<Target> targets) {
return targets.stream()
.map(t -> "<li>%s</li>".formatted(HTMLUtilities.escapeHTML(t.describe())))
.sorted()
.collect(Collectors.joining("\n"));
}
protected void checkCloseTraces(Collection<Trace> traces) {
List<Target> live =
traces.stream()
.map(t -> targetService.getTarget(t))
.filter(t -> t != null)
.toList();
/** /**
* A provider may be reading the trace, likely via the Swing thread, so schedule this on the * A provider may be reading a trace, likely via the Swing thread, so schedule this on the
* same thread to avoid a ClosedException. * same thread to avoid a ClosedException.
*/ */
Swing.runIfSwingOrRunLater(() -> { Swing.runIfSwingOrRunLater(() -> {
if (trace.getConsumerList().contains(this)) { if (live.isEmpty()) {
firePluginEvent(new TraceClosedPluginEvent(getName(), trace)); doCloseTraces(traces, live);
doTraceClosed(trace); return;
}
String msg = MSGPAT_TERMINATE.formatted(formatTargets(live));
int response = OptionDialog.showYesNoDialog(null, "Terminate", msg);
switch (response) {
case OptionDialog.YES_OPTION -> doCloseTraces(traces, live);
case OptionDialog.NO_OPTION -> List.of();
} }
}); });
} }
@Override
public void closeTrace(Trace trace) {
checkCloseTraces(List.of(trace));
}
@Override @Override
protected void dispose() { protected void dispose() {
super.dispose(); super.dispose();
@ -1034,6 +1081,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
return elem.toString(); return elem.toString();
} }
/**
* Gets the most recent coordinates among those traces still open
*/
protected DebuggerCoordinates getMostRecentCoordinates() {
synchronized (listenersByTrace) {
return lastCoordsByTrace.values()
.stream()
.sorted(Comparator.comparing(l -> -l.time))
.findFirst()
.map(l -> l.coords)
.orElse(DebuggerCoordinates.NOWHERE);
}
}
@Override @Override
public CompletableFuture<Void> activateAndNotify(DebuggerCoordinates coordinates, public CompletableFuture<Void> activateAndNotify(DebuggerCoordinates coordinates,
ActivationCause cause) { ActivationCause cause) {
@ -1048,6 +1109,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
throw new IllegalStateException( throw new IllegalStateException(
"Trace must be opened before activated: " + newTrace); "Trace must be opened before activated: " + newTrace);
} }
if (newTrace == null && ensureActiveTrace) {
coordinates = getMostRecentCoordinates(); // Might still be NOWHERE
}
} }
if (cause == ActivationCause.FOLLOW_PRESENT) { if (cause == ActivationCause.FOLLOW_PRESENT) {
@ -1228,8 +1292,13 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
traces = tracesView.stream().filter(t -> { traces = tracesView.stream().filter(t -> {
ProjectLocator loc = t.getDomainFile().getProjectLocator(); ProjectLocator loc = t.getDomainFile().getProjectLocator();
return loc != null && !loc.isTransient(); return loc != null && !loc.isTransient();
}).collect(Collectors.toList()); }).sorted(Comparator.comparingLong(t -> {
coordsByTrace = Map.copyOf(lastCoordsByTrace); LastCoords last = lastCoordsByTrace.get(t);
return last == null ? -1 : last.time;
})).toList();
coordsByTrace = lastCoordsByTrace.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().coords));
} }
saveState.putInt(KEY_TRACE_COUNT, traces.size()); saveState.putInt(KEY_TRACE_COUNT, traces.size());
@ -1246,6 +1315,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
@Override @Override
public void readDataState(SaveState saveState) { public void readDataState(SaveState saveState) {
synchronized (listenersByTrace) { synchronized (listenersByTrace) {
long baseTime = System.currentTimeMillis();
int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0); int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0);
for (int index = 0; index < traceCount; index++) { for (int index = 0; index < traceCount; index++) {
String stateName = PREFIX_OPEN_TRACE + index; String stateName = PREFIX_OPEN_TRACE + index;
@ -1253,7 +1323,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin
DebuggerCoordinates coords = DebuggerCoordinates coords =
DebuggerCoordinates.readDataState(tool, saveState, stateName); DebuggerCoordinates.readDataState(tool, saveState, stateName);
if (coords.getTrace() != null) { if (coords.getTrace() != null) {
lastCoordsByTrace.put(coords.getTrace(), coords); lastCoordsByTrace.put(coords.getTrace(),
new LastCoords(baseTime + index, coords));
} }
} }
} }

View file

@ -0,0 +1,25 @@
/* ###
* 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.listing;
import ghidra.app.plugin.core.debug.gui.trace.DebuggerTraceTabPanel;
public class DebuggerListingProviderTestAccess {
public static DebuggerTraceTabPanel getTraceTabs(DebuggerListingProvider provider) {
return provider.traceTabs;
}
}

View file

@ -40,6 +40,7 @@ import ghidra.app.plugin.core.debug.gui.modules.DebuggerModuleMapProposalDialog.
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns; import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.services.DebuggerListingService; import ghidra.app.services.DebuggerListingService;
import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry; import ghidra.debug.api.modules.ModuleMapProposal.ModuleMapEntry;
import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry; import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry;
@ -302,6 +303,7 @@ public class DebuggerModulesProviderLegacyTest extends AbstractGhidraHeadedDebug
@Test @Test
public void testActivatingNoTraceEmptiesProvider() throws Exception { public void testActivatingNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace(); createAndOpenTrace();
addModules(); addModules();

View file

@ -42,6 +42,7 @@ import ghidra.app.plugin.core.debug.gui.modules.DebuggerModuleMapProposalDialog.
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapModulesAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction; import ghidra.app.plugin.core.debug.gui.modules.DebuggerModulesProvider.MapSectionsAction;
import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns; import ghidra.app.plugin.core.debug.gui.modules.DebuggerSectionMapProposalDialog.SectionMapTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.services.DebuggerListingService; import ghidra.app.services.DebuggerListingService;
import ghidra.dbg.target.*; import ghidra.dbg.target.*;
import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.SchemaContext;
@ -434,6 +435,7 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerTes
@Test @Test
public void testActivatingNoTraceEmptiesProvider() throws Exception { public void testActivatingNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace(); createAndOpenTrace();
addModules(); addModules();

View file

@ -17,9 +17,7 @@ package ghidra.app.plugin.core.debug.gui.thread;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.awt.event.MouseEvent;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -29,8 +27,8 @@ import db.Transaction;
import generic.test.category.NightlyCategory; import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.thread.DebuggerLegacyThreadsPanel.ThreadTableColumns; import ghidra.app.plugin.core.debug.gui.thread.DebuggerLegacyThreadsPanel.ThreadTableColumns;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.trace.model.Lifespan; import ghidra.trace.model.Lifespan;
import ghidra.trace.model.Trace;
import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.thread.TraceThread;
import ghidra.trace.model.thread.TraceThreadManager; import ghidra.trace.model.thread.TraceThreadManager;
import ghidra.trace.model.time.TraceTimeManager; import ghidra.trace.model.time.TraceTimeManager;
@ -60,32 +58,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
} }
} }
/**
* Check that there exist no tabs, and that the tab row is invisible
*/
protected void assertZeroTabs() {
assertEquals(0, threadsProvider.traceTabs.getList().getModel().getSize());
assertEquals("Tab row should not be visible", 0,
threadsProvider.traceTabs.getVisibleRect().height);
}
/**
* Check that exactly one tab exists, and that the tab row is visible
*/
protected void assertOneTabPopulated() {
assertEquals(1, threadsProvider.traceTabs.getList().getModel().getSize());
assertNotEquals("Tab row should be visible", 0,
threadsProvider.traceTabs.getVisibleRect().height);
}
protected void assertNoTabSelected() {
assertTabSelected(null);
}
protected void assertTabSelected(Trace trace) {
assertEquals(trace, threadsProvider.traceTabs.getSelectedItem());
}
protected void assertThreadsEmpty() { protected void assertThreadsEmpty() {
List<ThreadRow> threadsDisplayed = List<ThreadRow> threadsDisplayed =
threadsProvider.legacyPanel.threadTableModel.getModelData(); threadsProvider.legacyPanel.threadTableModel.getModelData();
@ -122,7 +94,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
} }
protected void assertProviderEmpty() { protected void assertProviderEmpty() {
assertZeroTabs();
assertThreadsEmpty(); assertThreadsEmpty();
} }
@ -132,45 +103,9 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertProviderEmpty(); assertProviderEmpty();
} }
@Test
public void testOpenTracePopupatesTab() throws Exception {
createAndOpenTrace();
waitForSwing();
assertOneTabPopulated();
assertNoTabSelected();
assertThreadsEmpty();
}
@Test
public void testActivateTraceSelectsTab() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertOneTabPopulated();
assertTabSelected(tb.trace);
traceManager.activateTrace(null);
waitForSwing();
assertOneTabPopulated();
assertNoTabSelected();
}
@Test
public void testSelectTabActivatesTrace() throws Exception {
createAndOpenTrace();
waitForSwing();
threadsProvider.traceTabs.setSelectedItem(tb.trace);
waitForSwing();
assertEquals(tb.trace, traceManager.getCurrentTrace());
assertEquals(tb.trace, threadsProvider.current.getTrace());
}
@Test @Test
public void testActivateNoTraceEmptiesProvider() throws Exception { public void testActivateNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
@ -184,22 +119,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertThreadsEmpty(); assertThreadsEmpty();
} }
@Test
public void testCurrentTraceClosedUpdatesTabs() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForSwing();
assertOneTabPopulated();
assertTabSelected(tb.trace);
traceManager.closeTrace(tb.trace);
waitForSwing();
assertZeroTabs();
assertNoTabSelected();
}
@Test @Test
public void testCurrentTraceClosedEmptiesProvider() throws Exception { public void testCurrentTraceClosedEmptiesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
@ -215,25 +134,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug
assertThreadsEmpty(); assertThreadsEmpty();
} }
@Test
public void testCloseTraceTabPopupMenuItem() throws Exception {
createAndOpenTrace();
waitForSwing();
assertOneTabPopulated(); // pre-check
clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3);
waitForSwing();
Set<String> expected = Set.of("Close " + tb.trace.getName());
assertMenu(expected, expected);
clickSubMenuItemByText("Close " + tb.trace.getName());
waitForSwing();
waitForPass(() -> {
assertEquals(Set.of(), traceManager.getOpenTraces());
});
}
@Test @Test
public void testActivateThenAddThreadsPopulatesProvider() throws Exception { public void testActivateThenAddThreadsPopulatesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();

View file

@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.gui.thread;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.awt.event.MouseEvent;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import org.junit.*; import org.junit.*;
import org.junit.experimental.categories.Category; import org.junit.experimental.categories.Category;
@ -31,6 +29,7 @@ import generic.test.category.NightlyCategory;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*; import ghidra.app.plugin.core.debug.gui.model.ObjectTableModel.*;
import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper; import ghidra.app.plugin.core.debug.gui.model.QueryPanelTestHelper;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.dbg.target.TargetExecutionStateful; import ghidra.dbg.target.TargetExecutionStateful;
import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState; import ghidra.dbg.target.TargetExecutionStateful.TargetExecutionState;
import ghidra.dbg.target.schema.SchemaContext; import ghidra.dbg.target.schema.SchemaContext;
@ -119,32 +118,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
} }
} }
/**
* Check that there exist no tabs, and that the tab row is invisible
*/
protected void assertZeroTabs() {
assertEquals(0, provider.traceTabs.getList().getModel().getSize());
assertEquals("Tab row should not be visible", 0,
provider.traceTabs.getVisibleRect().height);
}
/**
* Check that exactly one tab exists, and that the tab row is visible
*/
protected void assertOneTabPopulated() {
assertEquals(1, provider.traceTabs.getList().getModel().getSize());
assertNotEquals("Tab row should be visible", 0,
provider.traceTabs.getVisibleRect().height);
}
protected void assertNoTabSelected() {
assertTabSelected(null);
}
protected void assertTabSelected(Trace trace) {
assertEquals(trace, provider.traceTabs.getSelectedItem());
}
protected void assertThreadsTableSize(int size) { protected void assertThreadsTableSize(int size) {
assertEquals(size, provider.panel.getAllItems().size()); assertEquals(size, provider.panel.getAllItems().size());
} }
@ -195,7 +168,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
} }
protected void assertProviderEmpty() { protected void assertProviderEmpty() {
assertZeroTabs();
assertThreadsEmpty(); assertThreadsEmpty();
} }
@ -218,53 +190,9 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertProviderEmpty()); waitForPass(() -> assertProviderEmpty());
} }
@Test
public void testOpenTracePopupatesTab() throws Exception {
createAndOpenTrace();
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertNoTabSelected();
assertThreadsEmpty();
});
}
@Test
public void testActivateTraceSelectsTab() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.activateTrace(null);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertNoTabSelected();
});
}
@Test
public void testSelectTabActivatesTrace() throws Exception {
createAndOpenTrace();
waitForTasks();
provider.traceTabs.setSelectedItem(tb.trace);
waitForTasks();
waitForPass(() -> {
assertEquals(tb.trace, traceManager.getCurrentTrace());
assertEquals(tb.trace, provider.current.getTrace());
});
}
@Test @Test
public void testActivateNoTraceEmptiesProvider() throws Exception { public void testActivateNoTraceEmptiesProvider() throws Exception {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace(); createAndOpenTrace();
addThreads(); addThreads();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
@ -278,26 +206,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertThreadsEmpty()); waitForPass(() -> assertThreadsEmpty());
} }
@Test
public void testCurrentTraceClosedUpdatesTabs() throws Exception {
createAndOpenTrace();
traceManager.activateTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertOneTabPopulated();
assertTabSelected(tb.trace);
});
traceManager.closeTrace(tb.trace);
waitForTasks();
waitForPass(() -> {
assertZeroTabs();
assertNoTabSelected();
});
}
@Test @Test
public void testCurrentTraceClosedEmptiesProvider() throws Exception { public void testCurrentTraceClosedEmptiesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();
@ -313,25 +221,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes
waitForPass(() -> assertThreadsEmpty()); waitForPass(() -> assertThreadsEmpty());
} }
@Test
public void testCloseTraceTabPopupMenuItem() throws Exception {
createAndOpenTrace();
waitForTasks();
waitForPass(() -> assertOneTabPopulated());
clickListItem(provider.traceTabs.getList(), 0, MouseEvent.BUTTON3);
waitForTasks();
Set<String> expected = Set.of("Close " + tb.trace.getName());
assertMenu(expected, expected);
clickSubMenuItemByText("Close " + tb.trace.getName());
waitForTasks();
waitForPass(() -> {
assertEquals(Set.of(), traceManager.getOpenTraces());
});
}
@Test @Test
public void testActivateThenAddThreadsPopulatesProvider() throws Exception { public void testActivateThenAddThreadsPopulatesProvider() throws Exception {
createAndOpenTrace(); createAndOpenTrace();

View file

@ -0,0 +1,111 @@
/* ###
* 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.trace;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest;
import ghidra.app.plugin.core.debug.gui.listing.*;
import ghidra.trace.database.ToyDBTraceBuilder;
public class DebuggerTraceTabPanelTest extends AbstractGhidraHeadedDebuggerTest {
private DebuggerTraceTabPanel traceTabs;
@Before
public void setUpTabTest() throws Throwable {
addPlugin(tool, DebuggerListingPlugin.class);
DebuggerListingProvider listingProvider =
waitForComponentProvider(DebuggerListingProvider.class);
traceTabs = DebuggerListingProviderTestAccess.getTraceTabs(listingProvider);
}
@Test
public void testEmpty() {
assertEquals(List.of(), traceTabs.getTabValues());
}
@Test
public void testOpenTraceAddsTab() throws Throwable {
createAndOpenTrace();
waitForSwing();
assertEquals(List.of(tb.trace), traceTabs.getTabValues());
}
@Test
public void testActivateTraceSelectsTab() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
traceManager.activateTrace(tb1.trace);
waitForSwing();
assertEquals(tb1.trace, traceTabs.getSelectedTabValue());
traceManager.activateTrace(tb2.trace);
waitForSwing();
assertEquals(tb2.trace, traceTabs.getSelectedTabValue());
}
}
@Test
public void testSelectTabActivatesTrace() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
traceTabs.selectTab(tb1.trace);
waitForSwing();
assertEquals(tb1.trace, traceManager.getCurrentTrace());
traceTabs.selectTab(tb2.trace);
waitForSwing();
assertEquals(tb2.trace, traceManager.getCurrentTrace());
}
}
@Test
public void testCloseTraceRemovesTab() throws Throwable {
try (
ToyDBTraceBuilder tb1 = new ToyDBTraceBuilder(getName() + "_1", LANGID_TOYBE64);
ToyDBTraceBuilder tb2 = new ToyDBTraceBuilder(getName() + "_2", LANGID_TOYBE64)) {
traceManager.openTrace(tb1.trace);
traceManager.openTrace(tb2.trace);
waitForSwing();
assertEquals(List.of(tb1.trace, tb2.trace), traceTabs.getTabValues());
traceManager.closeTrace(tb1.trace);
waitForSwing();
assertEquals(List.of(tb2.trace), traceTabs.getTabValues());
traceManager.closeTrace(tb2.trace);
waitForSwing();
assertEquals(List.of(), traceTabs.getTabValues());
}
}
}

View file

@ -46,6 +46,11 @@ class MockTarget implements Target {
this.trace = trace; this.trace = trace;
} }
@Override
public String describe() {
return "Mock Target";
}
@Override @Override
public boolean isValid() { public boolean isValid() {
return true; return true;

View file

@ -141,6 +141,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(thread, traceManager.getCurrentThread()); assertEquals(thread, traceManager.getCurrentThread());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForSwing();
@ -177,6 +178,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(5, traceManager.getCurrentSnap()); assertEquals(5, traceManager.getCurrentSnap());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForSwing();
@ -207,6 +209,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(5, traceManager.getCurrentFrame()); assertEquals(5, traceManager.getCurrentFrame());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForSwing();
@ -251,6 +254,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge
assertEquals(objThread0, traceManager.getCurrentObject()); assertEquals(objThread0, traceManager.getCurrentObject());
assertEquals(thread, traceManager.getCurrentThread()); assertEquals(thread, traceManager.getCurrentThread());
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
traceManager.activateTrace(null); traceManager.activateTrace(null);
waitForSwing(); waitForSwing();

View file

@ -0,0 +1,24 @@
/* ###
* 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.service.tracemgr;
import ghidra.app.services.DebuggerTraceManagerService;
public class DebuggerTraceManagerServiceTestAccess {
public static void setEnsureActiveTrace(DebuggerTraceManagerService traceManager, boolean b) {
((DebuggerTraceManagerServicePlugin) traceManager).ensureActiveTrace = b;
}
}

View file

@ -1,202 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets;
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.ChangeEvent;
// TODO: I'd like "close" buttons on the tabs, optionally.
// For now, client must use a popup menu.
@SuppressWarnings("serial")
public class HorizontalTabPanel<T> extends JPanel {
public static Color copyColor(Color c) {
return new Color(c.getRGB());
}
public static class TabListCellRenderer<T> implements ListCellRenderer<T> {
protected final Box hBox = Box.createHorizontalBox();
protected final JLabel label = new JLabel();
{
hBox.setBorder(new BevelBorder(BevelBorder.RAISED));
hBox.setOpaque(true);
hBox.add(label);
}
protected String getText(T value) {
return value.toString();
}
protected Icon getIcon(T value) {
return null;
}
@Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus) {
label.setText(getText(value));
label.setIcon(getIcon(value));
if (isSelected) {
//label.setForeground(list.getSelectionForeground());
label.setForeground(copyColor(list.getSelectionForeground()));
hBox.setBackground(list.getSelectionBackground());
}
else {
label.setForeground(list.getForeground());
hBox.setBackground(list.getBackground());
}
hBox.validate();
return hBox;
}
}
protected final JList<T> list = new JList<>();
private final JScrollPane scroll = new JScrollPane(list);
private final JViewport viewport = scroll.getViewport();
private final DefaultListModel<T> model = new DefaultListModel<>();
private final JButton left = new JButton("<");
private final JButton right = new JButton(">");
{
list.setModel(model);
// TODO: Experiment with multiple traces in one timeline
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(1);
list.setCellRenderer(new TabListCellRenderer<>());
list.setOpaque(false);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setBorder(null);
viewport.addChangeListener(this::viewportChanged);
left.setBorder(null);
right.setBorder(null);
left.setContentAreaFilled(false);
right.setContentAreaFilled(false);
left.setOpaque(true);
right.setOpaque(true);
left.addActionListener(this::leftActivated);
right.addActionListener(this::rightActivated);
}
public HorizontalTabPanel() {
super();
setLayout(new BorderLayout());
list.setBackground(getBackground());
add(scroll, BorderLayout.CENTER);
add(left, BorderLayout.WEST);
add(right, BorderLayout.EAST);
}
private void viewportChanged(ChangeEvent e) {
Dimension paneSize = getSize();
Dimension listSize = list.getSize();
boolean buttonsVisible = paneSize.getWidth() < listSize.getWidth();
left.setVisible(buttonsVisible);
right.setVisible(buttonsVisible);
}
/**
* Find the first cell which is even partially visible
*
* @param reverse true to search from right to left
* @return the cell index
*/
private int findFirstVisible(boolean reverse) {
int n = model.getSize();
Rectangle vis = list.getVisibleRect();
for (int i = reverse ? n - 1 : 0; reverse ? i >= 0 : i < n; i += reverse ? -1 : 1) {
Rectangle b = list.getCellBounds(i, i);
if (vis.intersects(b)) {
return i;
}
}
return -1;
}
/**
* Find the first cell <em>after</em> a given start which is even partially occluded
*
* @param start the starting cell index
* @param reverse true to search from right to left
* @return the cell index
*/
private int findNextOccluded(int start, boolean reverse) {
if (start == -1) {
return -1;
}
int n = model.getSize();
Rectangle vis = list.getVisibleRect();
for (int i = reverse ? start - 1 : start + 1; reverse ? i >= 0 : i < n; i +=
reverse ? -1 : 1) {
Rectangle b = list.getCellBounds(i, i);
if (!vis.contains(b)) {
return i;
}
}
return -1;
}
private void leftActivated(ActionEvent e) {
list.ensureIndexIsVisible(findNextOccluded(findFirstVisible(true), true));
}
private void rightActivated(ActionEvent e) {
list.ensureIndexIsVisible(findNextOccluded(findFirstVisible(false), false));
}
public JList<T> getList() {
return list;
}
public void addItem(T item) {
model.addElement(item);
revalidate();
}
public void removeItem(T item) {
model.removeElement(item);
revalidate();
}
public T getSelectedItem() {
int index = list.getSelectedIndex();
return index < 0 ? null : list.getModel().getElementAt(index);
}
public void setSelectedItem(T item) {
// NOTE: For large lists, this could get slow
int index = model.indexOf(item);
if (index < 0) {
list.clearSelection();
}
else {
list.setSelectedIndex(index);
}
}
public T getItem(int index) {
return list.getModel().getElementAt(index);
}
}

View file

@ -222,51 +222,12 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti
} }
} }
String getStringUsedInList(Program program) { private String getToolTip(Program program) {
DomainFile df = program.getDomainFile(); return DomainObjectDisplayUtils.getToolTip(program);
String changeIndicator = program.isChanged() ? "*" : "";
String pathString = getShortPath(df);
if (!df.isInWritableProject()) {
return pathString + " [Read-Only]" + changeIndicator;
}
return pathString + changeIndicator;
} }
private String getShortPath(DomainFile df) { private String getTabName(Program program) {
String pathString = df.toString(); return DomainObjectDisplayUtils.getTabText(program);
int length = pathString.length();
if (length < 100) {
return pathString;
}
String[] pathParts = pathString.split("/");
if (pathParts.length == 2) { // at least 2 for project name and filename
return pathString;
}
String projectName = df.getProjectLocator().getName();
int parentFolderIndex = pathParts.length - 2;
String parentName = pathParts[parentFolderIndex];
String filename = df.getName();
pathString = projectName + ":/.../" + parentName + "/" + filename;
return pathString;
}
String getToolTip(Program program) {
return getStringUsedInList(program);
}
String getName(Program program) {
DomainFile df = program.getDomainFile();
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += "@" + version;
}
tabName = tabName + " [Read-Only]";
}
return tabName;
} }
void keyTypedFromListWindow(KeyEvent e) { void keyTypedFromListWindow(KeyEvent e) {
@ -334,22 +295,6 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti
return EMPTY8_ICON; return EMPTY8_ICON;
} }
private String getTabName(Program program) {
DomainFile df = program.getDomainFile();
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += "@" + version;
}
tabName = tabName + " [Read-Only]";
}
if (program.isChanged()) {
tabName = "*" + tabName;
}
return tabName;
}
boolean removeProgram(Program program) { boolean removeProgram(Program program) {
return progService.closeProgram(program, false); return progService.closeProgram(program, false);
} }

View file

@ -0,0 +1,79 @@
/* ###
* 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.framework.model;
import ghidra.framework.store.FileSystem;
public class DomainObjectDisplayUtils {
private static final String VERSION_SEP = "@";
private static final String CHANGE_INDICATOR = "*";
private static final String READ_ONLY = " [Read-Only]";
private static final String PROJECT_SEP_ELLIPSES =
":" + FileSystem.SEPARATOR + "..." + FileSystem.SEPARATOR;
private DomainObjectDisplayUtils() {
}
public static String getShortPath(DomainFile df) {
String pathString = df.toString();
int length = pathString.length();
if (length < 100) {
return pathString;
}
String[] pathParts = pathString.split(FileSystem.SEPARATOR);
if (pathParts.length == 2) { // at least 2 for project name and filename
return pathString;
}
String projectName = df.getProjectLocator().getName();
int parentFolderIndex = pathParts.length - 2;
String parentName = pathParts[parentFolderIndex];
String filename = df.getName();
pathString =
projectName + PROJECT_SEP_ELLIPSES + parentName + FileSystem.SEPARATOR + filename;
return pathString;
}
public static String getToolTip(DomainObject object) {
DomainFile df = object.getDomainFile();
String changeIndicator = object.isChanged() ? CHANGE_INDICATOR : "";
String pathString = getShortPath(df);
if (!df.isInWritableProject()) {
return pathString + READ_ONLY + changeIndicator;
}
return pathString + changeIndicator;
}
public static String getTabText(DomainFile df) {
String tabName = df.getName();
if (df.isReadOnly()) {
int version = df.getVersion();
if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
tabName += VERSION_SEP + version;
}
tabName = tabName + READ_ONLY;
}
return tabName;
}
public static String getTabText(DomainObject object) {
if (object.isChanged()) {
return CHANGE_INDICATOR + getTabText(object.getDomainFile());
}
return getTabText(object.getDomainFile());
}
}

View file

@ -24,6 +24,7 @@ import org.junit.Test;
import db.Transaction; import db.Transaction;
import generic.Unique; import generic.Unique;
import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess;
import ghidra.app.script.GhidraState; import ghidra.app.script.GhidraState;
import ghidra.app.services.DebuggerTraceManagerService; import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.debug.api.breakpoint.LogicalBreakpoint; import ghidra.debug.api.breakpoint.LogicalBreakpoint;
@ -172,6 +173,7 @@ public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest<FlatDeb
@Test @Test
public void testActivateTraceNull() throws Throwable { public void testActivateTraceNull() throws Throwable {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
createAndOpenTrace(); createAndOpenTrace();
traceManager.activateTrace(tb.trace); traceManager.activateTrace(tb.trace);
waitForSwing(); waitForSwing();
@ -217,6 +219,7 @@ public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest<FlatDeb
@Test @Test
public void testActivateThreadNull() throws Throwable { public void testActivateThreadNull() throws Throwable {
DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false);
api.activateThread(null); api.activateThread(null);
assertEquals(null, traceManager.getCurrentThread()); assertEquals(null, traceManager.getCurrentThread());