Updated module system so Help no longer depends on Docking. Docking can now have help content.
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/helpProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
|
||||
apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
|
||||
|
@ -24,12 +25,8 @@ eclipse.project.name = 'Framework Docking'
|
|||
|
||||
dependencies {
|
||||
api project(':Generic')
|
||||
api 'net.java.dev.timingframework:timingframework:1.0'
|
||||
|
||||
// Only include this debug version of the jh library if necessary.
|
||||
//api name:'jh2.with.debug'
|
||||
api 'javax.help:javahelp:2.0.05'
|
||||
|
||||
api project(':Help')
|
||||
|
||||
|
||||
// include code from src/test in Generic
|
||||
testImplementation project(path: ':Generic', configuration: 'testArtifacts')
|
||||
|
|
|
@ -10,6 +10,18 @@
|
|||
##MODULE IP: Tango Icons - Public Domain
|
||||
Module.manifest||GHIDRA||||END|
|
||||
data/ExtensionPoint.manifest||GHIDRA||||END|
|
||||
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
|
||||
src/main/help/help/shared/arrow.gif||GHIDRA||||END|
|
||||
src/main/help/help/shared/close16.gif||GHIDRA||||END|
|
||||
src/main/help/help/shared/menu16.gif||GHIDRA||||END|
|
||||
src/main/help/help/shared/note-red.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
src/main/help/help/shared/note.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
src/main/help/help/shared/note.yellow.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
src/main/help/help/shared/redo.png||GHIDRA||||END|
|
||||
src/main/help/help/shared/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
src/main/help/help/shared/undo.png||GHIDRA||||END|
|
||||
src/main/help/help/shared/warning.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
|
||||
src/main/help/help/topics/PlacheholderTopic/Placeholder.htm||GHIDRA||||END|
|
||||
src/main/java/docking/dnd/package.html||GHIDRA||reviewed||END|
|
||||
src/main/java/docking/options/editor/package.html||GHIDRA||reviewed||END|
|
||||
src/main/java/docking/widgets/fieldpanel/package.html||GHIDRA||reviewed||END|
|
||||
|
|
54
Ghidra/Framework/Docking/src/main/help/help/TOC_Source.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version='1.0' encoding='ISO-8859-1' ?>
|
||||
<!--
|
||||
|
||||
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
|
||||
upon the JavaHelp table of contents document format. The Ghidra help system uses a
|
||||
TOC_Source.xml file to allow a module with help to define how its contents appear in the
|
||||
Ghidra help viewer's table of contents. The main document (in the Base module)
|
||||
defines a basic structure for the
|
||||
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
|
||||
their files directly into this structure (and optionally define a substructure).
|
||||
|
||||
|
||||
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
|
||||
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
|
||||
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
|
||||
appropriate id attribute value. Using these two tags allows any module to define a place
|
||||
in the table of contents system (<tocdef>), which also provides a place for
|
||||
other TOC_Source.xml files to insert content (<tocref>).
|
||||
|
||||
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
|
||||
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
|
||||
<module name>_TOC.xml files, which are table of contents files written in the format
|
||||
desired by the JavaHelp system. Additionally, the genated files will be merged together
|
||||
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
|
||||
help GUI, there will be on table of contents that has been created from the definitions in
|
||||
all of the modules' TOC_Source.xml files.
|
||||
|
||||
|
||||
Tags and Attributes
|
||||
|
||||
<tocdef>
|
||||
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
|
||||
-text - the display text of the node, as seen in the help GUI
|
||||
-target** - the file to display when the node is clicked in the GUI
|
||||
-sortgroup - this is a string that defines where a given node should appear under a given
|
||||
parent. The string values will be sorted by the JavaHelp system using
|
||||
a javax.text.RulesBasedCollator. If this attribute is not specified, then
|
||||
the text of attribute will be used.
|
||||
|
||||
<tocref>
|
||||
-id - The id of the <tocdef> that this reference points to
|
||||
|
||||
**The URL for the target is relative and should start with 'help/topics'. This text is
|
||||
used by the Ghidra help system to provide a universal starting point for all links so that
|
||||
they can be resolved at runtime, across modules.
|
||||
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<tocroot>
|
||||
|
||||
|
||||
</tocroot>
|
|
@ -0,0 +1,64 @@
|
|||
/* ###
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
WARNING!
|
||||
This file is copied to all help directories. If you change this file, you must copy it
|
||||
to each src/main/help/help/shared directory.
|
||||
|
||||
|
||||
Java Help Note: JavaHelp does not accept sizes (like in 'margin-top') in anything but
|
||||
px (pixel) or with no type marking.
|
||||
|
||||
*/
|
||||
|
||||
body { margin-bottom: 50px; margin-left: 10px; margin-right: 10px; margin-top: 10px; } /* some padding to improve readability */
|
||||
li { font-family:times new roman; font-size:14pt; }
|
||||
h1 { color:#000080; font-family:times new roman; font-size:36pt; font-style:italic; font-weight:bold; text-align:center; }
|
||||
h2 { margin: 10px; margin-top: 20px; color:#984c4c; font-family:times new roman; font-size:18pt; font-weight:bold; }
|
||||
h3 { margin-left: 10px; margin-top: 20px; color:#0000ff; font-family:times new roman; `font-size:14pt; font-weight:bold; }
|
||||
h4 { margin-left: 10px; margin-top: 20px; font-family:times new roman; font-size:14pt; font-style:italic; }
|
||||
|
||||
/*
|
||||
P tag code. Most of the help files nest P tags inside of blockquote tags (the was the
|
||||
way it had been done in the beginning). The net effect is that the text is indented. In
|
||||
modern HTML we would use CSS to do this. We need to support the Ghidra P tags, nested in
|
||||
blockquote tags, as well as naked P tags. The following two lines accomplish this. Note
|
||||
that the 'blockquote p' definition will inherit from the first 'p' definition.
|
||||
*/
|
||||
p { margin-left: 40px; font-family:times new roman; font-size:14pt; }
|
||||
blockquote p { margin-left: 10px; }
|
||||
|
||||
p.providedbyplugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px }
|
||||
p.ProvidedByPlugin { color:#7f7f7f; margin-left: 10px; font-size:14pt; margin-top:100px }
|
||||
p.relatedtopic { color:#800080; margin-left: 10px; font-size:14pt; }
|
||||
p.RelatedTopic { color:#800080; margin-left: 10px; font-size:14pt; }
|
||||
|
||||
/*
|
||||
We wish for a tables to have space between it and the preceding element, so that text
|
||||
is not too close to the top of the table. Also, nest the table a bit so that it is clear
|
||||
the table relates to the preceding text.
|
||||
*/
|
||||
table { margin-left: 20px; margin-top: 10px; width: 80%;}
|
||||
td { font-family:times new roman; font-size:14pt; vertical-align: top; }
|
||||
th { font-family:times new roman; font-size:14pt; font-weight:bold; background-color: #EDF3FE; }
|
||||
|
||||
/*
|
||||
Code-like formatting for things such as file system paths and proper names of classes,
|
||||
methods, etc. To apply this to a file path, use this syntax:
|
||||
<CODE CLASS="path">...</CODE>
|
||||
*/
|
||||
code { color: black; font-weight: bold; font-family: courier new, monospace; font-size: 14pt; white-space: nowrap; }
|
||||
code.path { color: #4682B4; font-weight: bold; font-family: courier new, monospace; font-size: 14pt; white-space: nowrap; }
|
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/arrow.gif
Normal file
After Width: | Height: | Size: 69 B |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/close16.gif
Normal file
After Width: | Height: | Size: 859 B |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/menu16.gif
Normal file
After Width: | Height: | Size: 62 B |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/note-red.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/note.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/redo.png
Normal file
After Width: | Height: | Size: 187 B |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/tip.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/undo.png
Normal file
After Width: | Height: | Size: 185 B |
BIN
Ghidra/Framework/Docking/src/main/help/help/shared/warning.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
|
||||
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<TITLE>Placholder Title</TITLE>
|
||||
<LINK rel="stylesheet" type="text/css" href="../../shared/Frontpage.css">
|
||||
</HEAD>
|
||||
|
||||
<BODY>
|
||||
<H1>Stub</H1>
|
||||
|
||||
<P>Blah blah</P>
|
||||
|
||||
<P class="relatedtopic">Related Topics:</P>
|
||||
|
||||
</BODY>
|
||||
</HTML>
|
|
@ -24,11 +24,11 @@ import javax.swing.event.ChangeListener;
|
|||
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.tabbedpane.DockingTabRenderer;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.HelpService;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,10 +23,10 @@ import java.util.*;
|
|||
import javax.swing.*;
|
||||
|
||||
import docking.action.*;
|
||||
import docking.help.HelpDescriptor;
|
||||
import docking.help.HelpService;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.HelpDescriptor;
|
||||
import help.HelpService;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
import javax.swing.JButton;
|
||||
|
||||
import docking.help.HelpDescriptor;
|
||||
import docking.help.HelpService;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -29,13 +29,13 @@ import org.jdesktop.animation.timing.TimingTargetAdapter;
|
|||
import docking.action.*;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.event.mouse.GMouseListenerAdapter;
|
||||
import docking.help.HelpService;
|
||||
import docking.menu.DialogToolbarButton;
|
||||
import docking.util.AnimationUtils;
|
||||
import docking.widgets.label.GDHtmlLabel;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.*;
|
||||
import help.HelpService;
|
||||
import utility.function.Callback;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,9 +23,9 @@ import javax.swing.*;
|
|||
import javax.swing.FocusManager;
|
||||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.help.HelpService;
|
||||
import ghidra.util.CascadedDropTarget;
|
||||
import ghidra.util.HelpLocation;
|
||||
import help.HelpService;
|
||||
|
||||
/**
|
||||
* Wrapper class for user components. Adds the title, local toolbar and provides the drag target
|
||||
|
|
|
@ -30,8 +30,6 @@ import org.jdesktop.animation.timing.Animator.RepeatBehavior;
|
|||
import org.jdesktop.animation.timing.TimingTargetAdapter;
|
||||
import org.jdesktop.animation.timing.interpolation.PropertySetter;
|
||||
|
||||
import docking.help.Help;
|
||||
import docking.help.HelpService;
|
||||
import docking.util.AnimationUtils;
|
||||
import generic.util.WindowUtilities;
|
||||
import generic.util.image.ImageUtils;
|
||||
|
@ -41,6 +39,8 @@ import ghidra.util.HelpLocation;
|
|||
import ghidra.util.Msg;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.bean.GGlassPanePainter;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,10 +25,10 @@ import javax.swing.*;
|
|||
import org.apache.commons.collections4.map.LazyMap;
|
||||
|
||||
import docking.framework.ApplicationInformationDisplayFactory;
|
||||
import docking.help.HelpDescriptor;
|
||||
import generic.util.WindowUtilities;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import help.HelpDescriptor;
|
||||
|
||||
// NOTE: this class has a static focus component variable that is set whenever the dialog gets
|
||||
// activated and is scheduled to get focus at a later time. This variable is static so that only
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.jdom.Element;
|
|||
|
||||
import docking.action.DockingActionIf;
|
||||
import docking.actions.*;
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.PasswordDialog;
|
||||
import generic.util.WindowUtilities;
|
||||
import ghidra.framework.OperatingSystem;
|
||||
|
@ -41,6 +40,8 @@ import ghidra.util.*;
|
|||
import ghidra.util.datastruct.*;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import util.CollectionUtils;
|
||||
|
||||
/**
|
||||
|
@ -72,11 +73,6 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
public static final String DOCKING_WINDOWS_OWNER = "DockingWindows";
|
||||
public static final String TOOL_PREFERENCES_XML_NAME = "PREFERENCES";
|
||||
|
||||
/**
|
||||
* The helpService field should be set to the appropriate help service provider.
|
||||
*/
|
||||
private static HelpService helpService = new DefaultHelpService();
|
||||
|
||||
// we use a list to maintain order
|
||||
private static List<DockingWindowManager> instances = new ArrayList<>();
|
||||
|
||||
|
@ -161,25 +157,13 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return "DockingWindowManager: " + root.getTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the help service for the all docking window managers.
|
||||
*
|
||||
* @param helpSvc the help service to use.
|
||||
*/
|
||||
public static void setHelpService(HelpService helpSvc) {
|
||||
if (helpSvc == null) {
|
||||
throw new IllegalArgumentException("HelpService may not be null");
|
||||
}
|
||||
helpService = helpSvc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global help service.
|
||||
*
|
||||
* @return the global help service.
|
||||
*/
|
||||
public static HelpService getHelpService() {
|
||||
return helpService;
|
||||
return Help.getHelpService();
|
||||
}
|
||||
|
||||
private static synchronized void addInstance(DockingWindowManager winMgr) {
|
||||
|
@ -516,6 +500,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
}
|
||||
|
||||
private void registerHelpLocation(ComponentProvider provider, HelpLocation helpLocation) {
|
||||
HelpService helpService = Help.getHelpService();
|
||||
HelpLocation registeredHelpLocation = helpService.getHelpLocation(provider);
|
||||
if (registeredHelpLocation != null) {
|
||||
return; // nothing to do; location already registered
|
||||
|
|
|
@ -21,7 +21,7 @@ import java.util.Set;
|
|||
import javax.swing.*;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.help.HelpDescriptor;
|
||||
import help.HelpDescriptor;
|
||||
|
||||
/**
|
||||
* The base interface for clients that wish to create commands to be registered with a tool.
|
||||
|
|
|
@ -20,9 +20,9 @@ import java.awt.*;
|
|||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.*;
|
||||
import docking.help.HelpService;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import help.HelpService;
|
||||
|
||||
public class HelpAction extends DockingAction {
|
||||
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/* ###
|
||||
* 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.
|
||||
* 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.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() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,524 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ import java.awt.*;
|
|||
import java.awt.geom.*;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -27,7 +26,8 @@ import javax.help.*;
|
|||
import javax.help.event.HelpModelEvent;
|
||||
import javax.help.event.HelpModelListener;
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.*;
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.html.HTML;
|
||||
import javax.swing.text.html.HTMLDocument;
|
||||
|
||||
|
@ -39,108 +39,45 @@ import docking.util.AnimationPainter;
|
|||
import docking.util.AnimationUtils;
|
||||
import ghidra.framework.preferences.Preferences;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.Swing;
|
||||
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
|
||||
import help.CustomTOCView;
|
||||
import help.GHelpBroker;
|
||||
|
||||
/**
|
||||
* Ghidra help broker that displays the help set; sets the Ghidra icon on
|
||||
* the help frame and attempts to maintain the user window size.
|
||||
* An extension of the {@link GHelpBroker} that allows {@code Docking} classes to be installed.
|
||||
* <p>
|
||||
* Additions include a search feature a navigation aid.
|
||||
*/
|
||||
public class GHelpBroker extends DefaultHelpBroker {
|
||||
public class DockingHelpBroker extends GHelpBroker {
|
||||
|
||||
private static final List<Image> ICONS = ApplicationInformationDisplayFactory.getWindowIcons();
|
||||
|
||||
private static final int MAX_CALLOUT_RETRIES = 3;
|
||||
|
||||
private Dimension windowSize = new Dimension(1100, 700);
|
||||
|
||||
private JEditorPane htmlEditorPane;
|
||||
private Animator lastAnimator;
|
||||
private URL loadingURL;
|
||||
private PropertyChangeListener pageLoadListener = new PageLoadingListener();
|
||||
private HelpModelListener helpModelListener = new HelpIDChangedListener();
|
||||
private Window activationWindow;
|
||||
private Animator lastAnimator;
|
||||
private URL loadingURL;
|
||||
|
||||
// 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");
|
||||
|
||||
/**
|
||||
* Construct a new GhidraHelpBroker.
|
||||
* @param hs java help set associated with this help broker
|
||||
*/
|
||||
public GHelpBroker(HelpSet hs) {
|
||||
public DockingHelpBroker(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 = getHelpModel();
|
||||
if (model != null) {
|
||||
model.setCurrentURL(URL, getHistoryName(URL), null);
|
||||
}
|
||||
else {
|
||||
super.setCurrentURL(URL);
|
||||
}
|
||||
protected List<Image> getApplicationIcons() {
|
||||
return ICONS;
|
||||
}
|
||||
|
||||
/* Perform some shenanigans to force Java Help to reload the given URL */
|
||||
void reloadHelpPage(URL url) {
|
||||
clearContentViewer();
|
||||
prepareToCallout(url);
|
||||
try {
|
||||
htmlEditorPane.setPage(url);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.error(this, "Unexpected error loading help page: " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearContentViewer() {
|
||||
htmlEditorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null);
|
||||
}
|
||||
|
||||
private JScrollPane getScrollPane(JEditorPane editorPane) {
|
||||
Container parent = editorPane.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JScrollPane) {
|
||||
return (JScrollPane) parent;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return 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();
|
||||
}
|
||||
|
||||
private HelpModel getHelpModel() {
|
||||
@Override
|
||||
protected HelpModel getCustomHelpModel() {
|
||||
//
|
||||
// Unusual Code Alert!: We have opened up access to the help system's HelpModel by way
|
||||
// of our CustomTOCView object that we install elsewhere. We need
|
||||
// access to the model because of a bug in the help system
|
||||
// of our CustomTOCView object that we install elsewhere. We need
|
||||
// access to the model because of a bug in the help system
|
||||
// (SCR 7639). Unfortunately, the Java Help system does not give us
|
||||
// access to the model directly, but we have opened up the access from
|
||||
// one of our overriding components. The following code is
|
||||
// one of our overriding components. The following code is
|
||||
// digging-out our custom component to get at the model. An
|
||||
// alternative approach would be to just use reflection and violate
|
||||
// security restrictions, but that seemed worse than this solution.
|
||||
|
@ -160,133 +97,18 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
}
|
||||
|
||||
@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);
|
||||
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 = getHelpModel();
|
||||
protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) {
|
||||
helpModel.addHelpModelListener(helpModelListener);
|
||||
new HelpViewSearcher(jHelp, helpModel);
|
||||
|
||||
installActions(jHelp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected void showNavigationAid(URL url) {
|
||||
prepareToCallout(url);
|
||||
}
|
||||
|
||||
private void installActions(JHelp help) {
|
||||
@Override
|
||||
protected void installActions(JHelp help) {
|
||||
JToolBar toolbar = null;
|
||||
Component[] components = help.getComponents();
|
||||
for (Component c : components) {
|
||||
|
@ -308,79 +130,24 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
toolbar.add(new JButton(action));
|
||||
}
|
||||
|
||||
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;
|
||||
@Override // opened access
|
||||
protected void reloadHelpPage(URL url) {
|
||||
super.reloadHelpPage(url);
|
||||
}
|
||||
|
||||
private void installRootPane(JFrame frame) {
|
||||
Component oldGlassPane = frame.getGlassPane();
|
||||
if (!(oldGlassPane instanceof GGlassPane)) {
|
||||
GGlassPane gGlassPane = new GGlassPane();
|
||||
frame.setGlassPane(gGlassPane);
|
||||
gGlassPane.setVisible(true);
|
||||
}
|
||||
}
|
||||
//=================================================================================================
|
||||
// Navigation Aid Section
|
||||
//=================================================================================================
|
||||
|
||||
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;
|
||||
private JScrollPane getScrollPane(JEditorPane editorPane) {
|
||||
Container parent = editorPane.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof JScrollPane) {
|
||||
return (JScrollPane) parent;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void showNavigationAid() {
|
||||
|
@ -398,7 +165,7 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
}
|
||||
|
||||
private void calloutReferenceLater() {
|
||||
SwingUtilities.invokeLater(() -> calloutReference(loadingURL));
|
||||
Swing.runLater(() -> calloutReference(loadingURL));
|
||||
}
|
||||
|
||||
private void calloutReference(final URL url) {
|
||||
|
@ -442,10 +209,10 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
* help pages and when the UI is adjusted in response to those changes.
|
||||
* <p>
|
||||
* Note: this method will call itself if the view is not yet updated for the requested
|
||||
* model change. In that case, this method will call itself again later. This may
|
||||
* need to happen more than once. However, we will only try a few times and
|
||||
* model change. In that case, this method will call itself again later. This may
|
||||
* need to happen more than once. However, we will only try a few times and
|
||||
* then just give up.
|
||||
*
|
||||
*
|
||||
* @param area the area to call out
|
||||
* @param callCount the number number of times this method has already been called
|
||||
*/
|
||||
|
@ -477,20 +244,20 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
//
|
||||
// Unusual Code: Not yet rendered! Try again.
|
||||
//
|
||||
SwingUtilities.invokeLater(() -> doCalloutReference(area, numberOfCalls));
|
||||
Swing.runLater(() -> doCalloutReference(area, numberOfCalls));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The area of the HTML content is absolute inside of the entire document.
|
||||
// However, the user is viewing the document inside of a scroll pane. So, we
|
||||
// The area of the HTML content is absolute inside of the entire document.
|
||||
// However, the user is viewing the document inside of a scroll pane. So, we
|
||||
// want the offset of the element within the viewer, not the absolute position.
|
||||
//
|
||||
area.y -= viewPosition.y;
|
||||
|
||||
//
|
||||
// Update the coordinates to be relative to the content pane, which is where we
|
||||
// Update the coordinates to be relative to the content pane, which is where we
|
||||
// are doing the painting.
|
||||
//
|
||||
Rectangle relativeArea = SwingUtilities.convertRectangle(scrollPane, area, contentPane);
|
||||
|
@ -519,7 +286,6 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
|
||||
loadingURL = url;
|
||||
|
||||
// TODO
|
||||
// updateTitle();
|
||||
|
||||
if (isCurrentPage(loadingURL)) {
|
||||
|
@ -650,8 +416,8 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
// At 0%, with 100% opacity; at the end, paint with 0% opacity
|
||||
//
|
||||
Composite originalComposite = g2d.getComposite();
|
||||
AlphaComposite alphaComposite = AlphaComposite.getInstance(
|
||||
AlphaComposite.SrcOver.getRule(), (float) (1 - percentComplete));
|
||||
AlphaComposite alphaComposite = AlphaComposite
|
||||
.getInstance(AlphaComposite.SrcOver.getRule(), (float) (1 - percentComplete));
|
||||
g2d.setComposite(alphaComposite);
|
||||
|
||||
double transition = 1 - percentComplete;
|
||||
|
@ -686,7 +452,7 @@ public class GHelpBroker extends DefaultHelpBroker {
|
|||
Shape box = scaler.createTransformedShape(b);
|
||||
g2d.setColor(Color.GREEN);
|
||||
g2d.fill(box);
|
||||
|
||||
|
||||
box = transform.createTransformedShape(box);
|
||||
g2d.setColor(Color.YELLOW);
|
||||
g2d.fill(box);
|
|
@ -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,16 +15,24 @@
|
|||
*/
|
||||
package docking.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.
|
||||
*/
|
||||
public Object getHelpObject();
|
||||
|
||||
/**
|
||||
* Returns a descriptive String about the help object that this descriptor represents.
|
||||
*/
|
||||
public String getHelpInfo();
|
||||
import java.net.URL;
|
||||
|
||||
import javax.help.HelpBroker;
|
||||
import javax.help.HelpSetException;
|
||||
|
||||
import help.GHelpSet;
|
||||
|
||||
/**
|
||||
* An extension of the {@link GHelpSet} that allows {@code Docking} classes to be installed.
|
||||
*/
|
||||
public class DockingHelpSet extends GHelpSet {
|
||||
|
||||
public DockingHelpSet(ClassLoader loader, URL helpset) throws HelpSetException {
|
||||
super(loader, helpset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpBroker createHelpBroker() {
|
||||
return new DockingHelpBroker(this);
|
||||
}
|
||||
}
|
|
@ -1,538 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,322 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.help;
|
||||
|
||||
import docking.DefaultHelpService;
|
||||
import docking.DockingWindowManager;
|
||||
|
||||
/**
|
||||
* 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
|
||||
static void installHelpService(HelpService service) {
|
||||
helpService = service;
|
||||
DockingWindowManager.setHelpService(helpService);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,6 +38,7 @@ import ghidra.util.*;
|
|||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import help.*;
|
||||
import resources.ResourceManager;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
|
@ -83,7 +84,7 @@ public class HelpManager implements HelpService {
|
|||
* @throws HelpSetException if HelpSet could not be created
|
||||
*/
|
||||
protected HelpManager(URL url) throws HelpSetException {
|
||||
mainHS = new GHelpSet(new GHelpClassLoader(null), url);
|
||||
mainHS = new DockingHelpSet(new GHelpClassLoader(null), url);
|
||||
mainHB = mainHS.createHelpBroker();
|
||||
mainHS.setTitle(GHIDRA_HELP_TITLE);
|
||||
|
||||
|
@ -260,8 +261,8 @@ public class HelpManager implements HelpService {
|
|||
displayHelp(createHelpID(helpId), window);
|
||||
}
|
||||
catch (BadIDException e) {
|
||||
Msg.info(this, "Could not find help for ID: \"" + helpId +
|
||||
"\" from HelpLocation: " + loc);
|
||||
Msg.info(this,
|
||||
"Could not find help for ID: \"" + helpId + "\" from HelpLocation: " + loc);
|
||||
displayHelp(HELP_NOT_FOUND_PAGE_URL, window);
|
||||
}
|
||||
}
|
||||
|
@ -565,12 +566,12 @@ public class HelpManager implements HelpService {
|
|||
/** This forces page to be redisplayed when location has not changed */
|
||||
private void reloadPage(URL helpURL) {
|
||||
|
||||
if (!(mainHB instanceof GHelpBroker)) {
|
||||
if (!(mainHB instanceof DockingHelpBroker)) {
|
||||
// not our broker installed; can't force a reload
|
||||
return;
|
||||
}
|
||||
|
||||
((GHelpBroker) mainHB).reloadHelpPage(validateUrl(helpURL));
|
||||
((DockingHelpBroker) mainHB).reloadHelpPage(validateUrl(helpURL));
|
||||
}
|
||||
|
||||
private URL getURLForID(ID ID) {
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package docking.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();
|
||||
}
|
|
@ -54,7 +54,7 @@ public class ToggleNavigationAid extends AbstractAction {
|
|||
showingNavigationAid = Boolean.parseBoolean(value);
|
||||
}
|
||||
else {
|
||||
// not yet in the preferences; save the default
|
||||
// not yet in the preferences; save the default
|
||||
savePreference();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,11 @@ import javax.swing.JButton;
|
|||
|
||||
import docking.ActionContext;
|
||||
import docking.action.*;
|
||||
import docking.help.Help;
|
||||
import docking.widgets.EventTrigger;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import help.Help;
|
||||
import resources.icons.EmptyIcon;
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,12 +24,12 @@ import javax.swing.BorderFactory;
|
|||
import javax.swing.JPanel;
|
||||
import javax.swing.border.Border;
|
||||
|
||||
import docking.help.Help;
|
||||
import docking.help.HelpService;
|
||||
import ghidra.framework.options.*;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.layout.VerticalLayout;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
|
@ -27,8 +27,6 @@ import javax.swing.*;
|
|||
import javax.swing.tree.TreePath;
|
||||
import javax.swing.tree.TreeSelectionModel;
|
||||
|
||||
import docking.help.Help;
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.MultiLineLabel;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.label.GIconLabel;
|
||||
|
@ -40,6 +38,8 @@ import ghidra.util.*;
|
|||
import ghidra.util.bean.opteditor.OptionsVetoException;
|
||||
import ghidra.util.layout.MiddleLayout;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import resources.ResourceManager;
|
||||
|
||||
public class OptionsPanel extends JPanel {
|
||||
|
|
|
@ -29,7 +29,6 @@ import javax.swing.table.TableModel;
|
|||
import org.jdom.Element;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.help.HelpService;
|
||||
import docking.menu.*;
|
||||
import docking.widgets.EmptyBorderButton;
|
||||
import docking.widgets.EventTrigger;
|
||||
|
@ -45,6 +44,7 @@ import ghidra.util.datastruct.WeakDataStructureFactory;
|
|||
import ghidra.util.datastruct.WeakSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.task.SwingUpdateManager;
|
||||
import help.HelpService;
|
||||
import resources.Icons;
|
||||
import resources.ResourceManager;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
|
|
@ -22,10 +22,10 @@ import javax.swing.*;
|
|||
import javax.swing.table.*;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
|
||||
import ghidra.util.HTMLUtilities;
|
||||
import ghidra.util.HelpLocation;
|
||||
import help.HelpService;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ import javax.swing.border.BevelBorder;
|
|||
import org.jdom.Element;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.EmptyBorderButton;
|
||||
import docking.widgets.filter.*;
|
||||
import docking.widgets.label.GLabel;
|
||||
|
@ -32,6 +31,7 @@ import docking.widgets.tree.support.GTreeFilter;
|
|||
import ghidra.framework.options.PreferenceState;
|
||||
import ghidra.util.FilterTransformer;
|
||||
import ghidra.util.HelpLocation;
|
||||
import help.HelpService;
|
||||
|
||||
public class DefaultGTreeFilterProvider implements GTreeFilterProvider {
|
||||
private static final String FILTER_STATE = "FILTER_STATE";
|
||||
|
|
|
@ -25,11 +25,11 @@ import javax.swing.border.TitledBorder;
|
|||
|
||||
import docking.DialogComponentProvider;
|
||||
import docking.DockingWindowManager;
|
||||
import docking.help.Help;
|
||||
import docking.help.HelpService;
|
||||
import docking.widgets.EmptyBorderButton;
|
||||
import docking.widgets.label.GDLabel;
|
||||
import ghidra.util.*;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
import resources.ResourceManager;
|
||||
|
||||
/**
|
||||
|
|