mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 02:09:44 +02:00
Updated module system so Help no longer depends on Docking. Docking can now have help content.
This commit is contained in:
parent
a438a1e1ea
commit
cb02db8313
87 changed files with 707 additions and 445 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
524
Ghidra/Framework/Help/src/main/java/help/CustomTOCView.java
Normal file
524
Ghidra/Framework/Help/src/main/java/help/CustomTOCView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
331
Ghidra/Framework/Help/src/main/java/help/GHelpBroker.java
Normal file
331
Ghidra/Framework/Help/src/main/java/help/GHelpBroker.java
Normal 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;
|
||||
}
|
||||
}
|
538
Ghidra/Framework/Help/src/main/java/help/GHelpHTMLEditorKit.java
Normal file
538
Ghidra/Framework/Help/src/main/java/help/GHelpHTMLEditorKit.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
322
Ghidra/Framework/Help/src/main/java/help/GHelpSet.java
Normal file
322
Ghidra/Framework/Help/src/main/java/help/GHelpSet.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
42
Ghidra/Framework/Help/src/main/java/help/Help.java
Normal file
42
Ghidra/Framework/Help/src/main/java/help/Help.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
32
Ghidra/Framework/Help/src/main/java/help/HelpDescriptor.java
Normal file
32
Ghidra/Framework/Help/src/main/java/help/HelpDescriptor.java
Normal 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();
|
||||
}
|
116
Ghidra/Framework/Help/src/main/java/help/HelpService.java
Normal file
116
Ghidra/Framework/Help/src/main/java/help/HelpService.java
Normal 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();
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.*;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue