Updated module system so Help no longer depends on Docking. Docking can now have help content.

This commit is contained in:
dragonmacher 2022-09-16 12:21:32 -04:00
parent a438a1e1ea
commit cb02db8313
87 changed files with 707 additions and 445 deletions

View file

@ -0,0 +1,113 @@
/* ###
* 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;
import java.awt.*;
import javax.swing.JButton;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import help.HelpDescriptor;
import help.HelpService;
public class DefaultHelpService implements HelpService {
@Override
public void showHelp(Object helpObj, boolean infoOnly, Component parent) {
if (infoOnly) {
displayHelpInfo(helpObj);
return;
}
}
@Override
public void showHelp(java.net.URL url) {
// no-op
}
@Override
public void showHelp(HelpLocation location) {
// no-op
}
@Override
public void excludeFromHelp(Object helpObject) {
// no-op
}
@Override
public boolean isExcludedFromHelp(Object helpObject) {
return false;
}
@Override
public void clearHelp(Object helpObject) {
// no-op
}
@Override
public void registerHelp(Object helpObj, HelpLocation helpLocation) {
// no-op
}
@Override
public HelpLocation getHelpLocation(Object object) {
return null;
}
@Override
public boolean helpExists() {
return false;
}
private void displayHelpInfo(Object helpObj) {
String msg = getHelpInfo(helpObj);
Msg.showInfo(this, null, "Help Info", msg);
}
private String getHelpInfo(Object helpObj) {
if (helpObj == null) {
return "Help Object is null";
}
StringBuilder buffy = new StringBuilder();
buffy.append("HELP OBJECT: " + helpObj.getClass().getName());
buffy.append("\n");
if (helpObj instanceof HelpDescriptor) {
HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj;
buffy.append(helpDescriptor.getHelpInfo());
}
else if (helpObj instanceof JButton) {
JButton button = (JButton) helpObj;
buffy.append(" BUTTON: " + button.getText());
buffy.append("\n");
Component c = button;
while (c != null && !(c instanceof Window)) {
c = c.getParent();
}
if (c instanceof Dialog) {
buffy.append(" DIALOG: " + ((Dialog) c).getTitle());
buffy.append("\n");
}
if (c instanceof Frame) {
buffy.append(" FRAME: " + ((Frame) c).getTitle());
buffy.append("\n");
}
}
return buffy.toString();
}
}

View file

@ -0,0 +1,237 @@
/* ###
* 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 help;
import java.awt.Component;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.Locale;
import javax.help.*;
import javax.help.Map.ID;
import javax.help.event.HelpModelEvent;
import javax.help.plaf.HelpNavigatorUI;
import javax.help.plaf.basic.BasicFavoritesCellRenderer;
import javax.help.plaf.basic.BasicFavoritesNavigatorUI;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import ghidra.util.Msg;
/**
* This class allows us to change the renderer of the favorites tree.
*/
public class CustomFavoritesView extends FavoritesView {
public CustomFavoritesView(HelpSet hs, String name, String label,
@SuppressWarnings("rawtypes") Hashtable params) {
this(hs, name, label, hs.getLocale(), params);
}
public CustomFavoritesView(HelpSet hs, String name, String label, Locale locale,
@SuppressWarnings("rawtypes") Hashtable params) {
super(hs, name, label, locale, params);
}
@Override
public Component createNavigator(HelpModel model) {
return new CustomHelpFavoritesNavigator(this, model);
}
//==================================================================================================
// Inner Classes
//==================================================================================================
class CustomHelpFavoritesNavigator extends JHelpFavoritesNavigator {
CustomHelpFavoritesNavigator(NavigatorView view, HelpModel model) {
super(view, model);
}
@Override
public void setUI(HelpNavigatorUI ui) {
super.setUI(new CustomFavoritesNavigatorUI(this));
}
}
class CustomFavoritesNavigatorUI extends BasicFavoritesNavigatorUI {
private PropertyChangeListener titleListener;
CustomFavoritesNavigatorUI(JHelpFavoritesNavigator b) {
super(b);
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
tree.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(java.awt.event.KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DELETE ||
e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
removeAction.actionPerformed(null);
}
}
});
// Note: add a listener to fix the bug described in 'idChanged()' below
HelpModel model = favorites.getModel();
titleListener = e -> {
if (lastIdEvent == null) {
return;
}
currentTitle = (String) e.getNewValue();
if (currentTitle == null) {
return;
}
String lastTitle = lastIdEvent.getHistoryName();
if (!currentTitle.equals(lastTitle)) {
resendNewEventWithFixedTitle(lastIdEvent, currentTitle);
}
};
model.addPropertyChangeListener(titleListener);
}
@Override
public void uninstallUI(JComponent c) {
HelpModel model = favorites.getModel();
if (model != null) {
model.removePropertyChangeListener(titleListener);
}
super.uninstallUI(c);
}
private void resendNewEventWithFixedTitle(HelpModelEvent originalEvent, String title) {
HelpModelEvent e = originalEvent;
HelpModelEvent newEvent =
new HelpModelEvent(e.getSource(), e.getID(), e.getURL(), title, favorites);
idChanged(newEvent);
}
private HelpModelEvent lastIdEvent;
private String currentTitle;
@Override
protected void setCellRenderer(NavigatorView view, JTree tree) {
tree.setCellRenderer(new CustomFavoritesCellRenderer(favorites.getModel()));
}
@Override
public void idChanged(HelpModelEvent e) {
//
// Overridden to track the change events. We need this to fix a bug where our
// parent class will get the wrong title of the page being loaded *when the user
// has navigated via a hyperlink*. The result of using the wrong title
// manifests itself when the user makes a 'favorite' item--the title will not
// match the 'favorite'd page.
//
// this is how the super class stores off it's 'contentTitle' variable
lastIdEvent = e;
super.idChanged(e);
}
}
class CustomFavoritesCellRenderer extends BasicFavoritesCellRenderer {
private final HelpModel helpModel;
public CustomFavoritesCellRenderer(HelpModel helpModel) {
this.helpModel = helpModel;
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
boolean expanded, boolean leaf, int row, boolean isFocused) {
CustomFavoritesCellRenderer renderer =
(CustomFavoritesCellRenderer) super.getTreeCellRendererComponent(tree, value, sel,
expanded, leaf, row, isFocused);
Object o = ((DefaultMutableTreeNode) value).getUserObject();
FavoritesItem item = (FavoritesItem) o;
if (item == null) {
return renderer;
}
HelpSet helpSet = helpModel.getHelpSet();
Map combinedMap = helpSet.getCombinedMap();
URL URL = getURL(item, helpSet, combinedMap);
if (URL == null) {
// should only happen if the user has old favorites; trust the old name
return renderer;
}
String text = URL.getFile();
int index = text.lastIndexOf('/');
if (index != -1) {
// we want just the filename
text = text.substring(index + 1);
}
String ref = URL.getRef();
if (ref != null) {
text += "#" + ref;
}
renderer.setText(item.getName() + " - " + text);
return renderer;
}
private URL getURL(FavoritesItem item, HelpSet helpSet, Map combinedMap) {
String target = item.getTarget();
if (target == null) {
// use the URL of the item
return item.getURL();
}
ID newID = null;
try {
newID = ID.create(target, helpSet);
}
catch (BadIDException e) {
Msg.debug(this, "Invalid help ID; Mabye bad favorite bookmark?: " + target);
return null;
}
try {
return combinedMap.getURLFromID(newID);
}
catch (MalformedURLException e) {
//shouldn't happen
Msg.error(this, "Unexpected Exception", e);
}
return null;
}
}
}

