diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java index 041c3aecd3..e24b9be390 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/target/Target.java @@ -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 * diff --git a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java index 76b2e8bef2..c65e205133 100644 --- a/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java +++ b/Ghidra/Debug/Debugger-api/src/main/java/ghidra/debug/api/tracemgr/DebuggerCoordinates.java @@ -45,6 +45,16 @@ import ghidra.util.NotOwnerException; public class DebuggerCoordinates { + /** + * Coordinates that indicate no trace is active in the Debugger UI. + * + *

+ * 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 = new DebuggerCoordinates(null, null, null, null, null, null, null, null); diff --git a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java index cf79ddc269..6efb3a138e 100644 --- a/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java +++ b/Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java @@ -81,6 +81,12 @@ public class TraceRmiTarget extends AbstractTarget { this.supportedBreakpointKinds = computeSupportedBreakpointKinds(); } + @Override + public String describe() { + return "%s in %s at %s (rmi)".formatted(getTrace().getDomainFile().getName(), + connection.getDescription(), connection.getRemoteAddress()); + } + @Override public boolean isValid() { return !connection.isClosed() && connection.isTarget(trace); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java index 855d6be8c2..2bf97cffae 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/DebuggerResources.java @@ -1614,7 +1614,7 @@ public interface DebuggerResources { } interface CloseAllTracesAction extends CloseTraceAction { - String NAME = NAME_PREFIX + " All Traces"; + String NAME = NAME_PREFIX + "All Traces"; String DESCRIPTION = "Close all traces"; String HELP_ANCHOR = "close_all_traces"; @@ -1641,7 +1641,7 @@ public interface DebuggerResources { } 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 HELP_ANCHOR = "close_other_traces"; @@ -1668,7 +1668,7 @@ public interface DebuggerResources { } 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 HELP_ANCHOR = "close_dead_traces"; diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java index 216097b4a7..b4ab401c7b 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DebuggerControlPlugin.java @@ -55,9 +55,6 @@ import ghidra.trace.model.time.schedule.TraceSchedule; import ghidra.trace.util.TraceEvents; import ghidra.util.Msg; import ghidra.util.Swing; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.Task; -import ghidra.util.task.TaskMonitor; @PluginInfo( shortDescription = "Debugger global controls", @@ -357,18 +354,7 @@ public class DebuggerControlPlugin extends AbstractDebuggerPlugin if (target == null) { return; } - TargetActionTask.executeTask(tool, new Task("Disconnect", false, false, false) { - @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); - } - } - }); + TargetActionTask.executeTask(tool, new DisconnectTask(tool, List.of(target))); } private boolean haveEmuAndTrace() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DisconnectTask.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DisconnectTask.java new file mode 100644 index 0000000000..638900a138 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/control/DisconnectTask.java @@ -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 targets; + + public DisconnectTask(PluginTool tool, Collection 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); + } + } + } +} diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java index 8233abb76d..057d532cb3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProvider.java @@ -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.action.*; 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.ProgramURLUtils; import ghidra.app.plugin.core.marker.MarkerMarginProvider; @@ -342,6 +344,7 @@ public class DebuggerListingProvider extends CodeViewerProvider { protected final ListenerSet trackingSpecChangeListeners = new ListenerSet<>(LocationTrackingSpecChangeListener.class, true); + protected final DebuggerTraceTabPanel traceTabs; protected final DebuggerLocationLabel locationLabel = new DebuggerLocationLabel(); protected final JLabel trackingLabel = new JLabel(); @@ -372,6 +375,8 @@ public class DebuggerListingProvider extends CodeViewerProvider { this.plugin = plugin; this.isMainListing = isConnected; + // TODO: An icon to distinguish dynamic from static + syncTrait = new ForListingSyncTrait(); goToTrait = new ForListingGoToTrait(); trackingTrait = new ForListingTrackingTrait(); @@ -394,13 +399,21 @@ public class DebuggerListingProvider extends CodeViewerProvider { readsMemTrait.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()); JPanel northPanel = new JPanel(new BorderLayout()); northPanel.add(locationLabel); northPanel.add(trackingLabel, BorderLayout.EAST); + if (traceTabs != null) { + northPanel.add(traceTabs, BorderLayout.NORTH); + } this.setNorthComponent(northPanel); if (isConnected) { setTitle(DebuggerResources.TITLE_PROVIDER_LISTING); @@ -929,6 +942,12 @@ public class DebuggerListingProvider extends CodeViewerProvider { @Override public ActionContext getActionContext(MouseEvent event) { + if (traceTabs != null) { + DebuggerTraceFileActionContext traceCtx = traceTabs.getActionContext(event); + if (traceCtx != null) { + return traceCtx; + } + } if (event == null || event.getSource() != locationLabel) { return super.getActionContext(event); } @@ -1036,7 +1055,6 @@ public class DebuggerListingProvider extends CodeViewerProvider { .collect(Collectors.toSet()); // Attempt to open probable matches. All others, list to import - // TODO: What if sections are not presented? for (TraceModule mod : modules) { DomainFile match = mappingService.findBestModuleProgram(space, mod); if (match == null) { @@ -1064,8 +1082,9 @@ public class DebuggerListingProvider extends CodeViewerProvider { new DebuggerMissingModuleActionContext(mod)); } /** - * Once the programs are opened, including those which are successfully imported, the mapper - * bot should take over, eventually invoking callbacks to our mapping change listener. + * Once the programs are opened, including those which are successfully imported, the + * automatic mapper should take effect, eventually invoking callbacks to our mapping change + * listener. */ } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java index c398fa740f..628fb4923a 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsPanel.java @@ -77,6 +77,14 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel { public ThreadPcColumn() { super(Address.class); @@ -85,7 +93,8 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel getProperty(ValueRow row) { TraceObject obj = row.getValue().getChild(); - DebuggerCoordinates coords = provider.current.object(obj); + + DebuggerCoordinates coords = coordsForObject(obj); return new ValueAddressProperty(row) { @Override public Address getValue() { @@ -111,7 +120,7 @@ public class DebuggerThreadsPanel extends AbstractObjectsTableBasedPanel getProperty(ValueRow row) { TraceObject obj = row.getValue().getChild(); - DebuggerCoordinates coords = provider.current.object(obj); + DebuggerCoordinates coords = coordsForObject(obj); return new ValueAddressProperty(row) { @Override public Address getValue() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java index 216f208c81..d83043533c 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProvider.java @@ -28,7 +28,8 @@ import docking.WindowPosition; import docking.action.DockingActionIf; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; 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.framework.model.DomainObjectChangeRecord; import ghidra.framework.model.DomainObjectEvent; @@ -97,8 +98,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { @AutoServiceConsumed DebuggerTargetService targetService; - // @AutoServiceConsumed // via method - private DebuggerTraceManagerService traceManager; @SuppressWarnings("unused") private final AutoService.Wiring autoServiceWiring; @@ -106,7 +105,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { private JPanel mainPanel; - DebuggerTraceTabPanel traceTabs; JPopupMenu traceTabPopupMenu; DebuggerThreadsPanel panel; DebuggerLegacyThreadsPanel legacyPanel; @@ -133,12 +131,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { setVisible(true); } - @AutoServiceConsumed - public void setTraceManager(DebuggerTraceManagerService traceManager) { - this.traceManager = traceManager; - contextChanged(); - } - @AutoServiceConsumed public void setEmulationService(DebuggerEmulationService emulationService) { contextChanged(); @@ -152,7 +144,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { current = coordinates; - traceTabs.coordinatesActivated(coordinates); if (Trace.isLegacy(coordinates.getTrace())) { panel.coordinatesActivated(DebuggerCoordinates.NOWHERE); legacyPanel.coordinatesActivated(coordinates); @@ -191,10 +182,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { myActionContext = legacyPanel.getActionContext(); } - void traceTabsContextChanged() { - myActionContext = traceTabs.getActionContext(); - } - @Override public ActionContext getActionContext(MouseEvent event) { if (myActionContext == null) { @@ -211,10 +198,6 @@ public class DebuggerThreadsProvider extends ComponentProviderAdapter { panel = new DebuggerThreadsPanel(this); legacyPanel = new DebuggerLegacyThreadsPanel(plugin, this); mainPanel.add(panel); - - traceTabs = new DebuggerTraceTabPanel(this); - - mainPanel.add(traceTabs, BorderLayout.NORTH); } protected void createActions() { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java similarity index 57% rename from Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java rename to Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java index ce3977fd95..46f66b1f3f 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerTraceTabPanel.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanel.java @@ -13,27 +13,24 @@ * See the License for the specific language governing permissions and * 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.*; -import java.util.Objects; +import java.awt.event.MouseEvent; import javax.swing.Icon; -import javax.swing.JList; -import javax.swing.event.ListSelectionEvent; import docking.action.DockingAction; -import docking.widgets.HorizontalTabPanel; -import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent; -import ghidra.app.plugin.core.debug.event.TraceOpenedPluginEvent; +import docking.widgets.tab.GTabPanel; +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.thread.DebuggerTraceFileActionContext; +import ghidra.app.plugin.core.progmgr.MultiTabPlugin; import ghidra.app.services.DebuggerTargetService; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.debug.api.target.Target; import ghidra.debug.api.target.TargetPublicationListener; -import ghidra.debug.api.tracemgr.DebuggerCoordinates; +import ghidra.framework.model.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.annotation.AutoServiceConsumed; import ghidra.framework.plugintool.util.PluginEventListener; @@ -42,26 +39,21 @@ import ghidra.util.Swing; import utilities.util.SuppressableCallback; import utilities.util.SuppressableCallback.Suppression; -public class DebuggerTraceTabPanel extends HorizontalTabPanel - implements PluginEventListener { +public class DebuggerTraceTabPanel extends GTabPanel + implements PluginEventListener, DomainObjectListener { private class TargetsChangeListener implements TargetPublicationListener { @Override public void targetPublished(Target target) { - Swing.runIfSwingOrRunLater(() -> repaint()); - + Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace())); } @Override public void targetWithdrawn(Target target) { - Swing.runIfSwingOrRunLater(() -> repaint()); - + Swing.runIfSwingOrRunLater(() -> refreshTab(target.getTrace())); } } - private final DebuggerThreadsPlugin plugin; - private final DebuggerThreadsProvider provider; - // @AutoServiceConsumed by method DebuggerTargetService targetService; @AutoServiceConsumed @@ -78,101 +70,80 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel private final SuppressableCallback cbCoordinateActivation = new SuppressableCallback<>(); - private DebuggerTraceFileActionContext myActionContext; - - public DebuggerTraceTabPanel(DebuggerThreadsProvider provider) { - this.plugin = provider.plugin; - this.provider = provider; - + public DebuggerTraceTabPanel(Plugin plugin) { + super("Trace"); this.autoServiceWiring = AutoService.wireServicesConsumed(plugin, this); PluginTool tool = plugin.getTool(); tool.addEventListener(TraceOpenedPluginEvent.class, this); + tool.addEventListener(TraceActivatedPluginEvent.class, this); tool.addEventListener(TraceClosedPluginEvent.class, this); - list.setCellRenderer(new TabListCellRenderer<>() { - protected String getText(Trace value) { - return value.getName(); - } - - protected Icon getIcon(Trace value) { - 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); - } - }); + setNameFunction(this::getNameForTrace); + setIconFunction(this::getIconForTrace); + setToolTipFunction(this::getTipForTrace); + setSelectedTabConsumer(this::traceTabSelected); + // Cannot use method ref here, since traceManager is still null + setCloseTabConsumer(t -> traceManager.closeTrace(t)); actionCloseTrace = CloseTraceAction.builderPopup(plugin) .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())) - .buildAndInstallLocal(provider); + .buildAndInstall(tool); actionCloseAllTraces = CloseAllTracesAction.builderPopup(plugin) .withContext(DebuggerTraceFileActionContext.class) .popupWhen(c -> !traceManager.getOpenTraces().isEmpty()) .onAction(c -> traceManager.closeAllTraces()) - .buildAndInstallLocal(provider); + .buildAndInstall(tool); actionCloseOtherTraces = CloseOtherTracesAction.builderPopup(plugin) .withContext(DebuggerTraceFileActionContext.class) .popupWhen(c -> traceManager.getOpenTraces().size() > 1 && c.getTrace() != null) .onAction(c -> traceManager.closeOtherTraces(c.getTrace())) - .buildAndInstallLocal(provider); + .buildAndInstall(tool); actionCloseDeadTraces = CloseDeadTracesAction.builderPopup(plugin) .withContext(DebuggerTraceFileActionContext.class) .popupWhen(c -> !traceManager.getOpenTraces().isEmpty() && targetService != null) .onAction(c -> traceManager.closeDeadTraces()) - .buildAndInstallLocal(provider); + .buildAndInstall(tool); } - private Trace computeClickedTraceTab(MouseEvent e) { - JList list = getList(); - int i = list.locationToIndex(e.getPoint()); - if (i < 0) { + private String getNameForTrace(Trace trace) { + return DomainObjectDisplayUtils.getTabText(trace); + } + + private Icon getIconForTrace(Trace trace) { + if (targetService == null) { return null; } - Rectangle cell = list.getCellBounds(i, i); - if (!cell.contains(e.getPoint())) { + Target target = targetService.getTarget(trace); + if (target == null || !target.isValid()) { return null; } - return getItem(i); + return DebuggerResources.ICON_RECORD; } - private Trace setTraceTabActionContext(MouseEvent e) { - Trace newTrace = e == null ? getSelectedItem() : computeClickedTraceTab(e); - actionCloseTrace.getPopupMenuData() - .setMenuItemName( - CloseTraceAction.NAME_PREFIX + (newTrace == null ? "..." : newTrace.getName())); - myActionContext = new DebuggerTraceFileActionContext(newTrace); - provider.traceTabsContextChanged(); - return newTrace; + private String getTipForTrace(Trace trace) { + return DomainObjectDisplayUtils.getToolTip(trace); } - public DebuggerTraceFileActionContext getActionContext() { - return myActionContext; - } - - public void coordinatesActivated(DebuggerCoordinates coordinates) { - try (Suppression supp = cbCoordinateActivation.suppress(null)) { - setSelectedItem(coordinates.getTrace()); + public DebuggerTraceFileActionContext getActionContext(MouseEvent e) { + if (e == null) { + return null; } + Trace trace = getValueFor(e); + if (trace == null) { + return null; + } + return new DebuggerTraceFileActionContext(trace); } @AutoServiceConsumed @@ -186,28 +157,46 @@ public class DebuggerTraceTabPanel extends HorizontalTabPanel } } + protected void add(Trace trace) { + addTab(trace); + trace.removeListener(this); + trace.addListener(this); + } + + protected void remove(Trace trace) { + trace.removeListener(this); + removeTab(trace); + } + @Override public void eventSent(PluginEvent event) { - if (Objects.equals(event.getSourceName(), plugin.getName())) { - return; - } if (event instanceof TraceOpenedPluginEvent evt) { 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) { + Trace trace = evt.getTrace(); try (Suppression supp = cbCoordinateActivation.suppress(null)) { - removeItem(evt.getTrace()); + remove(trace); } } } - private void traceTabSelected(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) { - return; + @Override + public void domainObjectChanged(DomainObjectChangedEvent ev) { + if (ev.getSource() instanceof Trace trace) { + refreshTab(trace); } - Trace newTrace = setTraceTabActionContext(null); + } + + private void traceTabSelected(Trace newTrace) { cbCoordinateActivation.invoke(() -> { traceManager.activateTrace(newTrace); }); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java index 2f94f2df87..6dd37d91b5 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/model/TraceRecorderTarget.java @@ -90,6 +90,12 @@ public class TraceRecorderTarget extends AbstractTarget { this.recorder = recorder; } + @Override + public String describe() { + return "%s in %s (recorder)".formatted(getTrace().getDomainFile().getName(), + recorder.getTarget().getModel().getBrief()); + } + @Override public boolean isValid() { return recorder.isRecording(); diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java index 20e8230328..5b0e3580d3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/modules/ProgramModuleIndexer.java @@ -410,7 +410,7 @@ public class ProgramModuleIndexer implements DomainFolderChangeListener { continue; } try (PeekOpenedDomainObject peek = new PeekOpenedDomainObject(df)) { - if (programs.contains(peek.object)) { + if (peek.object != null && programs.contains(peek.object)) { result.add(e); } } diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java index e1a3438da5..653d736710 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServicePlugin.java @@ -26,10 +26,13 @@ import java.util.stream.Stream; import docking.ActionContext; import docking.action.DockingAction; import docking.action.ToggleDockingAction; +import docking.widgets.OptionDialog; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.debug.DebuggerPluginPackage; import ghidra.app.plugin.core.debug.event.*; 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.DebuggerControlService.ControlModeChangeListener; import ghidra.async.*; @@ -135,8 +138,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin private void threadDeleted(TraceThread thread) { synchronized (listenersByTrace) { - DebuggerCoordinates last = lastCoordsByTrace.get(trace); - if (last != null && last.getThread() == thread) { + LastCoords last = lastCoordsByTrace.get(trace); + if (last != null && last.coords.getThread() == thread) { lastCoordsByTrace.remove(trace); } } @@ -261,7 +264,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } - protected final Map 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 lastCoordsByTrace = new WeakHashMap<>(); protected final Map listenersByTrace = new WeakHashMap<>(); protected final Set tracesView = Collections.unmodifiableSet(listenersByTrace.keySet()); @@ -275,6 +291,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin protected final AsyncReference saveTracesByDefault = new AsyncReference<>(true); @AutoConfigStateField(codec = BooleanAsyncConfigFieldCodec.class) protected final AsyncReference autoCloseOnTerminate = new AsyncReference<>(true); + // Do not save this one, it's for testing only + protected boolean ensureActiveTrace = true; // @AutoServiceConsumed via method private DebuggerTargetService targetService; @@ -431,37 +449,21 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void closeAllTraces() { - Swing.runIfSwingOrRunLater(() -> { - for (Trace trace : getOpenTraces()) { - closeTrace(trace); - } - }); + checkCloseTraces(getOpenTraces()); } @Override public void closeOtherTraces(Trace keep) { - Swing.runIfSwingOrRunLater(() -> { - for (Trace trace : getOpenTraces()) { - if (trace != keep) { - closeTrace(trace); - } - } - }); + checkCloseTraces(getOpenTraces().stream().filter(t -> t != keep).toList()); } @Override public void closeDeadTraces() { - Swing.runIfSwingOrRunLater(() -> { - if (targetService == null) { - return; - } - for (Trace trace : getOpenTraces()) { - Target target = targetService.getTarget(trace); - if (target == null) { - closeTrace(trace); - } - } - }); + checkCloseTraces(targetService == null + ? getOpenTraces() + : getOpenTraces().stream() + .filter(t -> targetService.getTarget(t) == null) + .toList()); } @AutoServiceConsumed @@ -549,7 +551,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } current = newCurrent; if (newCurrent.getTrace() != null) { - lastCoordsByTrace.put(newCurrent.getTrace(), newCurrent); + lastCoordsByTrace.put(newCurrent.getTrace(), new LastCoords(newCurrent)); } } contextChanged(); @@ -610,11 +612,10 @@ public class DebuggerTraceManagerServicePlugin extends Plugin if (!listenersByTrace.containsKey(trace)) { return; } - DebuggerCoordinates cur = - lastCoordsByTrace.getOrDefault(trace, DebuggerCoordinates.NOWHERE); + LastCoords cur = lastCoordsByTrace.getOrDefault(trace, LastCoords.NEVER); DebuggerCoordinates adj = - cur.platform(getPlatformForMapper(trace, cur.getObject(), mapper)); - lastCoordsByTrace.put(trace, adj); + cur.coords.platform(getPlatformForMapper(trace, cur.coords.getObject(), mapper)); + lastCoordsByTrace.put(trace, cur.keepTime(adj)); if (trace == current.getTrace()) { current = adj; fireLocationEvent(adj, ActivationCause.MAPPER_CHANGED); @@ -670,7 +671,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin synchronized (listenersByTrace) { // If known, fill in target ASAP, so it determines the time 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) { + @Override public void run(TaskMonitor monitor) throws CancelledException { String filename = trace.getName(); @@ -958,6 +960,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin future.completeExceptionally(e); } } + }); } return future; @@ -994,20 +997,64 @@ public class DebuggerTraceManagerServicePlugin extends Plugin } } - @Override - public void closeTrace(Trace trace) { + protected void doCloseTraces(Collection traces, Collection targets) { + 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 = """ + + +

This will terminate the following targets:

+ +

Proceed?

+ + + """; + + protected static String formatTargets(Collection targets) { + return targets.stream() + .map(t -> "
  • %s
  • ".formatted(HTMLUtilities.escapeHTML(t.describe()))) + .sorted() + .collect(Collectors.joining("\n")); + } + + protected void checkCloseTraces(Collection traces) { + List 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. */ Swing.runIfSwingOrRunLater(() -> { - if (trace.getConsumerList().contains(this)) { - firePluginEvent(new TraceClosedPluginEvent(getName(), trace)); - doTraceClosed(trace); + if (live.isEmpty()) { + doCloseTraces(traces, live); + 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 protected void dispose() { super.dispose(); @@ -1034,6 +1081,20 @@ public class DebuggerTraceManagerServicePlugin extends Plugin 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 public CompletableFuture activateAndNotify(DebuggerCoordinates coordinates, ActivationCause cause) { @@ -1048,6 +1109,9 @@ public class DebuggerTraceManagerServicePlugin extends Plugin throw new IllegalStateException( "Trace must be opened before activated: " + newTrace); } + if (newTrace == null && ensureActiveTrace) { + coordinates = getMostRecentCoordinates(); // Might still be NOWHERE + } } if (cause == ActivationCause.FOLLOW_PRESENT) { @@ -1228,8 +1292,13 @@ public class DebuggerTraceManagerServicePlugin extends Plugin traces = tracesView.stream().filter(t -> { ProjectLocator loc = t.getDomainFile().getProjectLocator(); return loc != null && !loc.isTransient(); - }).collect(Collectors.toList()); - coordsByTrace = Map.copyOf(lastCoordsByTrace); + }).sorted(Comparator.comparingLong(t -> { + 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()); @@ -1246,6 +1315,7 @@ public class DebuggerTraceManagerServicePlugin extends Plugin @Override public void readDataState(SaveState saveState) { synchronized (listenersByTrace) { + long baseTime = System.currentTimeMillis(); int traceCount = saveState.getInt(KEY_TRACE_COUNT, 0); for (int index = 0; index < traceCount; index++) { String stateName = PREFIX_OPEN_TRACE + index; @@ -1253,7 +1323,8 @@ public class DebuggerTraceManagerServicePlugin extends Plugin DebuggerCoordinates coords = DebuggerCoordinates.readDataState(tool, saveState, stateName); if (coords.getTrace() != null) { - lastCoordsByTrace.put(coords.getTrace(), coords); + lastCoordsByTrace.put(coords.getTrace(), + new LastCoords(baseTime + index, coords)); } } } diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTestAccess.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTestAccess.java new file mode 100644 index 0000000000..e144bd0d64 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/listing/DebuggerListingProviderTestAccess.java @@ -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; + } + +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderLegacyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderLegacyTest.java index f5ad46b758..a5b1a8b299 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderLegacyTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderLegacyTest.java @@ -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.MapSectionsAction; 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.debug.api.modules.ModuleMapProposal.ModuleMapEntry; import ghidra.debug.api.modules.SectionMapProposal.SectionMapEntry; @@ -302,6 +303,7 @@ public class DebuggerModulesProviderLegacyTest extends AbstractGhidraHeadedDebug @Test public void testActivatingNoTraceEmptiesProvider() throws Exception { + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); createAndOpenTrace(); addModules(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java index 7754328a67..923ab2b4ec 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/modules/DebuggerModulesProviderTest.java @@ -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.MapSectionsAction; 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.dbg.target.*; import ghidra.dbg.target.schema.SchemaContext; @@ -434,6 +435,7 @@ public class DebuggerModulesProviderTest extends AbstractGhidraHeadedDebuggerTes @Test public void testActivatingNoTraceEmptiesProvider() throws Exception { + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); createAndOpenTrace(); addModules(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java index c74611ac31..d8e77594fa 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderLegacyTest.java @@ -17,9 +17,7 @@ package ghidra.app.plugin.core.debug.gui.thread; import static org.junit.Assert.*; -import java.awt.event.MouseEvent; import java.util.List; -import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -29,8 +27,8 @@ import db.Transaction; import generic.test.category.NightlyCategory; import ghidra.app.plugin.core.debug.gui.AbstractGhidraHeadedDebuggerTest; 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.Trace; import ghidra.trace.model.thread.TraceThread; import ghidra.trace.model.thread.TraceThreadManager; 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() { List threadsDisplayed = threadsProvider.legacyPanel.threadTableModel.getModelData(); @@ -122,7 +94,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug } protected void assertProviderEmpty() { - assertZeroTabs(); assertThreadsEmpty(); } @@ -132,45 +103,9 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug 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 public void testActivateNoTraceEmptiesProvider() throws Exception { + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); @@ -184,22 +119,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug 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 public void testCurrentTraceClosedEmptiesProvider() throws Exception { createAndOpenTrace(); @@ -215,25 +134,6 @@ public class DebuggerThreadsProviderLegacyTest extends AbstractGhidraHeadedDebug assertThreadsEmpty(); } - @Test - public void testCloseTraceTabPopupMenuItem() throws Exception { - createAndOpenTrace(); - waitForSwing(); - - assertOneTabPopulated(); // pre-check - clickListItem(threadsProvider.traceTabs.getList(), 0, MouseEvent.BUTTON3); - waitForSwing(); - Set expected = Set.of("Close " + tb.trace.getName()); - assertMenu(expected, expected); - - clickSubMenuItemByText("Close " + tb.trace.getName()); - waitForSwing(); - - waitForPass(() -> { - assertEquals(Set.of(), traceManager.getOpenTraces()); - }); - } - @Test public void testActivateThenAddThreadsPopulatesProvider() throws Exception { createAndOpenTrace(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java index f655fcb87f..c02ef6692e 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/thread/DebuggerThreadsProviderTest.java @@ -17,10 +17,8 @@ package ghidra.app.plugin.core.debug.gui.thread; import static org.junit.Assert.*; -import java.awt.event.MouseEvent; import java.io.IOException; import java.util.Objects; -import java.util.Set; import org.junit.*; 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.model.ObjectTableModel.*; 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.TargetExecutionState; 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) { assertEquals(size, provider.panel.getAllItems().size()); } @@ -195,7 +168,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes } protected void assertProviderEmpty() { - assertZeroTabs(); assertThreadsEmpty(); } @@ -218,53 +190,9 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes 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 public void testActivateNoTraceEmptiesProvider() throws Exception { + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); createAndOpenTrace(); addThreads(); traceManager.activateTrace(tb.trace); @@ -278,26 +206,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes 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 public void testCurrentTraceClosedEmptiesProvider() throws Exception { createAndOpenTrace(); @@ -313,25 +221,6 @@ public class DebuggerThreadsProviderTest extends AbstractGhidraHeadedDebuggerTes waitForPass(() -> assertThreadsEmpty()); } - @Test - public void testCloseTraceTabPopupMenuItem() throws Exception { - createAndOpenTrace(); - waitForTasks(); - - waitForPass(() -> assertOneTabPopulated()); - clickListItem(provider.traceTabs.getList(), 0, MouseEvent.BUTTON3); - waitForTasks(); - Set expected = Set.of("Close " + tb.trace.getName()); - assertMenu(expected, expected); - - clickSubMenuItemByText("Close " + tb.trace.getName()); - waitForTasks(); - - waitForPass(() -> { - assertEquals(Set.of(), traceManager.getOpenTraces()); - }); - } - @Test public void testActivateThenAddThreadsPopulatesProvider() throws Exception { createAndOpenTrace(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanelTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanelTest.java new file mode 100644 index 0000000000..ea1bf923ef --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/gui/trace/DebuggerTraceTabPanelTest.java @@ -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()); + } + } +} diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java index 4c43781d7d..40fe1bcdf7 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/control/MockTarget.java @@ -46,6 +46,11 @@ class MockTarget implements Target { this.trace = trace; } + @Override + public String describe() { + return "Mock Target"; + } + @Override public boolean isValid() { return true; diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java index c9d5830177..2d4199b23e 100644 --- a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTest.java @@ -141,6 +141,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(thread, traceManager.getCurrentThread()); + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); traceManager.activateTrace(null); waitForSwing(); @@ -177,6 +178,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(5, traceManager.getCurrentSnap()); + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); traceManager.activateTrace(null); waitForSwing(); @@ -207,6 +209,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(5, traceManager.getCurrentFrame()); + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); traceManager.activateTrace(null); waitForSwing(); @@ -251,6 +254,7 @@ public class DebuggerTraceManagerServiceTest extends AbstractGhidraHeadedDebugge assertEquals(objThread0, traceManager.getCurrentObject()); assertEquals(thread, traceManager.getCurrentThread()); + DebuggerTraceManagerServiceTestAccess.setEnsureActiveTrace(traceManager, false); traceManager.activateTrace(null); waitForSwing(); diff --git a/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTestAccess.java b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTestAccess.java new file mode 100644 index 0000000000..43e8740e82 --- /dev/null +++ b/Ghidra/Debug/Debugger/src/test/java/ghidra/app/plugin/core/debug/service/tracemgr/DebuggerTraceManagerServiceTestAccess.java @@ -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; + } +} diff --git a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java b/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java deleted file mode 100644 index d7a8ac8254..0000000000 --- a/Ghidra/Debug/ProposedUtils/src/main/java/docking/widgets/HorizontalTabPanel.java +++ /dev/null @@ -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 extends JPanel { - public static Color copyColor(Color c) { - return new Color(c.getRGB()); - } - - public static class TabListCellRenderer implements ListCellRenderer { - 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 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 list = new JList<>(); - private final JScrollPane scroll = new JScrollPane(list); - private final JViewport viewport = scroll.getViewport(); - private final DefaultListModel 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 after 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 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); - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java index e8a99fd060..bc14648244 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java @@ -222,51 +222,12 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti } } - String getStringUsedInList(Program program) { - DomainFile df = program.getDomainFile(); - String changeIndicator = program.isChanged() ? "*" : ""; - String pathString = getShortPath(df); - if (!df.isInWritableProject()) { - return pathString + " [Read-Only]" + changeIndicator; - } - return pathString + changeIndicator; + private String getToolTip(Program program) { + return DomainObjectDisplayUtils.getToolTip(program); } - private String getShortPath(DomainFile df) { - String pathString = df.toString(); - 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; + private String getTabName(Program program) { + return DomainObjectDisplayUtils.getTabText(program); } void keyTypedFromListWindow(KeyEvent e) { @@ -334,22 +295,6 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti 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) { return progService.closeProgram(program, false); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObjectDisplayUtils.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObjectDisplayUtils.java new file mode 100644 index 0000000000..5aa1e002f4 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObjectDisplayUtils.java @@ -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()); + } +} diff --git a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java index 55df0404a0..e0a2a4b2e6 100644 --- a/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java +++ b/Ghidra/Test/DebuggerIntegrationTest/src/test/java/ghidra/debug/flatapi/DeadFlatDebuggerAPITest.java @@ -24,6 +24,7 @@ import org.junit.Test; import db.Transaction; import generic.Unique; +import ghidra.app.plugin.core.debug.service.tracemgr.DebuggerTraceManagerServiceTestAccess; import ghidra.app.script.GhidraState; import ghidra.app.services.DebuggerTraceManagerService; import ghidra.debug.api.breakpoint.LogicalBreakpoint; @@ -172,6 +173,7 @@ public class DeadFlatDebuggerAPITest extends AbstractFlatDebuggerAPITest