GP-4716 - Data Type Editors - Fixed the traversal order of the structure

editor
This commit is contained in:
dragonmacher 2024-06-26 19:11:04 -04:00
parent c95c7581d7
commit 6aadccc40a
7 changed files with 335 additions and 86 deletions

View File

@ -19,6 +19,7 @@ import java.awt.*;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.TitledBorder;
@ -35,8 +36,8 @@ import generic.theme.GThemeDefaults.Colors.Viewport;
import ghidra.app.plugin.core.compositeeditor.BitFieldPlacementComponent.BitAttributes;
import ghidra.program.model.data.*;
import ghidra.program.model.data.Composite;
import ghidra.util.HelpLocation;
import ghidra.util.InvalidNameException;
import ghidra.util.Swing;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.layout.PairLayout;
import ghidra.util.layout.VerticalLayout;
@ -77,12 +78,12 @@ public class CompEditorPanel extends CompositeEditorPanel {
private JLabel actualAlignmentLabel;
private JTextField actualAlignmentValueTextField;
private List<Component> focusList;
private BitFieldPlacementComponent bitViewComponent;
private DocumentListener fieldDocListener;
private ActionListener fieldActionListener;
private FocusListener fieldFocusListener;
private boolean updatingSize;
@ -234,6 +235,30 @@ public class CompEditorPanel extends CompositeEditorPanel {
return bitViewPanel;
}
@Override
protected List<Component> getFocusComponents() {
if (focusList == null) {
//@formatter:off
focusList = List.of(
table,
searchPanel.getTextField(),
nameTextField,
descriptionTextField,
sizeTextField,
// add the first radio button; the rest are reachable via arrow keys
defaultAlignButton,
packingEnablementButton,
// add the first radio button; the rest are reachable via arrow keys
defaultPackingButton
);
//@formatter:on
}
return focusList;
}
/**
* Create the Info Panel that is horizontally resizable. The panel contains
* the name, category, data type, size, and edit mode for the current
@ -288,10 +313,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
gridBagConstraints.gridwidth = 4;
infoPanel.add(nameTextField, gridBagConstraints);
if (helpManager != null) {
helpManager.registerHelp(nameTextField,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Name"));
}
provider.registerHelp(nameTextField, "Name");
}
private void setupDescription() {
@ -318,10 +340,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
gridBagConstraints.gridwidth = 4;
infoPanel.add(descriptionTextField, gridBagConstraints);
if (helpManager != null) {
helpManager.registerHelp(descriptionTextField, new HelpLocation(provider.getHelpTopic(),
provider.getHelpName() + "_" + "Description"));
}
provider.registerHelp(descriptionTextField, "Description");
}
private void setupCategory() {
@ -387,12 +406,10 @@ public class CompEditorPanel extends CompositeEditorPanel {
alignPanel = new JPanel(new GridBagLayout());
TitledBorder border = BorderFactory.createTitledBorder("align (minimum)");
// border.setTitlePosition(TitledBorder.ABOVE_TOP);
alignPanel.setBorder(border);
if (helpManager != null) {
helpManager.registerHelp(alignPanel,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Align"));
}
provider.registerHelp(alignPanel, "Align");
String alignmentToolTip =
"<html>The <B>align</B> control allows the overall minimum alignment of this<BR>" +
"data type to be specified. The actual computed alignment<BR>" +
@ -459,10 +476,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
});
defaultAlignButton.setToolTipText(alignmentToolTip);
if (helpManager != null) {
helpManager.registerHelp(defaultAlignButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Align"));
}
provider.registerHelp(defaultAlignButton, "Align");
}
private void setupMachineMinAlignButton() {
@ -478,10 +492,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
((CompEditorModel) model).setAlignmentType(AlignmentType.MACHINE, -1);
});
if (helpManager != null) {
helpManager.registerHelp(machineAlignButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Align"));
}
provider.registerHelp(machineAlignButton, "Align");
}
private void setupExplicitAlignButton() {
@ -492,18 +503,22 @@ public class CompEditorPanel extends CompositeEditorPanel {
"this composite may be any multiple of this value.</html>";
explicitAlignButton.setToolTipText(alignmentToolTip);
explicitAlignButton.addActionListener(e -> {
chooseExplicitAlign();
// As a convenience, when this radio button is focused, change focus to the editor field
explicitAlignButton.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
explicitAlignTextField.requestFocus();
}
});
explicitAlignButton.addActionListener(e -> chooseExplicitAlign());
if (helpManager != null) {
helpManager.registerHelp(explicitAlignButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Align"));
}
provider.registerHelp(explicitAlignButton, "Align");
explicitAlignTextField.setName("Explicit Alignment Value");
explicitAlignTextField.setEditable(true);
explicitAlignTextField.addActionListener(e -> adjustExplicitMinimumAlignmentValue());
explicitAlignTextField
.addKeyListener(new UpAndDownKeyListener(defaultAlignButton, machineAlignButton));
explicitAlignTextField.addFocusListener(new FocusListener() {
@Override
@ -522,10 +537,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
});
explicitAlignTextField.setToolTipText(alignmentToolTip);
if (helpManager != null) {
helpManager.registerHelp(explicitAlignTextField,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Align"));
}
provider.registerHelp(explicitAlignTextField, "Align");
refreshGUIMinimumAlignmentValue(); // Display the initial value.
}
@ -572,16 +584,16 @@ public class CompEditorPanel extends CompositeEditorPanel {
infoPanel.add(actualAlignmentPanel, gridBagConstraints);
actualAlignmentValueTextField = new JTextField(8);
actualAlignmentValueTextField
.setText(Integer.toString(((CompEditorModel) model).getActualAlignment()));
int actualAlignment = ((CompEditorModel) model).getActualAlignment();
actualAlignmentValueTextField.setText(Integer.toString(actualAlignment));
actualAlignmentValueTextField.setToolTipText(actualAlignmentToolTip);
actualAlignmentValueTextField.setEditable(false);
if (helpManager != null) {
helpManager.registerHelp(actualAlignmentValueTextField, new HelpLocation(
provider.getHelpTopic(), provider.getHelpName() + "_" + "ActualAlignment"));
}
actualAlignmentValueTextField.setEnabled(false);
actualAlignmentValueTextField.setBackground(getBackground());
actualAlignmentValueTextField.setName("Actual Alignment Value");
provider.registerHelp(actualAlignmentValueTextField, "ActualAlignment");
gridBagConstraints.insets = VERTICAL_INSETS;
gridBagConstraints.anchor = GridBagConstraints.LINE_START;
gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
@ -589,7 +601,6 @@ public class CompEditorPanel extends CompositeEditorPanel {
gridBagConstraints.gridx = 3;
gridBagConstraints.gridy = 3;
infoPanel.add(actualAlignmentValueTextField, gridBagConstraints);
actualAlignmentValueTextField.setBackground(getBackground());
}
private void setupPacking() {
@ -618,10 +629,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
packingGroup.add(defaultPackingButton);
packingGroup.add(explicitPackingButton);
if (helpManager != null) {
helpManager.registerHelp(packingPanel,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Pack"));
}
provider.registerHelp(packingPanel, "Pack");
addPackingComponents(innerPanel);
@ -669,6 +677,12 @@ public class CompEditorPanel extends CompositeEditorPanel {
// gridPanel.add(disabledPackingButton, gridBagConstraints);
}
protected boolean choosePacking() {
int choice = OptionDialog.showYesNoDialog(this, "Use Packing?",
"<html>Applying packing may drastically change this structure.<BR><BR>Use Packing?");
return choice == OptionDialog.YES_OPTION;
}
private void setupPackingEnablementButton() {
packingEnablementButton.setName("Packing Enablement");
String packingToolTipText =
@ -677,16 +691,24 @@ public class CompEditorPanel extends CompositeEditorPanel {
"<font color=\"" + Palette.BLUE.toHexString() +
"\" size=\"-2\">(&lt;F1&gt; for help)</font></html>";
packingEnablementButton.addActionListener(e -> {
// When turning this on, warn the use. This prevents accidental enablement
// destructively changing the structure.
if (packingEnablementButton.isSelected()) {
if (!choosePacking()) {
Swing.runLater(() -> packingEnablementButton.setSelected(false));
return;
}
}
((CompEditorModel) model).setPackingType(
packingEnablementButton.isSelected() ? PackingType.DEFAULT : PackingType.DISABLED,
-1);
});
packingEnablementButton.setToolTipText(packingToolTipText);
if (helpManager != null) {
helpManager.registerHelp(packingEnablementButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Pack"));
}
provider.registerHelp(packingEnablementButton, "Pack");
}
private void setupDefaultPackingButton() {
@ -699,10 +721,7 @@ public class CompEditorPanel extends CompositeEditorPanel {
});
defaultPackingButton.setToolTipText(packingToolTipText);
if (helpManager != null) {
helpManager.registerHelp(defaultPackingButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Pack"));
}
provider.registerHelp(defaultPackingButton, "Pack");
}
private void setupExplicitPackingButton() {
@ -710,16 +729,23 @@ public class CompEditorPanel extends CompositeEditorPanel {
String packingToolTipText =
"<html>Indicates an explicit pack size should be applied.</html>";
explicitPackingButton.addActionListener(e -> chooseByValuePacking());
explicitPackingButton.setToolTipText(packingToolTipText);
if (helpManager != null) {
helpManager.registerHelp(explicitPackingButton,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Pack"));
}
// As a convenience, when this radio button is focused, change focus to the editor field
explicitPackingButton.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
explicitPackingTextField.requestFocus();
}
});
explicitPackingButton.addActionListener(e -> chooseByValuePacking());
provider.registerHelp(explicitPackingButton, "Pack");
explicitPackingTextField.setName("Packing Value");
explicitPackingTextField.setEditable(true);
explicitPackingTextField.addActionListener(e -> adjustPackingValue());
explicitPackingTextField.addKeyListener(
new UpAndDownKeyListener(defaultPackingButton, defaultPackingButton));
explicitPackingTextField.addFocusListener(new FocusListener() {
@Override
@ -738,10 +764,8 @@ public class CompEditorPanel extends CompositeEditorPanel {
});
explicitPackingTextField.setToolTipText(packingToolTipText);
if (helpManager != null) {
helpManager.registerHelp(explicitPackingTextField,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + "Pack"));
}
provider.registerHelp(explicitPackingTextField, "Pack");
}
private void chooseByValuePacking() {
@ -830,8 +854,9 @@ public class CompEditorPanel extends CompositeEditorPanel {
protected void setSizeEditable(boolean editable) {
sizeTextField.setEditable(editable);
sizeTextField.setEnabled(editable);
if (editable) {
// editable - use same background as category field
// editable - use same background as description field
sizeTextField.setBackground(descriptionTextField.getBackground());
}
else {
@ -1216,8 +1241,46 @@ public class CompEditorPanel extends CompositeEditorPanel {
@Override
public void showUndefinedStateChanged(boolean showUndefinedBytes) {
// TODO Auto-generated method stub
// stub
}
/**
* A simple class that allows clients to focus other components when the up or down arrows keys
* are pressed
*/
private class UpAndDownKeyListener extends KeyAdapter {
private JRadioButton previousComponent;
private JRadioButton nextComponent;
UpAndDownKeyListener(JRadioButton previousComponent, JRadioButton nextComponent) {
this.previousComponent = previousComponent;
this.nextComponent = nextComponent;
}
@Override
public void keyPressed(KeyEvent e) {
if (e.isConsumed()) {
return;
}
int code = e.getKeyCode();
if (code == KeyEvent.VK_UP) {
// We need to run later due to focusLost() listener on the text field that will
// interfere with the selected state of our newly selected button
previousComponent.requestFocusInWindow();
Swing.runLater(() -> previousComponent.setSelected(true));
e.consume();
}
else if (code == KeyEvent.VK_DOWN) {
// We need to run later due to focusLost() listener on the text field that will
// interfere with the selected state of our newly selected button
nextComponent.requestFocusInWindow();
Swing.runLater(() -> nextComponent.setSelected(true));
e.consume();
}
}
}
}

View File

@ -71,8 +71,6 @@ public abstract class CompositeEditorPanel extends JPanel
protected static final Border BEVELED_BORDER = BorderFactory.createLoweredBevelBorder();
protected static final HelpService helpManager = Help.getHelpService();
protected CompositeEditorProvider provider;
protected CompositeEditorModel model;
protected GTable table;
@ -91,27 +89,47 @@ public abstract class CompositeEditorPanel extends JPanel
private DataFlavor[] acceptableFlavors; // data flavors that are valid.
protected int lastDndAction = DnDConstants.ACTION_NONE;
protected SearchControlPanel searchPanel;
public CompositeEditorPanel(CompositeEditorModel model, CompositeEditorProvider provider) {
super(new BorderLayout());
JPanel lowerPanel = new JPanel(new VerticalLayout(5));
this.provider = provider;
this.model = model;
createTable();
JPanel lowerPanel = new JPanel(new VerticalLayout(5));
JPanel bitViewerPanel = createBitViewerPanel();
if (bitViewerPanel != null) {
lowerPanel.add(bitViewerPanel);
}
JPanel infoPanel = createInfoPanel();
if (infoPanel != null) {
adjustCompositeInfo();
lowerPanel.add(infoPanel);
}
lowerPanel.add(createStatusPanel());
add(lowerPanel, BorderLayout.SOUTH);
model.addCompositeEditorModelListener(this);
setUpDragDrop();
// These 2 methods allow us to specify the order of component navigation when Tab and
// Shift-Tab are pressed
setFocusTraversalPolicy(new CompFocusTraversalPolicy());
setFocusTraversalPolicyProvider(true);
}
/**
* Returns a list of focus traversal components. This list will be used to navigate forward
* and backward when the Tab and Shift-Tab keys are pressed. The components will be traversed
* in the order they are contained in the list.
*
* @return the list
*/
protected abstract List<Component> getFocusComponents();
protected Composite getOriginalComposite() {
return model.getOriginalComposite();
}
@ -640,12 +658,11 @@ public abstract class CompositeEditorPanel extends JPanel
table.setPreferredScrollableViewportSize(new Dimension(model.getWidth(), 250));
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
tablePanel.add(sp, BorderLayout.CENTER);
SearchControlPanel searchPanel = new SearchControlPanel(this);
searchPanel = new SearchControlPanel(this);
HelpService help = Help.getHelpService();
help.registerHelp(searchPanel, new HelpLocation("DataTypeEditors", "Searching_In_Editor"));
if (helpManager != null) {
helpManager.registerHelp(searchPanel,
new HelpLocation("DataTypeEditors", "Searching_In_Editor"));
}
tablePanel.add(searchPanel, BorderLayout.SOUTH);
add(tablePanel, BorderLayout.CENTER);
@ -757,10 +774,8 @@ public abstract class CompositeEditorPanel extends JPanel
panel.add(label);
panel.add(Box.createHorizontalStrut(2));
panel.add(textField);
if (helpManager != null) {
helpManager.registerHelp(textField,
new HelpLocation(provider.getHelpTopic(), provider.getHelpName() + "_" + name));
}
provider.registerHelp(textField, name);
return panel;
}
@ -1456,4 +1471,140 @@ public abstract class CompositeEditorPanel extends JPanel
KeyBindingUtils.clearKeyBinding(this, keyStroke);
}
}
/**
* A simple traversal policy that allows this editor panel to control the order that components
* get focused when pressing Tab and Ctrl-Tab.
* <P>
* Note: We typically do not use traversal policies in the application. We do so here due to
* the complicated nature of this widget. It seemed easier to specify the policy than to
* change the order of the widgets in the UI to get the expected traversal order.
* <P>
* @see #getFocusComponents()
*/
private class CompFocusTraversalPolicy extends FocusTraversalPolicy {
@Override
public Component getComponentAfter(Container aContainer, Component aComponent) {
List<Component> list = getFocusComponents();
return getNext(aComponent, list);
}
private Component getNext(Component component, List<Component> list) {
int currentIndex = list.indexOf(component);
if (currentIndex < 0) {
// The given component is not in the list of traversal components. This can happen
// when some widget in the panel has focus but is not part of the focus traversal.
// Assume the component is part of a group of components that can be traversed. Get
// the next component after this group.
return getNextGroupComponent(component, list);
}
int nextIndex = currentIndex + 1;
if (nextIndex == list.size()) {
nextIndex = 0; // wrap
}
Component next = list.get(nextIndex);
if (!next.isFocusable() || !next.isEnabled()) {
return getNext(next, list);
}
return next;
}
/**
* Find a sibling of the given component and get the component after the sibling. We can do
* this since we have guilty knowledge that the few focusable components not in the
* traversal list have siblings that are. In that case, all siblings represent the focused
* group. This will move to the next component after that group.
*
* @param component the component used to find the next component
* @param list the list of traversal components
* @return the next component
*/
private Component getNextGroupComponent(Component component, List<Component> list) {
Component sibling = findSibling(component, list);
if (sibling == null) {
return list.get(0);
}
return getNext(sibling, list);
}
// see the description for getNextGroupComponent()
private Component getPreviousGroupComponent(Component component, List<Component> list) {
Component sibling = findSibling(component, list);
if (sibling == null) {
return list.get(0);
}
return getPrevious(sibling, list);
}
/**
* Finds the first sibling of the given component in the given list.
*
* @param component the component that is not in the list, but has a sibling in the list
* @param list the list of focus traversal components
* @return the sibling or null
*/
private Component findSibling(Component component, List<Component> list) {
Container parent = component.getParent();
Component[] siblings = parent.getComponents();
for (Component sibling : siblings) {
if (list.contains(sibling)) {
return sibling;
}
}
return null;
}
@Override
public Component getComponentBefore(Container aContainer, Component aComponent) {
List<Component> list = getFocusComponents();
return getPrevious(aComponent, list);
}
private Component getPrevious(Component aComponent, List<Component> list) {
int currentIndex = list.indexOf(aComponent);
if (currentIndex < 0) {
// The given component is not in the list of traversal components. This can happen
// when some widget in the panel has focus but is not part of the focus traversal.
// Assume the component is part of a group of components that can be traversed. Get
// the previous component before this group.
return getPreviousGroupComponent(aComponent, list);
}
int previousIndex = currentIndex - 1;
if (previousIndex == -1) {
previousIndex = list.size() - 1; // wrap
}
Component previous = list.get(previousIndex);
if (!previous.isFocusable() || !previous.isEnabled()) {
return getPrevious(previous, list);
}
return previous;
}
@Override
public Component getFirstComponent(Container aContainer) {
List<Component> list = getFocusComponents();
return list.get(0);
}
@Override
public Component getLastComponent(Container aContainer) {
List<Component> list = getFocusComponents();
return list.get(list.size() - 1);
}
@Override
public Component getDefaultComponent(Container aContainer) {
List<Component> list = getFocusComponents();
return list.get(0);
}
}
}