View file

@ -0,0 +1,96 @@
/* ###
* 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 help;
import ghidra.util.Msg;
import java.awt.*;
import java.util.*;
import javax.help.*;
import javax.help.plaf.HelpNavigatorUI;
import javax.help.plaf.basic.BasicSearchNavigatorUI;
import javax.help.search.SearchEvent;
public class CustomSearchView extends SearchView {
public CustomSearchView(HelpSet hs, String name, String label, Locale locale,
@SuppressWarnings("rawtypes") Hashtable params) {
super(hs, name, label, locale, params);
}
@Override
public Component createNavigator(HelpModel model) {
return new CustomHelpSearchNavigator(this, model);
}
//==================================================================================================
// Inner Classes
//==================================================================================================
class CustomHelpSearchNavigator extends JHelpSearchNavigator {
public CustomHelpSearchNavigator(NavigatorView view, HelpModel model) {
super(view, model);
}
@Override
public void setUI(HelpNavigatorUI ui) {
super.setUI(new CustomSearchNavigatorUI(this));
}
}
class CustomSearchNavigatorUI extends BasicSearchNavigatorUI {
private boolean hasResults;
public CustomSearchNavigatorUI(JHelpSearchNavigator navigator) {
super(navigator);
}
@Override
public synchronized void searchStarted(SearchEvent e) {
hasResults = false;
super.searchStarted(e);
}
@Override
public synchronized void itemsFound(SearchEvent e) {
super.itemsFound(e);
@SuppressWarnings("rawtypes")
Enumeration searchItems = e.getSearchItems();
if (searchItems == null) {
return;
}
hasResults |= e.getSearchItems().hasMoreElements();
}
@Override
public synchronized void searchFinished(SearchEvent e) {
super.searchFinished(e);
if (!hasResults) {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Window activeWindow = kfm.getActiveWindow();
Msg.showInfo(this, activeWindow, "No Results Founds",
"No search results found for \"" + e.getParams() + "\"");
}
}
}
}

View file

@ -0,0 +1,524 @@
/* ###
* 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 help;
import java.awt.Component;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.Locale;
import javax.help.*;
import javax.help.Map.ID;
import javax.help.event.HelpModelEvent;
import javax.help.plaf.HelpNavigatorUI;
import javax.help.plaf.basic.BasicTOCCellRenderer;
import javax.help.plaf.basic.BasicTOCNavigatorUI;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
/**
* A custom Table of Contents view that we specify in our JavaHelp xml documents. This view
* lets us install custom renderers and custom tree items for use by those renderers. These
* renderers let us display custom text defined by the TOC_Source.xml files. We also add some
* utility like: tooltips in development mode, node selection when pressing F1.
*/
public class CustomTOCView extends TOCView {
private CustomTOCNavigatorUI ui;
private boolean isSelectingNodeInternally;
// Hashtable
public CustomTOCView(HelpSet hs, String name, String label,
@SuppressWarnings("rawtypes") Hashtable params) {
this(hs, name, label, hs.getLocale(), params);
}
// Hashtable
public CustomTOCView(HelpSet hs, String name, String label, Locale locale,
@SuppressWarnings("rawtypes") Hashtable params) {
super(hs, name, label, locale, params);
}
@Override
// overrode this method to install our custom UI, which lets us use our custom renderer
public Component createNavigator(HelpModel model) {
JHelpTOCNavigator helpTOCNavigator = new JHelpTOCNavigator(this, model) {
@Override
public void setUI(HelpNavigatorUI newUI) {
CustomTOCView.this.ui = new CustomTOCNavigatorUI(this);
super.setUI(CustomTOCView.this.ui);
}
};
return helpTOCNavigator;
}
public HelpModel getHelpModel() {
return ui.getHelpModel();
}
@Override
// overrode this method to install our custom factory
public DefaultMutableTreeNode getDataAsTree() {
DefaultMutableTreeNode superNode = super.getDataAsTree();
if (superNode.getChildCount() == 0) {
return superNode; // something is not initialized
}
@SuppressWarnings("rawtypes")
Hashtable viewParameters = getParameters();
String TOCData = (String) viewParameters.get("data");
HelpSet helpSet = getHelpSet();
URL helpSetURL = helpSet.getHelpSetURL();
URL url;
try {
url = new URL(helpSetURL, TOCData);
}
catch (Exception ex) {
throw new Error("Unable to create tree for view data: " + ex);
}
return parse(url, helpSet, helpSet.getLocale(), new CustomDefaultTOCFactory(), this);
}
/**
* Our custom factory that knows how to look for extra XML attributes and how to
* create our custom tree items
*/
public static class CustomDefaultTOCFactory extends DefaultTOCFactory {
@Override
public TreeItem createItem(String tagName, @SuppressWarnings("rawtypes") Hashtable atts,
HelpSet hs, Locale locale) {
try {
return doCreateItem(tagName, atts, hs, locale);
}
catch (Exception e) {
Msg.error(this, "Unexected error creating a TOC item", e);
throw new RuntimeException("Unexpected error creating a TOC item", e);
}
}
private TreeItem doCreateItem(String tagName, @SuppressWarnings("rawtypes") Hashtable atts,
HelpSet hs, Locale locale) {
TreeItem item = super.createItem(tagName, atts, hs, locale);
CustomTreeItemDecorator newItem = new CustomTreeItemDecorator((TOCItem) item);
if (atts != null) {
String displayText = (String) atts.get("display");
newItem.setDisplayText(displayText);
String tocID = (String) atts.get("toc_id");
newItem.setTocID(tocID);
}
return newItem;
}
}
/**
* Our hook to install our custom cell renderer.
*/
class CustomTOCNavigatorUI extends BasicTOCNavigatorUI {
public CustomTOCNavigatorUI(JHelpTOCNavigator b) {
super(b);
}
@Override
public void installUI(JComponent c) {
super.installUI(c);
tree.setExpandsSelectedPaths(true);
}
@Override
protected void setCellRenderer(NavigatorView view, JTree tree) {
Map map = view.getHelpSet().getCombinedMap();
tree.setCellRenderer(new CustomCellRenderer(map, (TOCView) view));
ToolTipManager.sharedInstance().registerComponent(tree);
}
public HelpModel getHelpModel() {
JHelpNavigator helpNavigator = getHelpNavigator();
return helpNavigator.getModel();
}
// Overridden to change the value used for the 'historyName', which we want to be our
// display name and not the item's name
@Override
public void valueChanged(TreeSelectionEvent e) {
if (isSelectingNodeInternally) {
// ignore our own selection events, as this method will get called twice if we don't
return;
}
JHelpNavigator navigator = getHelpNavigator();
HelpModel helpModel = navigator.getModel();
TreeItem treeItem = getSelectedItem(e, navigator);
if (treeItem == null) {
return; // nothing selected
}
TOCItem item = (TOCItem) treeItem;
ID itemID = item.getID();
if (itemID == null) {
Msg.debug(this, "No help ID for " + item);
return;
}
String presentation = item.getPresentation();
if (presentation != null) {
return; // don't currently support presentations
}
CustomTreeItemDecorator customItem = (CustomTreeItemDecorator) item;
String customDisplayText = customItem.getDisplayText();
try {
helpModel.setCurrentID(itemID, customDisplayText, navigator);
}
catch (InvalidHelpSetContextException ex) {
Msg.error(this, "Exception setting new help item ID", ex);
}
}
private TOCItem getSelectedItem(TreeSelectionEvent e, JHelpNavigator navigator) {
TreePath newLeadSelectionPath = e.getNewLeadSelectionPath();
if (newLeadSelectionPath == null) {
navigator.setSelectedItems(null);
return null;
}
DefaultMutableTreeNode node =
(DefaultMutableTreeNode) newLeadSelectionPath.getLastPathComponent();
TOCItem treeItem = (TOCItem) node.getUserObject();
navigator.setSelectedItems(new TreeItem[] { treeItem });
return treeItem;
}
// Overridden to try to find a parent file for IDs that are based upon anchors within
// a file
@Override
public synchronized void idChanged(HelpModelEvent e) {
selectNodeForID(e.getURL(), e.getID());
}
private void selectNodeForID(URL url, ID ID) {
if (ID == null) {
ID = getClosestID(url);
}
TreePath path = tree.getSelectionPath();
if (isAlreadySelected(path, ID)) {
return;
}
DefaultMutableTreeNode node = getNodeForID(topNode, ID);
if (node != null) {
isSelectingNodeInternally = true;
TreePath newPath = new TreePath(node.getPath());
tree.setSelectionPath(newPath);
tree.scrollPathToVisible(newPath);
isSelectingNodeInternally = false;
return;
}
// See if the given ID is based upon a URL with an anchor. If that is the case, then
// there may be a node for the parent file of that URL. In that case, select the
// parent file.
if (url == null) {
clearSelection();
return;
}
String urlString = url.toExternalForm();
int anchorIndex = urlString.indexOf('#');
if (anchorIndex < 0) {
clearSelection();
return;
}
urlString = urlString.substring(0, anchorIndex);
try {
URL newURL = new URL(urlString);
selectNodeForID(newURL, null);
}
catch (MalformedURLException e) {
// shouldn't happen, as we are starting with a valid URL
Msg.debug(this,
"Unexpected error create a help URL from an existing URL: " + urlString, e);
}
}
private ID getClosestID(URL url) {
HelpModel helpModel = toc.getModel();
HelpSet helpSet = helpModel.getHelpSet();
Map combinedMap = helpSet.getCombinedMap();
return combinedMap.getClosestID(url);
}
private boolean isAlreadySelected(TreePath path, ID id) {
if (path == null) {
return false;
}
Object pathComponent = path.getLastPathComponent();
if (!(pathComponent instanceof DefaultMutableTreeNode)) {
return false;
}
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) pathComponent;
TOCItem item = (TOCItem) treeNode.getUserObject();
if (item == null) {
return false;
}
ID selectedID = item.getID();
return selectedID != null && selectedID.equals(id);
}
private DefaultMutableTreeNode getNodeForID(DefaultMutableTreeNode node, ID ID) {
if (ID == null) {
return null;
}
if (isNodeID(node, ID)) {
return node;
}
int childCount = node.getChildCount();
for (int i = 0; i < childCount; i++) {
DefaultMutableTreeNode matchingNode =
getNodeForID((DefaultMutableTreeNode) node.getChildAt(i), ID);
if (matchingNode != null) {
return matchingNode;
}
}
return null;
}
private boolean isNodeID(DefaultMutableTreeNode node, ID ID) {
Object userObject = node.getUserObject();
if (!(userObject instanceof TOCItem)) {
return false;
}
TOCItem item = (TOCItem) userObject;
ID nodeID = item.getID();
if (nodeID == null) {
return false;
}
return nodeID.equals(ID);
}
private void clearSelection() {
isSelectingNodeInternally = true;
tree.clearSelection();
isSelectingNodeInternally = false;
}
}
static class CustomCellRenderer extends BasicTOCCellRenderer {
public CustomCellRenderer(Map map, TOCView view) {
super(map, view);
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
boolean expanded, boolean leaf, int row, boolean isFocused) {
CustomCellRenderer renderer =
(CustomCellRenderer) super.getTreeCellRendererComponent(tree, value, sel, expanded,
leaf, row, isFocused);
TOCItem item = (TOCItem) ((DefaultMutableTreeNode) value).getUserObject();
if (item == null) {
return renderer;
}
CustomTreeItemDecorator customItem = (CustomTreeItemDecorator) item;
renderer.setText(customItem.getDisplayText());
if (!SystemUtilities.isInDevelopmentMode()) {
return renderer;
}
URL url = customItem.getURL();
if (url != null) {
renderer.setToolTipText(url.toExternalForm());
return renderer;
}
ID id = customItem.getID();
if (id != null) {
renderer.setToolTipText("Missing Help - " + id.id + " in '" + id.hs + "' help set");
return renderer;
}
// this can happen if there is no 'target' attribute in the TOC
// (see TOCView.createItem())
return renderer;
}
}
/**
* A custom tree item that allows us to store and retrieve custom attributes that we parsed
* from the TOC xml document.
*/
public static class CustomTreeItemDecorator extends javax.help.TOCItem {
private final TOCItem wrappedItem;
private String displayText;
private String tocID;
private URL cachedURL;
public CustomTreeItemDecorator(javax.help.TOCItem wrappedItem) {
super(wrappedItem.getID(), wrappedItem.getImageID(), wrappedItem.getHelpSet(),
wrappedItem.getLocale());
this.wrappedItem = wrappedItem;
}
void setDisplayText(String text) {
this.displayText = text;
}
public String getDisplayText() {
return displayText;
}
void setTocID(String tocID) {
this.tocID = tocID;
}
public String getTocID() {
return tocID;
}
@Override
public boolean equals(Object obj) {
return wrappedItem.equals(obj);
}
@Override
public int getExpansionType() {
return wrappedItem.getExpansionType();
}
@Override
public HelpSet getHelpSet() {
return wrappedItem.getHelpSet();
}
@Override
public ID getID() {
return wrappedItem.getID();
}
@Override
public ID getImageID() {
return wrappedItem.getImageID();
}
@Override
public Locale getLocale() {
return wrappedItem.getLocale();
}
@Override
public String getMergeType() {
return wrappedItem.getMergeType();
}
@Override
public String getName() {
return wrappedItem.getName();
}
@Override
public String getPresentation() {
return wrappedItem.getPresentation();
}
@Override
public String getPresentationName() {
return wrappedItem.getPresentationName();
}
@Override
public URL getURL() {
if (cachedURL == null) {
cachedURL = wrappedItem.getURL();
}
return cachedURL;
}
@Override
public int hashCode() {
return wrappedItem.hashCode();
}
@Override
public void setExpansionType(int type) {
wrappedItem.setExpansionType(type);
}
@Override
public void setHelpSet(HelpSet hs) {
wrappedItem.setHelpSet(hs);
}
@Override
public void setID(ID id) {
wrappedItem.setID(id);
}
@Override
public void setMergeType(String mergeType) {
wrappedItem.setMergeType(mergeType);
}
@Override
public void setName(String name) {
wrappedItem.setName(name);
}
@Override
public void setPresentation(String presentation) {
wrappedItem.setPresentation(presentation);
}
@Override
public void setPresentationName(String presentationName) {
wrappedItem.setPresentationName(presentationName);
}
@Override
public String toString() {
return displayText;
}
}
}

