/* ### * IP: GHIDRA * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package help; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.net.URL; import java.util.List; import java.util.Objects; import javax.help.*; import javax.swing.*; import javax.swing.text.Document; import generic.theme.GIcon; import ghidra.util.Msg; import ghidra.util.Swing; import ghidra.util.bean.GGlassPane; import resources.Icons; import resources.MultiIconBuilder; import resources.icons.EmptyIcon; // NOTE: for JH 2.0, this class has been rewritten to not // access the 'frame' and 'dialog' variable directly /** * Ghidra help broker that displays the help set; sets the application icon on the help frame and * attempts to maintain the user window size. */ public class GHelpBroker extends DefaultHelpBroker { // Create the zoom in/out icons that will be added to the default jHelp toolbar. private static final Icon ZOOM_OUT_ICON = new GIcon("icon.subtract"); private static final Icon ZOOM_IN_ICON = Icons.ADD_ICON; private Dimension windowSize = new Dimension(1100, 700); protected JEditorPane htmlEditorPane; private Window activationWindow; private boolean initialized; /** * Construct a new GhidraHelpBroker. * @param hs java help set associated with this help broker */ public GHelpBroker(HelpSet hs) { super(hs); } @Override // Overridden so that we can call the preferred version of setCurrentURL on the HelpModel, // which fixes a bug with the history list (SCR 7639) public void setCurrentURL(final URL URL) { HelpModel model = getCustomHelpModel(); if (model != null) { model.setCurrentURL(URL, getHistoryName(URL), null); } else { super.setCurrentURL(URL); } } protected List getApplicationIcons() { return null; } protected HelpModel getCustomHelpModel() { return null; } /* Perform some shenanigans to force Java Help to reload the given URL */ protected void reloadHelpPage(URL url) { clearContentViewer(); showNavigationAid(url); try { // Page loading is asynchronous. Listen for the page to be loaded and then restore the // users current view state. htmlEditorPane.addPropertyChangeListener(new PageLocationUpdater()); htmlEditorPane.setPage(url); } catch (IOException e) { Msg.error(this, "Unexpected error loading help page: " + url, e); return; } } private void reloadHelpPage() { reloadHelpPage(getCurrentURL()); } public void reload() { clearHighlights(); initialized = false; if (isDisplayed()) { setDisplayed(false); setDisplayed(true); } } private void clearHighlights() { TextHelpModel helpModel = (TextHelpModel) getCustomHelpModel(); if (helpModel != null) { helpModel.removeAllHighlights(); } } protected void showNavigationAid(URL url) { // this base class does not have a navigation aid } private void clearContentViewer() { htmlEditorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null); } private JEditorPane getHTMLEditorPane(JHelpContentViewer contentViewer) { // // Intimate Knowledge - construction of the viewer: // // -BorderLayout // -JScrollPane // -Viewport // -JHEditorPane extends JEditorPane // // Component[] components = contentViewer.getComponents(); JScrollPane scrollPane = (JScrollPane) components[0]; JViewport viewport = scrollPane.getViewport(); return (JEditorPane) viewport.getView(); } @Override public void setDisplayed(boolean b) { if (!b) { super.setDisplayed(b); return; } // this must be before any call that triggers the help system to create its window initializeScreenDevice(); WindowPresentation windowPresentation = getWindowPresentation(); updateWindowSize(windowPresentation); // this has to be before getHelpWindow() or the value returned will be null super.setDisplayed(b); initializeUIWindowPresentation(windowPresentation); } private void initializeScreenDevice() { if (initialized) { return; } if (activationWindow == null) { // This can happen when we show the 'What's New' help page on a fresh install. In // that case, we were not activated from an existing window, thus, there may // be no parent window. return; } GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice[] gs = ge.getScreenDevices(); GraphicsConfiguration config = activationWindow.getGraphicsConfiguration(); GraphicsDevice parentDevice = config.getDevice(); for (int i = 0; i < gs.length; i++) { if (gs[i] == parentDevice) { // update the help window's screen to match that of the parent setScreen(i); } } } private void initializeUIWindowPresentation(WindowPresentation windowPresentation) { Window helpWindow = windowPresentation.getHelpWindow(); Container contentPane = null; if (helpWindow instanceof JFrame) { JFrame frame = (JFrame) helpWindow; installRootPane(frame); List icons = getApplicationIcons(); if (icons != null) { frame.setIconImages(icons); } contentPane = frame.getContentPane(); } else if (helpWindow instanceof JDialog) { JDialog dialog = (JDialog) helpWindow; installRootPane(dialog); contentPane = dialog.getContentPane(); } initializeUIComponents(contentPane); } private void initializeUIComponents(Container contentPane) { if (initialized) { return;// already initialized } Component[] components = contentPane.getComponents(); JHelp jHelp = (JHelp) components[0]; JHelpContentViewer contentViewer = jHelp.getContentViewer(); JEditorPane activeHtmlPane = getHTMLEditorPane(contentViewer); if (activeHtmlPane == htmlEditorPane && initialized) { return; // already initialized } addCustomToolbarItems(jHelp); htmlEditorPane = getHTMLEditorPane(contentViewer); // just creating the search wires everything together HelpModel helpModel = getCustomHelpModel(); installHelpSearcher(jHelp, helpModel); if (helpModel != null) { installHelpSearcher(jHelp, helpModel); } installActions(jHelp); initialized = true; } protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) { // this base class does not provide an in-page search feature } /** * Create zoom in/out buttons on the default help window toolbar. * @param jHelp the java help object used to retrieve the help components */ protected void addCustomToolbarItems(final JHelp jHelp) { for (Component component : jHelp.getComponents()) { if (component instanceof JToolBar) { JToolBar toolbar = (JToolBar) component; toolbar.addSeparator(); ImageIcon icon = new MultiIconBuilder(new EmptyIcon(24, 24)).addCenteredIcon(ZOOM_OUT_ICON) .build(); Icon zoomOutIcon = icon; 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(); }); toolbar.add(zoomOutBtn); icon = new MultiIconBuilder(new EmptyIcon(24, 24)).addCenteredIcon(ZOOM_IN_ICON) .build(); Icon zoomInIcon = icon; 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(); }); toolbar.add(zoomInBtn); // Once we've found the toolbar we can break out of the loop and stop looking for it. break; } } } protected void installActions(JHelp help) { // subclasses may have actions } private String getHistoryName(URL URL) { String text = URL.getFile(); int index = text.lastIndexOf('/'); if (index != -1) { // we want just the filename text = text.substring(index + 1); } String ref = URL.getRef(); if (ref != null) { text += " - " + ref; } return text; } private void installRootPane(JFrame frame) { Component oldGlassPane = frame.getGlassPane(); if (!(oldGlassPane instanceof GGlassPane)) { GGlassPane gGlassPane = new GGlassPane(); frame.setGlassPane(gGlassPane); gGlassPane.setVisible(true); } } private void installRootPane(JDialog dialog) { Component oldGlassPane = dialog.getGlassPane(); if (!(oldGlassPane instanceof GGlassPane)) { GGlassPane gGlassPane = new GGlassPane(); dialog.setGlassPane(gGlassPane); gGlassPane.setVisible(true); } } private void updateWindowSize(WindowPresentation presentation) { if (windowSize == null) { return; } presentation.createHelpWindow(); presentation.setSize(windowSize); } @Override public void setActivationWindow(Window window) { WindowPresentation windowPresentation = getWindowPresentation(); Window helpWindow = windowPresentation.getHelpWindow(); if (helpWindow == null) { activationWindow = window; super.setActivationWindow(window); return; } windowSize = helpWindow.getSize();// remember the previous size boolean wasModal = isModalWindow(helpWindow); boolean willBeModal = isModalWindow(window); if (!wasModal && willBeModal) { // in this condition, a new window will be shown, but the old one is not properly // closed by JavaHelp helpWindow.setVisible(false); } super.setActivationWindow(window); } private boolean isModalWindow(Window window) { if (window instanceof Dialog) { Dialog dialog = (Dialog) window; if (dialog.isModal()) { return true; } } return false; } //================================================================================================= // Inner Classes //================================================================================================= private class PageLocationUpdater implements PropertyChangeListener { private URL url; private int caretPosition; private Rectangle viewPosition; PageLocationUpdater() { url = getCurrentURL(); caretPosition = htmlEditorPane.getCaretPosition(); viewPosition = htmlEditorPane.getVisibleRect(); } @Override public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); if (!name.equals("page")) { return; } htmlEditorPane.removePropertyChangeListener(this); URL currentUrl = getCurrentURL(); if (!Objects.equals(currentUrl, url)) { return; // new page loaded; ignore } htmlEditorPane.setCaretPosition(caretPosition); // not sure why this needs to be done later, but setting the caret seems to trigger a // view updated, so try to run after that Swing.runLater(() -> { htmlEditorPane.scrollRectToVisible(viewPosition); }); } } }