View File

@ -33,6 +33,8 @@ import ghidra.util.HelpLocation;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException;
import help.Help;
import help.HelpService;
/**
* Editor provider for a Composite Data Type.
@ -332,4 +334,9 @@ public abstract class CompositeEditorProvider extends ComponentProviderAdapter
return true;
}
protected void registerHelp(Object object, String anchor) {
HelpService help = Help.getHelpService();
help.registerHelp(object, new HelpLocation(getHelpTopic(), getHelpName() + "_" + anchor));
}
}

View File

@ -107,4 +107,7 @@ public class SearchControlPanel extends JPanel {
}
}
public JTextField getTextField() {
return textField;
}
}

View File

@ -28,4 +28,8 @@ public class UnionEditorPanel extends CompEditorPanel {
return null;
}
@Override
protected boolean choosePacking() {
return true; // packing is not destructive to unions, so safe to use without prompting
}
}

View File

@ -15,7 +15,9 @@
*/
package ghidra.app.plugin.core.stackeditor;
import java.awt.Component;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
@ -36,6 +38,7 @@ public class StackEditorPanel extends CompositeEditorPanel {
private JTextField paramSizeField;
private JTextField paramOffsetField;
private JTextField returnAddrOffsetField;
private List<Component> focusList;
public StackEditorPanel(Program program, StackEditorModel model, StackEditorProvider provider) {
super(model, provider);
@ -61,10 +64,21 @@ public class StackEditorPanel extends CompositeEditorPanel {
return Integer.decode(returnAddrOffsetField.getText()).intValue();
}
/*
* (non-Javadoc)
* @see ghidra.app.plugin.compositeeditor.CompositeEditorPanel#createInfoPanel()
*/
@Override
protected List<Component> getFocusComponents() {
if (focusList == null) {
//@formatter:off
focusList = List.of(
table,
searchPanel.getTextField(),
localSizeField,
paramSizeField
);
//@formatter:on
}
return focusList;
}
@Override
protected JPanel createInfoPanel() {
@ -83,11 +97,10 @@ public class StackEditorPanel extends CompositeEditorPanel {
JPanel returnAddrOffsetPanel =
createNamedTextPanel(returnAddrOffsetField, "Return Address Offset");
JPanel[] hPanels =
new JPanel[] {
createHorizontalPanel(new JPanel[] { frameSizePanel, returnAddrOffsetPanel,
localSizePanel }),
createHorizontalPanel(new JPanel[] { paramOffsetPanel, paramSizePanel }) };
JPanel[] hPanels = new JPanel[] {
createHorizontalPanel(
new JPanel[] { frameSizePanel, returnAddrOffsetPanel, localSizePanel }),
createHorizontalPanel(new JPanel[] { paramOffsetPanel, paramSizePanel }) };
JPanel outerPanel = createVerticalPanel(hPanels);
outerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
@ -98,6 +111,7 @@ public class StackEditorPanel extends CompositeEditorPanel {
frameSizeField = new JTextField(20);
frameSizeField.setName("Frame Size");
frameSizeField.setEditable(false);
frameSizeField.setEnabled(false);
}
private void setupLocalSize() {
@ -195,12 +209,14 @@ public class StackEditorPanel extends CompositeEditorPanel {
paramOffsetField = new JTextField(20);
paramOffsetField.setName("Parameter Offset");
paramOffsetField.setEditable(false);
paramOffsetField.setEnabled(false);
}
private void setupReturnAddrOffset() {
returnAddrOffsetField = new JTextField(20);
returnAddrOffsetField.setName("Return Address Offset");
returnAddrOffsetField.setEditable(false);
returnAddrOffsetField.setEnabled(false);
}
/* (non-Javadoc)

View File

@ -16,6 +16,7 @@
package help;
import docking.DefaultHelpService;
import ghidra.util.Msg;
/**
* Creates the HelpManager for the application. This is just a glorified global variable for
@ -28,7 +29,7 @@ public class Help {
/**
* Get the help service
*
* @return null if the call to setMainHelpSetURL() failed
* @return a non-null help service
*/
public static HelpService getHelpService() {
return helpService;
@ -36,6 +37,10 @@ public class Help {
// allows help services to install themselves
public static void installHelpService(HelpService service) {
if (service == null) {
Msg.debug(Help.class, "Attempted to install null help service");
return;
}
helpService = service;
}