View file

@ -0,0 +1,331 @@
/* ###
* 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 help;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import javax.help.*;
import javax.swing.*;
import javax.swing.text.Document;
import ghidra.util.Msg;
import ghidra.util.bean.GGlassPane;
import resources.ResourceManager;
// NOTE: for JH 2.0, this class has been rewritten to not
// access the 'frame' and 'dialog' variable directly
/**
* Ghidra help broker that displays the help set; sets the application icon on the help frame and
* attempts to maintain the user window size.
*/
public class GHelpBroker extends DefaultHelpBroker {
// Create the zoom in/out icons that will be added to the default jHelp toolbar.
private static final ImageIcon ZOOM_OUT_ICON =
ResourceManager.loadImage("images/list-remove.png");
private static final ImageIcon ZOOM_IN_ICON = ResourceManager.loadImage("images/list-add.png");
private Dimension windowSize = new Dimension(1100, 700);
protected JEditorPane htmlEditorPane;
private Window activationWindow;
/**
* Construct a new GhidraHelpBroker.
* @param hs java help set associated with this help broker
*/
public GHelpBroker(HelpSet hs) {
super(hs);
}
@Override
// Overridden so that we can call the preferred version of setCurrentURL on the HelpModel,
// which fixes a bug with the history list (SCR 7639)
public void setCurrentURL(final URL URL) {
HelpModel model = getCustomHelpModel();
if (model != null) {
model.setCurrentURL(URL, getHistoryName(URL), null);
}
else {
super.setCurrentURL(URL);
}
}
protected List<Image> getApplicationIcons() {
return null;
}
protected HelpModel getCustomHelpModel() {
return null;
}
/* Perform some shenanigans to force Java Help to reload the given URL */
protected void reloadHelpPage(URL url) {
clearContentViewer();
showNavigationAid(url);
try {
htmlEditorPane.setPage(url);
}
catch (IOException e) {
Msg.error(this, "Unexpected error loading help page: " + url, e);
}
}
protected void showNavigationAid(URL url) {
// this base class does not have a navigation aid
}
private void clearContentViewer() {
htmlEditorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null);
}
private JEditorPane getHTMLEditorPane(JHelpContentViewer contentViewer) {
//
// Intimate Knowledge - construction of the viewer:
//
// -BorderLayout
// -JScrollPane
// -Viewport
// -JHEditorPane extends JEditorPane
//
//
Component[] components = contentViewer.getComponents();
JScrollPane scrollPane = (JScrollPane) components[0];
JViewport viewport = scrollPane.getViewport();
return (JEditorPane) viewport.getView();
}
@Override
public void setDisplayed(boolean b) {
if (!b) {
super.setDisplayed(b);
return;
}
// this must be before any call that triggers the help system to create its window
initializeScreenDevice();
WindowPresentation windowPresentation = getWindowPresentation();
updateWindowSize(windowPresentation);
// this has to be before getHelpWindow() or the value returned will be null
super.setDisplayed(b);
initializeUIWindowPresentation(windowPresentation);
}
private void initializeScreenDevice() {
if (isInitialized()) {
return;
}
if (activationWindow == null) {
// This can happen when we show the 'What's New' help page on a fresh install. In
// that case, we were not activated from an existing window, thus, there may
// be no parent window.
return;
}
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] gs = ge.getScreenDevices();
GraphicsConfiguration config = activationWindow.getGraphicsConfiguration();
GraphicsDevice parentDevice = config.getDevice();
for (int i = 0; i < gs.length; i++) {
if (gs[i] == parentDevice) {
// update the help window's screen to match that of the parent
setScreen(i);
}
}
}
private void initializeUIWindowPresentation(WindowPresentation windowPresentation) {
Window helpWindow = windowPresentation.getHelpWindow();
Container contentPane = null;
if (helpWindow instanceof JFrame) {
JFrame frame = (JFrame) helpWindow;
installRootPane(frame);
List<Image> icons = getApplicationIcons();
if (icons != null) {
frame.setIconImages(icons);
}
contentPane = frame.getContentPane();
}
else if (helpWindow instanceof JDialog) {
JDialog dialog = (JDialog) helpWindow;
installRootPane(dialog);
contentPane = dialog.getContentPane();
}
initializeUIComponents(contentPane);
}
private boolean isInitialized() {
return htmlEditorPane != null;
}
private void initializeUIComponents(Container contentPane) {
if (isInitialized()) {
return;// already initialized
}
Component[] components = contentPane.getComponents();
JHelp jHelp = (JHelp) components[0];
addCustomToolbarItems(jHelp);
JHelpContentViewer contentViewer = jHelp.getContentViewer();
htmlEditorPane = getHTMLEditorPane(contentViewer);
// just creating the search wires everything together
HelpModel helpModel = getCustomHelpModel();
installHelpSearcher(jHelp, helpModel);
if (helpModel != null) {
installHelpSearcher(jHelp, helpModel);
}
installActions(jHelp);
}
protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) {
// this base class does not provide an in-page search feature
}
/**
* Create zoom in/out buttons on the default help window toolbar.
* @param jHelp the java help object used to retrieve the help components
*/
protected void addCustomToolbarItems(final JHelp jHelp) {
for (Component component : jHelp.getComponents()) {
if (component instanceof JToolBar) {
JToolBar toolbar = (JToolBar) component;
toolbar.addSeparator();
ImageIcon zoomOutIcon = ResourceManager.getScaledIcon(ZOOM_OUT_ICON, 24, 24);
JButton zoomOutBtn = new JButton(zoomOutIcon);
zoomOutBtn.setToolTipText("Zoom out");
zoomOutBtn.addActionListener(e -> {
GHelpHTMLEditorKit.zoomOut();
// Need to reload the page to force the scroll panes to resize properly. A
// simple revalidate/repaint won't do it.
reloadHelpPage(getCurrentURL());
});
toolbar.add(zoomOutBtn);
ImageIcon zoomInIcon = ResourceManager.getScaledIcon(ZOOM_IN_ICON, 24, 24);
JButton zoomInBtn = new JButton(zoomInIcon);
zoomInBtn.setToolTipText("Zoom in");
zoomInBtn.addActionListener(e -> {
GHelpHTMLEditorKit.zoomIn();
// Need to reload the page to force the scroll panes to resize properly. A
// simple revalidate/repaint won't do it.
reloadHelpPage(getCurrentURL());
});
toolbar.add(zoomInBtn);
// Once we've found the toolbar we can break out of the loop and stop looking for it.
break;
}
}
}
protected void installActions(JHelp help) {
// subclasses may have actions
}
private String getHistoryName(URL URL) {
String text = URL.getFile();
int index = text.lastIndexOf('/');
if (index != -1) {
// we want just the filename
text = text.substring(index + 1);
}
String ref = URL.getRef();
if (ref != null) {
text += " - " + ref;
}
return text;
}
private void installRootPane(JFrame frame) {
Component oldGlassPane = frame.getGlassPane();
if (!(oldGlassPane instanceof GGlassPane)) {
GGlassPane gGlassPane = new GGlassPane();
frame.setGlassPane(gGlassPane);
gGlassPane.setVisible(true);
}
}
private void installRootPane(JDialog dialog) {
Component oldGlassPane = dialog.getGlassPane();
if (!(oldGlassPane instanceof GGlassPane)) {
GGlassPane gGlassPane = new GGlassPane();
dialog.setGlassPane(gGlassPane);
gGlassPane.setVisible(true);
}
}
private void updateWindowSize(WindowPresentation presentation) {
if (windowSize == null) {
return;
}
presentation.createHelpWindow();
presentation.setSize(windowSize);
}
@Override
public void setActivationWindow(Window window) {
WindowPresentation windowPresentation = getWindowPresentation();
Window helpWindow = windowPresentation.getHelpWindow();
if (helpWindow == null) {
activationWindow = window;
super.setActivationWindow(window);
return;
}
windowSize = helpWindow.getSize();// remember the previous size
boolean wasModal = isModalWindow(helpWindow);
boolean willBeModal = isModalWindow(window);
if (!wasModal && willBeModal) {
// in this condition, a new window will be shown, but the old one is not properly
// closed by JavaHelp
helpWindow.setVisible(false);
}
super.setActivationWindow(window);
}
private boolean isModalWindow(Window window) {
if (window instanceof Dialog) {
Dialog dialog = (Dialog) window;
if (dialog.isModal()) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,538 @@
/* ###
* 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 help;
import java.awt.Desktop;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.net.*;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.ImageIcon;
import javax.swing.JEditorPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.text.html.HTML.Tag;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.framework.preferences.Preferences;
import ghidra.util.Msg;
import resources.*;
import utilities.util.FileUtilities;
/**
* A class that allows Ghidra to intercept JavaHelp navigation events in order to resolve them
* to Ghidra's help system. Without this class, contribution plugins have no way of
* referencing help documents within Ghidra's default help location.
* <p>
* This class is currently installed by the {@link GHelpSet}.
*
* @see GHelpSet
*/
public class GHelpHTMLEditorKit extends HTMLEditorKit {
private static final String G_HELP_STYLE_SHEET = "help/shared/Frontpage.css";
private static final Pattern EXTERNAL_URL_PATTERN = Pattern.compile("https?://.*");
/** A pattern to strip the font size value from a line of CSS */
private static final Pattern FONT_SIZE_PATTERN = Pattern.compile("font-size:\\s*(\\d{1,2})");
private static final String HELP_WINDOW_ZOOM_FACTOR = "HELP.WINDOW.FONT.SIZE.MODIFIER";
private static int fontSizeModifier;
private HyperlinkListener[] delegateListeners = null;
private HyperlinkListener resolverHyperlinkListener;
public GHelpHTMLEditorKit() {
fontSizeModifier =
Integer.valueOf(Preferences.getProperty(HELP_WINDOW_ZOOM_FACTOR, "0", true));
}
@Override
public ViewFactory getViewFactory() {
return new GHelpHTMLFactory();
}
@Override
public void install(JEditorPane c) {
super.install(c);
delegateListeners = c.getHyperlinkListeners();
for (HyperlinkListener listener : delegateListeners) {
c.removeHyperlinkListener(listener);
}
resolverHyperlinkListener = new ResolverHyperlinkListener();
c.addHyperlinkListener(resolverHyperlinkListener);
// add a listener to report trace information
c.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if ("page".equals(propertyName)) {
Msg.trace(this, "Page loaded: " + evt.getNewValue());
}
}
});
}
@Override
public void deinstall(JEditorPane c) {
c.removeHyperlinkListener(resolverHyperlinkListener);
for (HyperlinkListener listener : delegateListeners) {
c.addHyperlinkListener(listener);
}
super.deinstall(c);
}
private class ResolverHyperlinkListener implements HyperlinkListener {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (delegateListeners == null) {
return;
}
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
if (isExternalLink(e)) {
browseExternalLink(e);
return;
}
Msg.trace(this, "Link activated: " + e.getURL());
e = validateURL(e);
Msg.trace(this, "Validated event: " + e.getURL());
}
for (HyperlinkListener listener : delegateListeners) {
listener.hyperlinkUpdate(e);
}
}
}
private boolean isExternalLink(HyperlinkEvent e) {
String description = e.getDescription();
return description != null && EXTERNAL_URL_PATTERN.matcher(description).matches();
}
private void browseExternalLink(HyperlinkEvent e) {
String description = e.getDescription();
if (!Desktop.isDesktopSupported()) {
Msg.info(this, "Unable to launch external browser for " + description);
return;
}
try {
// use an external browser
URI uri = e.getURL().toURI();
Desktop.getDesktop().browse(uri);
}
catch (URISyntaxException | IOException e1) {
Msg.error(this, "Error browsing to external URL " + description, e1);
}
}
/**
* Tests the URL of the given event. If the URL is invalid, a new event may be created if
* a new, valid URL can be created. Creates a new event with a patched URL if
* the given event's URL is invalid.
*/
private HyperlinkEvent validateURL(HyperlinkEvent event) {
URL url = event.getURL();
try {
url.openStream();// assume that this will fail if the file does not exist
}
catch (IOException ioe) {
// assume this means that the url is invalid
Msg.trace(this, "URL of link is invalid: " + url.toExternalForm());
return maybeCreateNewHyperlinkEventWithUpdatedURL(event);
}
return event;// url is fine
}
/** Generates a new event with a URL based upon Ghidra's resources if needed. */
private HyperlinkEvent maybeCreateNewHyperlinkEventWithUpdatedURL(HyperlinkEvent event) {
Element element = event.getSourceElement();
if (element == null) {
return event;// this shouldn't happen since we were triggered from an A tag
}
AttributeSet a = element.getAttributes();
AttributeSet anchor = (AttributeSet) a.getAttribute(HTML.Tag.A);
if (anchor == null) {
return event;// this shouldn't happen since we were triggered from an A tag
}
String HREF = (String) anchor.getAttribute(HTML.Attribute.HREF);
Msg.trace(this, "HREF of <a> tag: " + HREF);
URL newUrl = getURLForHREFFromResources(HREF);
if (newUrl == null) {
return event;// unable to locate a resource by the name--bad link!
}
return new HyperlinkEvent(event.getSource(), event.getEventType(), newUrl,
event.getDescription(), event.getSourceElement());
}
private URL getURLForHREFFromResources(String originalHREF) {
int anchorIndex = originalHREF.indexOf("#");
String HREF = originalHREF;
String anchor = null;
if (anchorIndex != -1) {
HREF = HREF.substring(0, anchorIndex);
anchor = originalHREF.substring(anchorIndex);
}
// look for a URL using an installation environment setup...
URL newUrl = ResourceManager.getResource(HREF);
if (newUrl != null) {
return createURLWithAnchor(newUrl, anchor);
}
//
// The item was not found by the ResourceManager (i.e., it is not in a 'resources'
// directory). See if it may be a relative link to a build's installation root (like
// a file in <install dir>/docs).
//
newUrl = findApplicationfile(HREF);
return newUrl;
}
private URL createURLWithAnchor(URL anchorlessURL, String anchor) {
if (anchorlessURL == null) {
return anchorlessURL;
}
if (anchor == null) {
// nothing to do
return anchorlessURL;
}
try {
// put the anchor back into the URL
return new URL(anchorlessURL, anchor);
}
catch (MalformedURLException e) {
// shouldn't happen, since the file exists
Msg.showError(this, null, "Unexpected Error",
"Unexpected error creating a valid URL: " + anchorlessURL + "#" + anchor);
return null;
}
}
@Override
public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
super.read(in, doc, pos);
HTMLDocument htmlDoc = (HTMLDocument) doc;
loadGHelpStyleSheet(htmlDoc);
}
private void loadGHelpStyleSheet(HTMLDocument doc) {
Reader reader = getGStyleSheetReader();
if (reader == null) {
return;
}
StyleSheet ss = doc.getStyleSheet();
try {
ss.loadRules(reader, null);
}
catch (IOException e) {
// shouldn't happen
Msg.debug(this, "Unable to load help style sheet");
}
}
private Reader getGStyleSheetReader() {
URL url = getGStyleSheetURL();
if (url == null) {
return null;
}
StringBuffer buffy = new StringBuffer();
try {
List<String> lines = FileUtilities.getLines(url);
for (String line : lines) {
changePixels(line, fontSizeModifier, buffy);
buffy.append('\n');
}
}
catch (IOException e) {
// shouldn't happen
Msg.debug(this, "Unable to read the lines of the help style sheet: " + url);
}
StringReader reader = new StringReader(buffy.toString());
return reader;
}
private void changePixels(String line, int amount, StringBuffer buffy) {
Matcher matcher = FONT_SIZE_PATTERN.matcher(line);
while (matcher.find()) {
String oldFontSize = matcher.group(1);
String adjustFontSize = adjustFontSize(oldFontSize);
matcher.appendReplacement(buffy, "font-size: " + adjustFontSize);
}
matcher.appendTail(buffy);
}
private String adjustFontSize(String sizeString) {
try {
int size = Integer.parseInt(sizeString);
String adjusted = Integer.toString(size + fontSizeModifier);
return adjusted;
}
catch (NumberFormatException e) {
Msg.debug(this, "Unable to parse font size string '" + sizeString + "'");
}
return sizeString;
}
private URL getGStyleSheetURL() {
URL GStyleSheetURL = ResourceManager.getResource(G_HELP_STYLE_SHEET);
if (GStyleSheetURL != null) {
return GStyleSheetURL;
}
return findModuleFile("help/shared/FrontPage.css");
}
private URL findApplicationfile(String relativePath) {
ResourceFile installDir = Application.getInstallationDirectory();
ResourceFile file = new ResourceFile(installDir, relativePath);
if (file.exists()) {
try {
return file.toURL();
}
catch (MalformedURLException e) {
Msg.showError(this, null, "Unexpected Error",
"Unexpected error parsing file to URL: " + file);
}
}
return null;
}
private URL findModuleFile(String relativePath) {
Collection<ResourceFile> moduleDirs = Application.getModuleRootDirectories();
for (ResourceFile dir : moduleDirs) {
ResourceFile file = new ResourceFile(dir, relativePath);
if (file.exists()) {
try {
return file.toURL();
}
catch (MalformedURLException e) {
Msg.showError(this, null, "Unexpected Error",
"Unexpected error parsing file to URL: " + file);
return null;
}
}
}
return null;
}
public static void zoomOut() {
fontSizeModifier -= 2;
saveZoomFactor();
}
public static void zoomIn() {
fontSizeModifier += 2;
saveZoomFactor();
}
private static void saveZoomFactor() {
Preferences.setProperty(HELP_WINDOW_ZOOM_FACTOR, Integer.toString(fontSizeModifier));
Preferences.store();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class GHelpHTMLFactory extends HTMLFactory {
@Override
public View create(Element e) {
AttributeSet attributes = e.getAttributes();
Object elementName = attributes.getAttribute(AbstractDocument.ElementNameAttribute);
if (elementName != null) {
// not an HTML element
return super.create(e);
}
Object html = attributes.getAttribute(StyleConstants.NameAttribute);
if (html instanceof HTML.Tag) {
HTML.Tag tag = (Tag) html;
if (tag == HTML.Tag.IMG) {
return new GHelpImageView(e);
}
}
return super.create(e);
}
}
/**
* Overridden to allow us to find images that are defined as constants in places like
* {@link Icons}
*/
private class GHelpImageView extends ImageView {
/*
* Unusual Code Alert!
* This class exists to enable our help system to find custom icons defined in source
* code. The default behavior herein is to supply a URL to the base class to load. This
* works fine.
*
* There is another use case where we wish to have the base class load an image of our
* choosing. Why? Well, we modify, in memory, some icons we use. We do this for things
* like overlays and rotations.
*
* In order to have our base class use the image that we want (and not the one
* it loads via a URL), we have to play a small game. We have to allow the base class
* to load the image it wants, which is done asynchronously. If we install our custom
* image during that process, the loading will throw away the image and not render
* anything.
*
* To get the base class to use our image, we override getImage(). However, we should
* only return our image when the base class is finished loading. (See the base class'
* paint() method for why we need to do this.)
*
* Note: if we start seeing unusual behavior, like images not rendering, or any size
* issues, then we can revert this code.
*/
private Image image;
private float spanX;
private float spanY;
public GHelpImageView(Element elem) {
super(elem);
}
@Override
public Image getImage() {
Image superImage = super.getImage();
if (image == null) {
// no custom image
return superImage;
}
if (isLoading()) {
return superImage;
}
return image;
}
private boolean isLoading() {
return spanX < 1 || spanY < 1;
}
@Override
public float getPreferredSpan(int axis) {
float span = super.getPreferredSpan(axis);
if (axis == View.X_AXIS) {
spanX = span;
}
else {
spanY = span;
}
return span;
}
@Override
public URL getImageURL() {
AttributeSet attributes = getElement().getAttributes();
Object src = attributes.getAttribute(HTML.Attribute.SRC);
if (src == null) {
return null;
}
String srcString = src.toString();
if (isJavaCode(srcString)) {
return installImageFromJavaCode(srcString);
}
URL url = doGetImageURL(srcString);
return url;
}
private URL installImageFromJavaCode(String srcString) {
IconProvider iconProvider = getIconFromJavaCode(srcString);
if (iconProvider == null || iconProvider.isInvalid()) {
return null;
}
ImageIcon imageIcon = iconProvider.getIcon();
this.image = imageIcon.getImage();
URL url = iconProvider.getOrCreateUrl();
return url;
}
private URL doGetImageURL(String srcString) {
HTMLDocument htmlDocument = (HTMLDocument) getDocument();
URL context = htmlDocument.getBase();
try {
URL url = new URL(context, srcString);
if (FileUtilities.exists(url.toURI())) {
// it's a good one, let it through
return url;
}
}
catch (MalformedURLException | URISyntaxException e) {
// check below
}
// Try the ResourceManager. This will work for images that start with GHelp
// relative link syntax such as 'help/', 'help/topics/' and 'images/'
URL resource = ResourceManager.getResource(srcString);
return resource;
}
private boolean isJavaCode(String src) {
// not sure of the best way to handle this--be exact for now
return Icons.isIconsReference(src);
}
private IconProvider getIconFromJavaCode(String src) {
return Icons.getIconForIconsReference(src);
}
}
}

