GP-2623: Improve connect dialog and factory descriptions

This commit is contained in:
Dan 2023-01-18 16:53:01 -05:00
parent 5195aaebc1
commit 8dbf2341b2
30 changed files with 565 additions and 333 deletions

View file

@ -34,10 +34,12 @@ 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.DebuggerModelService;
import ghidra.app.services.ProgramManager;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.framework.plugintool.AutoService;
import ghidra.framework.plugintool.ComponentProviderAdapter;
import ghidra.framework.plugintool.annotation.AutoServiceConsumed;
import ghidra.program.model.listing.Program;
public class DebuggerTargetsProvider extends ComponentProviderAdapter {
@ -107,7 +109,9 @@ public class DebuggerTargetsProvider extends ComponentProviderAdapter {
public void actionPerformed(ActionContext context) {
// NB. Drop the future on the floor, because the UI will report issues.
// Cancellation should be ignored.
modelService.showConnectDialog();
ProgramManager programManager = tool.getService(ProgramManager.class);
Program program = programManager == null ? null : programManager.getCurrentProgram();
modelService.showConnectDialog(program);
}
@Override

View file

@ -37,7 +37,6 @@ public class DbgDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
protected List<String> getLauncherPath() {
return PathUtils.parse("");
}
}
protected class InVmDbgengDebuggerProgramLaunchOffer
@ -133,7 +132,7 @@ public class DbgDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -95,7 +95,7 @@ public class FridaDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchO
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -139,7 +139,7 @@ public class GdbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOpi
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -90,7 +90,7 @@ public class LldbDebuggerProgramLaunchOpinion implements DebuggerProgramLaunchOp
}
List<DebuggerProgramLaunchOffer> offers = new ArrayList<>();
for (DebuggerModelFactory factory : service.getModelFactories()) {
if (!factory.isCompatible()) {
if (!factory.isCompatible(program)) {
continue;
}
String clsName = factory.getClass().getName();

View file

@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.text.View;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
@ -40,13 +41,14 @@ import ghidra.dbg.DebuggerModelFactory;
import ghidra.dbg.DebuggerObjectModel;
import ghidra.dbg.util.ConfigurableFactory.Property;
import ghidra.framework.options.SaveState;
import ghidra.program.model.listing.Program;
import ghidra.util.*;
import ghidra.util.datastruct.CollectionChangeListener;
import ghidra.util.layout.PairLayout;
public class DebuggerConnectDialog extends DialogComponentProvider
implements PropertyChangeListener {
private static final String KEY_FACTORY_CLASSNAME = "factoryClassname";
private static final String KEY_CURRENT_FACTORY_CLASSNAME = "currentFactoryCls";
private static final String KEY_SUCCESS_FACTORY_CLASSNAME = "successFactoryCls";
private static final String HTML_BOLD_DESCRIPTION = "<html><b>Description:</b> ";
protected class FactoriesChangedListener
@ -67,9 +69,40 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
}
protected record FactoryEntry(DebuggerModelFactory factory) {
@Override
public String toString() {
return factory.getBrief();
}
}
protected record PrioritizedFactory(FactoryEntry entry, int priority) {
public PrioritizedFactory(FactoryEntry ent, Program program) {
this(ent, ent.factory.getPriority(program));
}
}
protected enum NameComparator implements Comparator<String> {
INSTANCE;
@Override
public int compare(String o1, String o2) {
boolean p1 = o1.startsWith("PROTOTYPE:");
boolean p2 = o2.startsWith("PROTOTYPE:");
if (p1 && !p2) {
return 1;
}
if (!p1 && p2) {
return -1;
}
return o1.toLowerCase().compareTo(o2.toLowerCase());
}
}
private DebuggerModelService modelService;
private DebuggerModelFactory factory;
private DebuggerModelFactory currentFactory;
private DebuggerModelFactory successFactory;
private final Map<DebuggerModelFactory, FactoryEntry> factories = new HashMap<>();
private FactoriesChangedListener listener = new FactoriesChangedListener();
@ -81,26 +114,12 @@ public class DebuggerConnectDialog extends DialogComponentProvider
private final Map<Property<?>, Component> components = new LinkedHashMap<>();
protected JLabel description;
protected JPanel pairPanel;
private PairLayout layout;
protected JPanel gridPanel;
protected JButton connectButton;
protected CompletableFuture<? extends DebuggerObjectModel> futureConnect;
protected CompletableFuture<DebuggerObjectModel> result;
protected static class FactoryEntry {
DebuggerModelFactory factory;
public FactoryEntry(DebuggerModelFactory factory) {
this.factory = factory;
}
@Override
public String toString() {
return factory.getBrief();
}
}
public DebuggerConnectDialog() {
super(AbstractConnectAction.NAME, true, true, true, false);
@ -126,7 +145,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
toAdd.add(entry);
}
SwingUtilities.invokeLater(() -> {
toAdd.sort(Comparator.comparing(FactoryEntry::toString));
toAdd.sort(Comparator.comparing(FactoryEntry::toString, NameComparator.INSTANCE));
for (FactoryEntry entry : toAdd) {
dropdownModel.addElement(entry);
}
@ -190,20 +209,20 @@ public class DebuggerConnectDialog extends DialogComponentProvider
JPanel inner = new JPanel(new BorderLayout());
description = new JLabel(HTML_BOLD_DESCRIPTION + "</html>");
description.setBorder(new EmptyBorder(10, 0, 10, 0));
description.setPreferredSize(new Dimension(400, 150));
inner.add(description);
topBox.add(inner);
panel.add(topBox, BorderLayout.NORTH);
layout = new PairLayout(5, 5);
pairPanel = new JPanel(layout);
gridPanel = new JPanel(new GridBagLayout());
JPanel centering = new JPanel(new FlowLayout(FlowLayout.CENTER));
JScrollPane scrolling = new JScrollPane(centering, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrolling.setPreferredSize(new Dimension(100, 130));
scrolling.setPreferredSize(new Dimension(100, 200));
panel.add(scrolling, BorderLayout.CENTER);
centering.add(pairPanel);
centering.add(gridPanel);
addWorkPanel(panel);
@ -219,13 +238,15 @@ public class DebuggerConnectDialog extends DialogComponentProvider
private void itemSelected(ItemEvent evt) {
if (evt.getStateChange() == ItemEvent.DESELECTED) {
pairPanel.removeAll();
gridPanel.removeAll();
}
else if (evt.getStateChange() == ItemEvent.SELECTED) {
FactoryEntry ent = (FactoryEntry) evt.getItem();
factory = ent.factory;
currentFactory = ent.factory;
populateOptions();
//repack();
/**
* Don't repack here. It can shrink the dialog, which may not be what the user wants.
*/
}
}
@ -239,7 +260,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
setStatusText("Connecting...");
synchronized (this) {
futureConnect = factory.build();
futureConnect = currentFactory.build();
}
futureConnect.thenCompose(m -> m.fetchModelRoot()).thenAcceptAsync(r -> {
DebuggerObjectModel m = r.getModel();
@ -268,6 +289,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
synchronized (this) {
futureConnect = null;
}
successFactory = currentFactory;
connectButton.setEnabled(true);
});
}
@ -284,7 +306,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
protected synchronized CompletableFuture<DebuggerObjectModel> reset(
DebuggerModelFactory factory) {
DebuggerModelFactory factory, Program program) {
if (factory != null) {
synchronized (factories) {
dropdownModel.setSelectedItem(factories.get(factory));
@ -292,6 +314,7 @@ public class DebuggerConnectDialog extends DialogComponentProvider
dropdown.setEnabled(false);
}
else {
selectCompatibleFactory(program);
dropdown.setEnabled(true);
}
@ -311,18 +334,42 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
protected void populateOptions() {
description.setText(
HTML_BOLD_DESCRIPTION + HTMLUtilities.friendlyEncodeHTML(factory.getHtmlDetails()));
description.setText(HTML_BOLD_DESCRIPTION + currentFactory.getHtmlDetails());
propertyEditors.clear();
components.clear();
Map<String, Property<?>> optsMap = factory.getOptions();
//layout.setRows(Math.max(1, optsMap.size()));
pairPanel.removeAll();
Map<String, Property<?>> optsMap = currentFactory.getOptions();
gridPanel.removeAll();
GridBagConstraints constraints;
if (optsMap.isEmpty()) {
JLabel label =
new JLabel("<html>There are no configuration options for this connector.");
constraints = new GridBagConstraints();
gridPanel.add(label, constraints);
}
int i = 0;
for (Map.Entry<String, Property<?>> opt : optsMap.entrySet()) {
Property<?> property = opt.getValue();
JLabel label = new JLabel(opt.getKey());
pairPanel.add(label);
JLabel label = new JLabel("<html>" + HTMLUtilities.escapeHTML(opt.getKey())) {
@Override
public Dimension getPreferredSize() {
View v = (View) getClientProperty("html");
if (v == null) {
return super.getPreferredSize();
}
v.setSize(200, 0);
float height = v.getPreferredSpan(View.Y_AXIS);
return new Dimension(200, (int) height);
}
};
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.BOTH;
constraints.gridx = 0;
constraints.gridy = i;
constraints.insets = new Insets(i == 0 ? 0 : 5, 0, 0, 5);
gridPanel.add(label, constraints);
Class<?> type = property.getValueClass();
PropertyEditor editor = PropertyEditorManager.findEditor(type);
@ -331,11 +378,22 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
editor.setValue(property.getValue());
editor.addPropertyChangeListener(this);
Component comp = MiscellaneousUtils.getEditorComponent(editor);
pairPanel.add(comp);
Component editorComponent = MiscellaneousUtils.getEditorComponent(editor);
if (editorComponent instanceof JTextField textField) {
textField.setColumns(13);
}
constraints = new GridBagConstraints();
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.anchor = GridBagConstraints.WEST;
constraints.gridx = 1;
constraints.gridy = i;
constraints.insets = new Insets(i == 0 ? 0 : 5, 0, 0, 0);
gridPanel.add(editorComponent, constraints);
propertyEditors.put(property, editor);
components.put(property, comp);
components.put(property, editorComponent);
i++;
}
}
@ -350,24 +408,75 @@ public class DebuggerConnectDialog extends DialogComponentProvider
}
public void writeConfigState(SaveState saveState) {
if (factory != null) {
saveState.putString(KEY_FACTORY_CLASSNAME, factory.getClass().getName());
if (currentFactory != null) {
saveState.putString(KEY_CURRENT_FACTORY_CLASSNAME, currentFactory.getClass().getName());
}
if (successFactory != null) {
saveState.putString(KEY_SUCCESS_FACTORY_CLASSNAME, successFactory.getClass().getName());
}
}
public void readConfigState(SaveState saveState) {
String factoryName = saveState.getString(KEY_FACTORY_CLASSNAME, null);
if (factoryName == null) {
return;
}
protected FactoryEntry getByName(String className) {
synchronized (factories) {
for (Map.Entry<DebuggerModelFactory, FactoryEntry> ent : factories.entrySet()) {
String name = ent.getKey().getClass().getName();
if (factoryName.equals(name)) {
factory = ent.getKey();
dropdown.setSelectedItem(ent.getValue());
for (FactoryEntry ent : factories.values()) {
String name = ent.factory.getClass().getName();
if (className.equals(name)) {
return ent;
}
}
return null;
}
}
protected Collection<PrioritizedFactory> getByPriority(Program program) {
synchronized (factories) {
return factories.values()
.stream()
.map(e -> new PrioritizedFactory(e, program))
.sorted(Comparator.comparing(pf -> -pf.priority()))
.toList();
}
}
protected PrioritizedFactory getFirstCompatibleByPriority(Program program) {
for (PrioritizedFactory pf : getByPriority(program)) {
if (pf.priority >= 0) {
return pf;
}
return null;
}
return null;
}
protected void selectCompatibleFactory(Program program) {
if (currentFactory != null && currentFactory.isCompatible(program)) {
return;
}
if (successFactory != null && successFactory.isCompatible(program)) {
currentFactory = successFactory;
synchronized (factories) {
dropdown.setSelectedItem(factories.get(successFactory));
}
return;
}
PrioritizedFactory compat = getFirstCompatibleByPriority(program);
if (compat == null) {
return;
}
currentFactory = compat.entry.factory;
dropdown.setSelectedItem(compat.entry);
}
public void readConfigState(SaveState saveState) {
String currentFactoryName = saveState.getString(KEY_CURRENT_FACTORY_CLASSNAME, null);
FactoryEntry restoreCurrent =
currentFactoryName == null ? null : getByName(currentFactoryName);
currentFactory = restoreCurrent == null ? null : restoreCurrent.factory;
dropdown.setSelectedItem(restoreCurrent);
String successFactoryName = saveState.getString(KEY_SUCCESS_FACTORY_CLASSNAME, null);
FactoryEntry restoreSuccess =
successFactoryName == null ? null : getByName(successFactoryName);
successFactory = restoreSuccess == null ? null : restoreSuccess.factory;
}
}

View file

@ -641,14 +641,24 @@ public class DebuggerModelServicePlugin extends Plugin
}
protected CompletableFuture<DebuggerObjectModel> doShowConnectDialog(PluginTool tool,
DebuggerModelFactory factory) {
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory);
DebuggerModelFactory factory, Program program) {
CompletableFuture<DebuggerObjectModel> future = connectDialog.reset(factory, program);
tool.showDialog(connectDialog);
return future;
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return doShowConnectDialog(tool, null, null);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program) {
return doShowConnectDialog(tool, null, program);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return doShowConnectDialog(tool, factory);
return doShowConnectDialog(tool, factory, null);
}
}

View file

@ -269,9 +269,19 @@ public class DebuggerModelServiceProxyPlugin extends Plugin
closeAllModels();
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return delegate.doShowConnectDialog(tool, null, null);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program) {
return delegate.doShowConnectDialog(tool, null, program);
}
@Override
public CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory) {
return delegate.doShowConnectDialog(tool, factory);
return delegate.doShowConnectDialog(tool, factory, null);
}
@Override

View file

@ -353,14 +353,20 @@ public interface DebuggerModelService {
*
* @return a future which completes with the new connection, possibly cancelled
*/
default CompletableFuture<DebuggerObjectModel> showConnectDialog() {
return showConnectDialog(null);
}
CompletableFuture<DebuggerObjectModel> showConnectDialog();
/**
* Prompt the user to create a new connection, hinting at the program to launch
*
* @param program the current program used to help select a default
* @return a future which completes with the new connection, possibly cancelled
*/
CompletableFuture<DebuggerObjectModel> showConnectDialog(Program program);
/**
* Prompt the user to create a new connection, optionally fixing the factory
*
* @param factory the required factory, or null for user selection
* @param factory the required factory
* @return a future which completes with the new connection, possible cancelled
*/
CompletableFuture<DebuggerObjectModel> showConnectDialog(DebuggerModelFactory factory);

View file

@ -473,15 +473,15 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
DebuggerConnectDialog dialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) dialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(mb.testFactory, fe.factory());
assertEquals(TestDebuggerModelFactory.FAKE_DETAILS_HTML, dialog.description.getText());
Component[] components = dialog.pairPanel.getComponents();
Component[] components = dialog.gridPanel.getComponents();
assertTrue(components[0] instanceof JLabel);
JLabel label = (JLabel) components[0];
assertEquals(TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertEquals("<html>" + TestDebuggerModelFactory.FAKE_OPTION_NAME, label.getText());
assertTrue(components[1] instanceof JTextField);
JTextField field = (JTextField) components[1];
@ -518,7 +518,7 @@ public class DebuggerModelServiceTest extends AbstractGhidraHeadedDebuggerGUITes
DebuggerConnectDialog connectDialog = waitForDialogComponent(DebuggerConnectDialog.class);
FactoryEntry fe = (FactoryEntry) connectDialog.dropdownModel.getSelectedItem();
assertEquals(mb.testFactory, fe.factory);
assertEquals(mb.testFactory, fe.factory());
pressButtonByText(connectDialog, AbstractConnectAction.NAME, true);
// NOTE: testModel is null. Don't use #createTestModel(), which adds to service