/* * HelpViewerDialog.java - HTML Help viewer * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 1999, 2005 Slava Pestov, Nicholas O'Leary * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.help; //{{{ Imports import java.awt.BorderLayout; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.datatransfer.StringSelection; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.SwingWorker; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLFrameHyperlinkEvent; import javax.swing.text.html.HTML; import javax.swing.text.AttributeSet; import javax.swing.text.Element; import org.gjt.sp.jedit.EditBus; import org.gjt.sp.jedit.GUIUtilities; import org.gjt.sp.jedit.jEdit; import org.gjt.sp.jedit.MiscUtilities; import org.gjt.sp.jedit.EditBus.EBHandler; import org.gjt.sp.jedit.io.AutoDetection; import org.gjt.sp.jedit.io.RegexEncodingDetector; import org.gjt.sp.jedit.msg.PluginUpdate; import org.gjt.sp.util.Log; import static org.gjt.sp.jedit.help.HelpHistoryModel.HistoryEntry; //}}} /** * jEdit's searchable help viewer. It uses a Swing JEditorPane to display the HTML, * and implements a URL history. * @author Slava Pestov * @version $Id$ */ public class HelpViewer extends JFrame implements HelpViewerInterface, HelpHistoryModelListener { private static final long serialVersionUID = 1L; private static final RegexEncodingDetector ENCODING_DETECTOR = new RegexEncodingDetector(":encoding=([^:]+):", "$1"); //{{{ HelpViewer constructor /** * Creates a new help viewer with the default help page. * @since jEdit 4.0pre4 */ public HelpViewer() { this("welcome.html"); } //}}} //{{{ HelpViewer constructor /** * Creates a new help viewer for the specified URL. * @param url The URL */ public HelpViewer(URL url) { this(url.toString()); } //}}} //{{{ HelpViewer constructor /** * Creates a new help viewer for the specified URL. * @param url The URL */ public HelpViewer(String url) { super(jEdit.getProperty("helpviewer.title")); setIconImage(GUIUtilities.getEditorIcon()); try { baseURL = new File(MiscUtilities.constructPath( jEdit.getJEditHome(),"doc")).toURI().toURL().toString(); } catch(MalformedURLException mu) { Log.log(Log.ERROR,this,mu); // what to do? } ActionHandler actionListener = new ActionHandler(); JTabbedPane tabs = new JTabbedPane(); tabs.addTab(jEdit.getProperty("helpviewer.toc.label"), toc = new HelpTOCPanel(this)); tabs.addTab(jEdit.getProperty("helpviewer.search.label"), new HelpSearchPanel(this)); tabs.setMinimumSize(new Dimension(0,0)); JPanel rightPanel = new JPanel(new BorderLayout()); Box toolBar = new Box(BoxLayout.X_AXIS); //toolBar.setFloatable(false); toolBar.add(title = new JLabel()); toolBar.add(Box.createGlue()); historyModel = new HelpHistoryModel(25); back = new HistoryButton(HistoryButton.BACK,historyModel); back.addActionListener(actionListener); toolBar.add(back); forward = new HistoryButton(HistoryButton.FORWARD,historyModel); forward.addActionListener(actionListener); toolBar.add(forward); back.setPreferredSize(forward.getPreferredSize()); rightPanel.add(BorderLayout.NORTH,toolBar); viewer = new JEditorPane(); viewer.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); viewer.setEditable(false); viewer.addHyperlinkListener(new LinkHandler()); viewer.setFont(jEdit.getFontProperty("helpviewer.font")); viewer.addPropertyChangeListener(new PropertyChangeHandler()); viewer.addKeyListener(new KeyHandler()); viewer.addMouseListener(new MouseHandler()); viewerScrollPane = new JScrollPane(viewer); rightPanel.add(BorderLayout.CENTER,viewerScrollPane); splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tabs, rightPanel); splitter.setBorder(null); getContentPane().add(BorderLayout.CENTER,splitter); historyModel.addHelpHistoryModelListener(this); historyUpdated(); gotoURL(url,true,0); setDefaultCloseOperation(DISPOSE_ON_CLOSE); getRootPane().setPreferredSize(new Dimension(750,500)); pack(); GUIUtilities.loadGeometry(this,"helpviewer"); GUIUtilities.addSizeSaver(this,"helpviewer"); EditBus.addToBus(this); setVisible(true); EventQueue.invokeLater(new Runnable() { @Override public void run() { splitter.setDividerLocation(jEdit.getIntegerProperty( "helpviewer.splitter",250)); viewer.requestFocus(); } }); } //}}} //{{{ gotoURL() method /** * Displays the specified URL in the HTML component. * * @param url The URL * @param addToHistory Should the URL be added to the back/forward * history? * @param scrollPosition The vertical scrollPosition */ @Override public void gotoURL(String url, final boolean addToHistory, final int scrollPosition) { // the TOC pane looks up user's guide URLs relative to the // doc directory... String shortURL; if (MiscUtilities.isURL(url)) { if (url.startsWith(baseURL)) { shortURL = url.substring(baseURL.length()); if(shortURL.startsWith("/")) { shortURL = shortURL.substring(1); } } else { shortURL = url; } } else { shortURL = url; if(baseURL.endsWith("/")) { url = baseURL + url; } else { url = baseURL + '/' + url; } } // reset default cursor so that the hand cursor doesn't // stick around viewer.setCursor(Cursor.getDefaultCursor()); try { final URL _url = new URL(url); final String _shortURL = shortURL; if(!_url.equals(viewer.getPage())) { title.setText(jEdit.getProperty("helpviewer.loading")); } else { /* don't show loading msg because we won't receive a propertyChanged */ } historyModel.setCurrentScrollPosition(viewer.getPage(),getCurrentScrollPosition()); /* call setPage asynchronously, because it can block when one can't connect to host. Calling setPage outside from the EDT violates the single-tread rule of Swing, but it's an experienced workaround (see merge request #2984022 - fix blocking HelpViewer https://sourceforge.net/tracker/?func=detail&aid=2984022&group_id=588&atid=1235750 for discussion). Once jEdit sets JDK 7 as dependency, all this should be reverted to synchronous code. */ SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() { private boolean success; @Override protected Void doInBackground() throws Exception { try { // reset encoding viewer.putClientProperty("charset", null); // guess encoding if(_url.getPath().matches(".+\\.([tT][xX][tT])")) { URLConnection connection = _url.openConnection(); if(connection.getContentEncoding() == null) { InputStream is = connection.getInputStream(); BufferedInputStream in = AutoDetection.getMarkedStream(is); String encoding = ENCODING_DETECTOR.detectEncoding(in); if(encoding != null) { // JEditorPane uses charset to create the reader passed to the // EditorKit in JEditorPane.read(). viewer.putClientProperty("charset", encoding); } in.close(); } } viewer.setPage(_url); success = true; } catch(IOException io) { Log.log(Log.ERROR,this,io); String[] args = { _url.toString(), io.toString() }; GUIUtilities.error(HelpViewer.this,"read-error",args); } return null; } @Override protected void done() { if (success) { if (scrollPosition != 0) { viewerScrollPane.getVerticalScrollBar().setValue(scrollPosition); } if(addToHistory) { historyModel.addToHistory(_url.toString()); } HelpViewer.this.shortURL = _shortURL; // select the appropriate tree node. if(_shortURL != null) { toc.selectNode(_shortURL); } viewer.requestFocus(); } } }; worker.execute(); } catch(MalformedURLException mf) { Log.log(Log.ERROR,this,mf); String[] args = { url, mf.getMessage() }; GUIUtilities.error(this,"badurl",args); } } //}}} //{{{ getCurrentScrollPosition() method int getCurrentScrollPosition() { return viewerScrollPane.getVerticalScrollBar().getValue(); } //}}} //{{{ getCurrentPage() method URL getCurrentPage() { return viewer.getPage(); } //}}} //{{{ dispose() method @Override public void dispose() { EditBus.removeFromBus(this); jEdit.setIntegerProperty("helpviewer.splitter", splitter.getDividerLocation()); super.dispose(); } //}}} //{{{ handlePluginUpdate() method @EBHandler public void handlePluginUpdate(PluginUpdate pmsg) { if(pmsg.getWhat() == PluginUpdate.LOADED || pmsg.getWhat() == PluginUpdate.UNLOADED) { if(!pmsg.isExiting()) { if(!queuedTOCReload) queueTOCReload(); queuedTOCReload = true; } } } //}}} //{{{ getBaseURL() method @Override public String getBaseURL() { return baseURL; } //}}} //{{{ getShortURL() method @Override public String getShortURL() { return shortURL; } //}}} //{{{ historyUpdated() method @Override public void historyUpdated() { back.setEnabled(historyModel.hasPrevious()); forward.setEnabled(historyModel.hasNext()); } //}}} //{{{ getComponent method @Override public Component getComponent() { return getRootPane(); } //}}} //{{{ Private members //{{{ Instance members private String baseURL; private String shortURL; private final HistoryButton back; private final HistoryButton forward; private final JEditorPane viewer; private final JScrollPane viewerScrollPane; private final JLabel title; private final JSplitPane splitter; private final HelpHistoryModel historyModel; private final HelpTOCPanel toc; private boolean queuedTOCReload; //}}} //{{{ queueTOCReload() method @Override public void queueTOCReload() { EventQueue.invokeLater(new Runnable() { @Override public void run() { queuedTOCReload = false; toc.load(); } }); } //}}} //}}} //{{{ Inner classes //{{{ ActionHandler class class ActionHandler implements ActionListener { //{{{ actionPerformed() class @Override public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); String actionCommand = evt.getActionCommand(); int separatorPosition = actionCommand.lastIndexOf(':'); String url; int scrollPosition; if (-1 == separatorPosition) { url = actionCommand; scrollPosition = 0; } else { url = actionCommand.substring(0,separatorPosition); scrollPosition = Integer.parseInt(actionCommand.substring(separatorPosition+1)); } if (!url.isEmpty()) { gotoURL(url,false,scrollPosition); return; } if(source == back) { HistoryEntry entry = historyModel.back(HelpViewer.this); if(entry == null) { javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); } else { gotoURL(entry.url,false,entry.scrollPosition); } } else if(source == forward) { HistoryEntry entry = historyModel.forward(HelpViewer.this); if(entry == null) { javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); } else { gotoURL(entry.url,false,entry.scrollPosition); } } } //}}} } //}}} //{{{ LinkHandler class class LinkHandler implements HyperlinkListener { //{{{ hyperlinkUpdate() method @Override public void hyperlinkUpdate(HyperlinkEvent evt) { if(evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { if(evt instanceof HTMLFrameHyperlinkEvent) { ((HTMLDocument)viewer.getDocument()) .processHTMLFrameHyperlinkEvent( (HTMLFrameHyperlinkEvent)evt); historyUpdated(); } else { URL url = evt.getURL(); if(url != null) { gotoURL(url.toString(),true,0); } } } else if (evt.getEventType() == HyperlinkEvent.EventType.ENTERED) { viewer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else if (evt.getEventType() == HyperlinkEvent.EventType.EXITED) { viewer.setCursor(Cursor.getDefaultCursor()); } } //}}} } //}}} //{{{ PropertyChangeHandler class class PropertyChangeHandler implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { if("page".equals(evt.getPropertyName())) { String titleStr = (String)viewer.getDocument() .getProperty("title"); if(titleStr == null) { titleStr = MiscUtilities.getFileName( viewer.getPage().toString()); } title.setText(titleStr); historyModel.updateTitle(viewer.getPage().toString(), titleStr); } } } //}}} //{{{ KeyHandler class private class KeyHandler extends KeyAdapter { @Override public void keyPressed(KeyEvent ke) { switch (ke.getKeyCode()) { case KeyEvent.VK_UP: JScrollBar scrollBar = viewerScrollPane.getVerticalScrollBar(); scrollBar.setValue(scrollBar.getValue()-scrollBar.getUnitIncrement(-1)); ke.consume(); break; case KeyEvent.VK_DOWN: scrollBar = viewerScrollPane.getVerticalScrollBar(); scrollBar.setValue(scrollBar.getValue()+scrollBar.getUnitIncrement(1)); ke.consume(); break; case KeyEvent.VK_LEFT: scrollBar = viewerScrollPane.getHorizontalScrollBar(); scrollBar.setValue(scrollBar.getValue()-scrollBar.getUnitIncrement(-1)); ke.consume(); break; case KeyEvent.VK_RIGHT: scrollBar = viewerScrollPane.getHorizontalScrollBar(); scrollBar.setValue(scrollBar.getValue()+scrollBar.getUnitIncrement(1)); ke.consume(); break; case KeyEvent.VK_HOME: scrollBar = viewerScrollPane.getHorizontalScrollBar(); scrollBar.setValue(0); scrollBar = viewerScrollPane.getVerticalScrollBar(); scrollBar.setValue(0); ke.consume(); break; case KeyEvent.VK_END: scrollBar = viewerScrollPane.getHorizontalScrollBar(); scrollBar.setValue(scrollBar.getMaximum()); scrollBar = viewerScrollPane.getVerticalScrollBar(); scrollBar.setValue(scrollBar.getMaximum()); ke.consume(); break; } } } //}}} //{{{ MouseHandler class private class MouseHandler extends MouseAdapter { @Override public void mousePressed(MouseEvent me) { if(me.isPopupTrigger()) { handlePopupTrigger(me); } } @Override public void mouseReleased(MouseEvent me) { if(me.isPopupTrigger()) { handlePopupTrigger(me); } } private void handlePopupTrigger(MouseEvent me) { int caret = viewer.getUI().viewToModel(viewer, me.getPoint()); if (caret >= 0 && viewer.getDocument() instanceof HTMLDocument) { HTMLDocument hdoc = (HTMLDocument) viewer.getDocument(); Element elem = hdoc.getCharacterElement(caret); if (elem.getAttributes().getAttribute(HTML.Tag.A) != null) { Object attribute = elem.getAttributes().getAttribute(HTML.Tag.A); if (attribute instanceof AttributeSet) { AttributeSet set = (AttributeSet) attribute; final String href = (String) set.getAttribute(HTML.Attribute.HREF); if (href != null) { JPopupMenu popup = new JPopupMenu(); JMenuItem copy = popup.add(jEdit.getProperty("helpviewer.copy-link.label")); copy.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { StringSelection url = new StringSelection(href); Toolkit.getDefaultToolkit().getSystemClipboard().setContents(url, url); } }); popup.show(viewer, me.getX(), me.getY()); } } } } } } //}}} //}}} }