View file

@ -0,0 +1,322 @@
/* ###
* 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 help;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Map.Entry;
import java.util.Set;
import javax.help.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.SystemUtilities;
/**
* Ghidra help set that creates a GhidraHelpBroker, installs some custom HTML handling code via
* the GHelpHTMLEditorKit, and most importantly, changes how the JavaHelp system works with
* regard to integrating Help Sets.
* <p>
* The HelpSet class uses a javax.help.Map object to locate HTML files by javax.help.map.ID objects.
* This class has overridden that basic usage of the Map object to allow ID lookups to take
* place across GHelpSet objects. We need to do this due to how we merge help set content
* across modules. More specifically, in order to merge, we have to make all {@code <tocitem>} xml tags
* the same, including the target HTML file they may reference. Well, when a module uses a
* {@code <tocitem>} tag that references an HTML file <b>not inside of it's module</b>, then JavaHelp
* considers this an error and does not correctly merge the HelpSets that share the reference.
* Further, it does not properly locate the shared HTML file reference. This class allows lookups
* across modules by overridden the lookup functionality done by the map object. More specifically,
* we override {@link #getCombinedMap()} and {@link #getLocalMap()} to use a custom delegate map
* object that knows how do do this "cross-module" help lookup.
*
*
*@see GHelpHTMLEditorKit
*/
public class GHelpSet extends HelpSet {
private static final String HOME_ID = "Misc_Welcome_to_Ghidra_Help";
/** <b>static</b> map that contains all known help sets in the system. */
private static java.util.Map<HelpSet, Map> helpSetsToCombinedMaps = new java.util.HashMap<>();
private static java.util.Map<HelpSet, Map> helpSetsToLocalMaps = new java.util.HashMap<>();
private Logger LOG = LogManager.getLogger(GHelpSet.class);
private GHelpMap combinedMapWrapper;
private GHelpMap localMapWrapper;
public GHelpSet(ClassLoader loader, URL helpset) throws HelpSetException {
super(loader, helpset);
init();
}
private void init() {
// swap in Ghidra's editor kit, which is an overridden version of Java's
String type = "text/html";
String editorKit = GHelpHTMLEditorKit.class.getName();
ClassLoader classLoader = getClass().getClassLoader();
setKeyData(kitTypeRegistry, type, editorKit);
setKeyData(kitLoaderRegistry, type, classLoader);
setHomeID(HOME_ID);
initializeCombinedMapWrapper();
}
@Override
public HelpBroker createHelpBroker() {
return new GHelpBroker(this);
}
@Override
public Map getLocalMap() {
Map localMap = super.getLocalMap();
if (localMap == null) {
return null;
}
initializeLocalMapWrapper();
return localMapWrapper;
}
private void initializeLocalMapWrapper() {
if (localMapWrapper == null) {
Map localMap = super.getLocalMap();
helpSetsToLocalMaps.put(this, localMap);
localMapWrapper = new GHelpMap(localMap);
}
}
@Override
public Map getCombinedMap() {
return combinedMapWrapper;
}
private void initializeCombinedMapWrapper() {
if (combinedMapWrapper == null) {
Map combinedMap = super.getCombinedMap();
helpSetsToCombinedMaps.put(this, combinedMap);
combinedMapWrapper = new GHelpMap(combinedMap);
}
}
//==================================================================================================
// Inner Classes
//==================================================================================================
/** A special class to allow us to handle help ID lookups across help sets */
private class GHelpMap implements Map {
private final Map mapDelegate;
private GHelpMap(Map mapDelegate) {
this.mapDelegate = mapDelegate;
}
@Override
public Enumeration<?> getAllIDs() {
return mapDelegate.getAllIDs();
}
@Override
public ID getClosestID(URL url) {
ID closestID = mapDelegate.getClosestID(url);
if (closestID != null) {
return closestID; // it's in our map
}
LOG.trace("Help Set \"" + GHelpSet.this + "\" does not contain ID for URL: " + url);
Set<Entry<HelpSet, Map>> entrySet = helpSetsToCombinedMaps.entrySet();
for (Entry<HelpSet, Map> entry : entrySet) {
Map map = entry.getValue();
closestID = map.getClosestID(url);
if (closestID != null) {
return closestID;
}
}
LOG.trace("No ID found in any HelpSet for URL: " + url);
return null;
}
@Override
public ID getIDFromURL(URL url) {
return mapDelegate.getIDFromURL(url);
}
@Override
public Enumeration<?> getIDs(URL url) {
return mapDelegate.getIDs(url);
}
@Override
public URL getURLFromID(ID id) throws MalformedURLException {
URL URL = mapDelegate.getURLFromID(id);
if (URL != null) {
return URL; // it's in our map
}
Set<Entry<HelpSet, Map>> entrySet = helpSetsToCombinedMaps.entrySet();
for (Entry<HelpSet, Map> entry : entrySet) {
Map map = entry.getValue();
URL = map.getURLFromID(id);
if (URL != null) {
return URL;
}
}
LOG.trace("No URL found in any HelpSet for ID: " + id);
URL = tryToCreateURLFromID(id.id);
if (URL != null) {
return URL;
}
return null;
}
/**
* This is meant for help files that are not included in the standard help system. Their
* id paths are expected to be relative to the application install directory.
* @param id the help id.
* @return the URL to the help file.
*/
private URL tryToCreateURLFromID(String id) {
URL fileURL = createFileURL(id);
if (fileURL != null) {
return fileURL;
}
URL rawURL = createRawURL(id);
return rawURL;
}
private URL createRawURL(String id) {
URL url = null;
try {
url = new URL(id);
}
catch (MalformedURLException e) {
LOG.trace("ID is not a URL; tried to make URL from string: " + id);
return null;
}
try {
InputStream inputStream = url.openStream();
inputStream.close();
return url; // it is valid
}
catch (IOException e) {
LOG.trace("ID is not a URL; unable to read URL: " + url);
}
return null;
}
private URL createFileURL(String id) {
ResourceFile helpFile = fileFromID(id);
if (!helpFile.exists()) {
LOG.trace("ID is not a file; tried: " + helpFile);
return null;
}
try {
return helpFile.toURL();
}
catch (MalformedURLException e) {
// this shouldn't happen, as the file exists
LOG.trace("ID is not a URL; tried to make URL from file: " + helpFile);
}
return null;
}
private ResourceFile fileFromID(String id) {
// this allows us to find files by using relative paths (e.g., 'docs/WhatsNew.html'
// will get resolved relative to the installation directory in a build).
ResourceFile installDir = Application.getInstallationDirectory();
ResourceFile helpFile = new ResourceFile(installDir, id);
return helpFile;
}
@Override
public boolean isID(URL url) {
return mapDelegate.isID(url);
}
@Override
public boolean isValidID(String id, HelpSet hs) {
HelpService service = Help.getHelpService();
if (!service.helpExists()) {
// Treat everything as valid until all help is loaded, otherwise, we
// can't be sure that when something is missing, it is just not yet merged in.
return true;
}
boolean isValid = mapDelegate.isValidID(id, hs);
if (isValid) {
return true;
}
Set<Entry<HelpSet, Map>> entrySet = helpSetsToCombinedMaps.entrySet();
for (Entry<HelpSet, Map> entry : entrySet) {
Map map = entry.getValue();
if (map.isValidID(id, hs)) {
return true;
}
}
// This can happen for help files that are generated during the build,
// such as 'What's New'; return true here so the values will still be loaded into
// the help system; handle the error condition later.
if (ignoreExternalHelp(id)) {
return true;
}
return false;
}
private boolean ignoreExternalHelp(String id) {
if (id.startsWith("help/topics")) {
return false; // not external help location
}
URL url = tryToCreateURLFromID(id);
if (url != null) {
return true; // ignore this id; it is valid
}
// no url for ID
if (SystemUtilities.isInDevelopmentMode()) {
// ignore external files that do not exist in dev mode
return true;
}
return false;
}
}
}

View file

@ -0,0 +1,42 @@
/* ###
* 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 help;
import docking.DefaultHelpService;
/**
* Creates the HelpManager for the application. This is just a glorified global variable for
* the application.
*/
public class Help {
private static HelpService helpService = new DefaultHelpService();
/**
* Get the help service
*
* @return null if the call to setMainHelpSetURL() failed
*/
public static HelpService getHelpService() {
return helpService;
}
// allows help services to install themselves
public static void installHelpService(HelpService service) {
helpService = service;
}
}

View file

@ -0,0 +1,32 @@
/* ###
* 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 help;
public interface HelpDescriptor {
/**
* Returns the object for which help locations are defined. This may be the implementor of
* this interface or some other delegate object.
* @return the help object
*/
public Object getHelpObject();
/**
* Returns a descriptive String about the help object that this descriptor represents.
* @return the help info
*/
public String getHelpInfo();
}

View file

@ -0,0 +1,116 @@
/* ###
* 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 help;
import java.awt.Component;
import java.net.URL;
import ghidra.util.HelpLocation;
/**
* <code>HelpService</code> defines a service for displaying Help content by an ID or URL.
*/
public interface HelpService {
public static final String DUMMY_HELP_SET_NAME = "Dummy_HelpSet.hs";
/**
* Display the Help content identified by the help object.
*
* @param helpObject the object to which help was previously registered
* @param infoOnly display {@link HelpLocation} information only, not the help UI
* @param parent requesting component
*
* @see #registerHelp(Object, HelpLocation)
*/
public void showHelp(Object helpObject, boolean infoOnly, Component parent);
/**
* Display the help page for the given URL. This is a specialty method for displaying
* help when a specific file is desired, like an introduction page. Showing help for
* objects within the system is accomplished by calling
* {@link #showHelp(Object, boolean, Component)}.
*
* @param url the URL to display
* @see #showHelp(Object, boolean, Component)
*/
public void showHelp(URL url);
/**
* Display the help page for the given help location.
*
* @param location the location to display.
* @see #showHelp(Object, boolean, Component)
*/
public void showHelp(HelpLocation location);
/**
* Signals to the help system to ignore the given object when searching for and validating
* help. Once this method has been called, no help can be registered for the given object.
*
* @param helpObject the object to exclude from the help system.
*/
public void excludeFromHelp(Object helpObject);
/**
* Returns true if the given object is meant to be ignored by the help system
*
* @param helpObject the object to check
* @return true if ignored
* @see #excludeFromHelp(Object)
*/
public boolean isExcludedFromHelp(Object helpObject);
/**
* Register help for a specific object.
*
* <P>Do not call this method will a <code>null</code> help location. Instead, to signal that
* an item has no help, call {@link #excludeFromHelp(Object)}.
*
* @param helpObject the object to associate the specified help location with
* @param helpLocation help content location
*/
public void registerHelp(Object helpObject, HelpLocation helpLocation);
/**
* Removes this object from the help system. This method is useful, for example,
* when a single Java {@link Component} will have different help locations
* assigned over its lifecycle.
*
* @param helpObject the object for which to clear help
*/
public void clearHelp(Object helpObject);
/**
* Returns the registered (via {@link #registerHelp(Object, HelpLocation)} help
* location for the given object; null if there is no registered
* help.
*
* @param object The object for which to find a registered HelpLocation.
* @return the registered HelpLocation
* @see #registerHelp(Object, HelpLocation)
*/
public HelpLocation getHelpLocation(Object object);
/**
* Returns true if the help system has been initialized properly; false if help does not
* exist or is not working.
*
* @return true if the help system has found the applications help content and has finished
* initializing
*/
public boolean helpExists();
}

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,22 +15,14 @@
*/
package help;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class JavaHelpSetBuilder {
private static final String TAB = "\t";
private static final Set<String> searchFileNames =
new HashSet<String>(Arrays.asList(new String[] { "DOCS", "DOCS.TAB", "OFFSETS",
"POSITIONS", "SCHEMA", "TMAP" }));
private static final Set<String> searchFileNames = new HashSet<String>(Arrays
.asList(new String[] { "DOCS", "DOCS.TAB", "OFFSETS", "POSITIONS", "SCHEMA", "TMAP" }));
private static int indentionLevel;
private final String moduleName;
@ -97,7 +88,7 @@ public class JavaHelpSetBuilder {
indentionLevel++;
writeLine("<name>TOC</name>", writer);
writeLine("<label>Ghidra Table of Contents</label>", writer);
writeLine("<type>docking.help.CustomTOCView</type>", writer);
writeLine("<type>" + CustomTOCView.class.getName() + "</type>", writer);
writeLine("<data>" + helpSetTOCFile.getFileName() + "</data>", writer);
indentionLevel--;
@ -116,7 +107,7 @@ public class JavaHelpSetBuilder {
writeLine("<name>Search</name>", writer);
writeLine("<label>Search for Keywords</label>", writer);
// writeLine("<type>javax.help.SearchView</type>", writer);
writeLine("<type>docking.help.CustomSearchView</type>", writer);
writeLine("<type>" + CustomSearchView.class.getName() + "</type>", writer);
if (hasIndexerFiles(helpSearchDirectory)) {
writeLine("<data engine=\"com.sun.java.help.search.DefaultSearchEngine\">" +
@ -148,7 +139,7 @@ public class JavaHelpSetBuilder {
// writeLine( "<label>Favorites</label>", writer );
// writeLine( "<type>javax.help.FavoritesView</type>", writer );
writeLine("<label>Ghidra Favorites</label>", writer);
writeLine("<type>docking.help.CustomFavoritesView</type>", writer);
writeLine("<type>" + CustomFavoritesView.class.getName() + "</type>", writer);
indentionLevel--;
writeLine("</view>", writer);
@ -158,8 +149,8 @@ public class JavaHelpSetBuilder {
throws IOException {
writer.write("<?xml version='1.0' encoding='ISO-8859-1' ?>");
writer.newLine();
writer.write("<!DOCTYPE helpset PUBLIC \"-//Sun Microsystems Inc.//DTD JavaHelp "
+ "HelpSet Version 2.0//EN\" \"http://java.sun.com/products/javahelp/helpset_2_0.dtd\">");
writer.write("<!DOCTYPE helpset PUBLIC \"-//Sun Microsystems Inc.//DTD JavaHelp " +
"HelpSet Version 2.0//EN\" \"http://java.sun.com/products/javahelp/helpset_2_0.dtd\">");
writer.newLine();
writer.newLine();

View file

@ -26,9 +26,9 @@ import javax.help.Map.ID;
import javax.help.TOCView;
import javax.swing.tree.DefaultMutableTreeNode;
import docking.help.CustomTOCView.CustomTreeItemDecorator;
import help.HelpBuildUtils;
import help.TOCItemProvider;
import help.CustomTOCView.CustomTreeItemDecorator;
import help.validator.model.*;
/**

View file

@ -26,8 +26,8 @@ import java.util.regex.Pattern;
import javax.help.HelpSet;
import javax.help.HelpSetException;
import docking.help.GHelpSet;
import ghidra.util.exception.AssertException;
import help.GHelpSet;
import help.validator.model.GhidraTOCFile;
public class JarHelpModuleLocation extends HelpModuleLocation {