/*
* 12/21/2008
*
* AutoCompleteDescWindow.java - A window containing a description of the
* currently selected completion.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.autocomplete;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import org.fife.ui.rsyntaxtextarea.PopupWindowDecorator;
/**
* The optional "description" window that describes the currently selected
* item in the auto-completion window.
*
* @author Robert Futrell
* @version 1.0
*/
class AutoCompleteDescWindow extends JWindow implements HyperlinkListener,
DescWindowCallback {
/**
* The parent AutoCompletion instance.
*/
private AutoCompletion ac;
/**
* Renders the HTML description.
*/
private JEditorPane descArea;
/**
* The scroll pane that {@link #descArea} is in.
*/
private JScrollPane scrollPane;
/**
* The bottom panel, containing the toolbar and size grip.
*/
private JPanel bottomPanel;
/**
* The toolbar with "back" and "forward" buttons.
*/
private JToolBar descWindowNavBar;
/**
* Action that goes to the previous description displayed.
*/
private Action backAction;
/**
* Action that goes to the next description displayed.
*/
private Action forwardAction;
/**
* History of descriptions displayed.
*/
private List<HistoryEntry> history;
/**
* The current position in {@link #history}.
*/
private int historyPos;
/**
* Provides a slight delay between asking to set a description and actually
* displaying it, so that if the user is scrolling quickly through
* completions, those with slow-to-calculate summaries won't bog down the
* scrolling.
*/
private Timer timer;
/**
* The action that listens for the timer to fire.
*/
private TimerAction timerAction;
/**
* The resource bundle for this window.
*/
private ResourceBundle bundle;
/**
* The amount of time to wait after the user changes the selected
* completion to refresh the description. This delay is in place to help
* performance for {@link Completion}s that may be slow to compute their
* summary text.
*/
private static final int INITIAL_TIMER_DELAY = 120;
/**
* The resource bundle name.
*/
private static final String MSG =
"org.fife.ui.autocomplete.AutoCompleteDescWindow";
/**
* Constructor.
*
* @param owner The parent window.
* @param ac The parent auto-completion.
*/
public AutoCompleteDescWindow(Window owner, AutoCompletion ac) {
super(owner);
this.ac = ac;
ComponentOrientation o = ac.getTextComponentOrientation();
JPanel cp = new JPanel(new BorderLayout());
cp.setBorder(TipUtil.getToolTipBorder());
descArea = new JEditorPane("text/html", null);
TipUtil.tweakTipEditorPane(descArea);
descArea.addHyperlinkListener(this);
scrollPane = new JScrollPane(descArea);
Border b = BorderFactory.createEmptyBorder();
scrollPane.setBorder(b);
scrollPane.setViewportBorder(b);
scrollPane.setBackground(descArea.getBackground());
scrollPane.getViewport().setBackground(descArea.getBackground());
cp.add(scrollPane);
descWindowNavBar = new JToolBar();
backAction = new ToolBarBackAction(o.isLeftToRight());
forwardAction = new ToolBarForwardAction(o.isLeftToRight());
descWindowNavBar.setFloatable(false);
descWindowNavBar.add(new JButton(backAction));
descWindowNavBar.add(new JButton(forwardAction));
bottomPanel = new JPanel(new BorderLayout());
b = new AbstractBorder() {
@Override
public Insets getBorderInsets(Component c) {
return new Insets(1, 0, 0, 0);
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
g.setColor(UIManager.getColor("controlDkShadow"));
g.drawLine(x,y, x+w-1,y);
}
};
bottomPanel.setBorder(b);
SizeGrip rp = new SizeGrip();
bottomPanel.add(descWindowNavBar, BorderLayout.LINE_START);
bottomPanel.add(rp, BorderLayout.LINE_END);
cp.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(cp);
applyComponentOrientation(o);
setFocusableWindowState(false);
// Give apps a chance to decorate us with drop shadows, etc.
if (Util.getShouldAllowDecoratingMainAutoCompleteWindows()) {
PopupWindowDecorator decorator = PopupWindowDecorator.get();
if (decorator!=null) {
decorator.decorate(this);
}
}
history = new ArrayList<HistoryEntry>(1); // Usually small
historyPos = -1;
timerAction = new TimerAction();
timer = new Timer(INITIAL_TIMER_DELAY, timerAction);
timer.setRepeats(false);
}
/**
* Sets the currently displayed description and updates the history.
*
* @param historyItem The item to add to the history.
*/
private void addToHistory(HistoryEntry historyItem) {
history.add(++historyPos, historyItem);
clearHistoryAfterCurrentPos();
setActionStates();
}
/**
* Clears the history of viewed descriptions.
*/
private void clearHistory() {
history.clear(); // Try to free some memory.
historyPos = -1;
if (descWindowNavBar!=null) {
setActionStates();
}
}
/**
* Makes the current history page the last one in the history.
*/
private void clearHistoryAfterCurrentPos() {
for (int i=history.size()-1; i>historyPos; i--) {
history.remove(i);
}
setActionStates();
}
/**
* Copies from the description text area, if it is visible and there is
* a selection.
*
* @return Whether a copy occurred.
*/
public boolean copy() {
if (isVisible() &&
descArea.getSelectionStart()!=descArea.getSelectionEnd()) {
descArea.copy();
return true;
}
return false;
}
/**
* Returns the localized message for the specified key.
*
* @param key The key.
* @return The localized message.
*/
private String getString(String key) {
if (bundle==null) {
bundle = ResourceBundle.getBundle(MSG);
}
return bundle.getString(key);
}
/**
* Called when a hyperlink is clicked.
*
* @param e The event.
*/
public void hyperlinkUpdate(HyperlinkEvent e) {
HyperlinkEvent.EventType type = e.getEventType();
if (!type.equals(HyperlinkEvent.EventType.ACTIVATED)) {
return;
}
// Users can redirect URL's, perhaps to a local copy of documentation.
URL url = e.getURL();
if (url!=null) {
LinkRedirector redirector = AutoCompletion.getLinkRedirector();
if (redirector!=null) {
URL newUrl = redirector.possiblyRedirect(url);
if (newUrl!=null && newUrl!=url) {
url = newUrl;
e = new HyperlinkEvent(e.getSource(), e.getEventType(),
newUrl, e.getDescription(), e.getSourceElement());
}
}
}
// Custom hyperlink handler for this completion type
ExternalURLHandler handler = ac.getExternalURLHandler();
if (handler!=null) {
HistoryEntry current = history.get(historyPos);
handler.urlClicked(e, current.completion, this);
return;
}
// No custom handler...
if (url!=null) {
// Try loading in external browser (Java 6+ only).
try {
Util.browse(new URI(url.toString()));
} catch (/*IO*/URISyntaxException ioe) {
UIManager.getLookAndFeel().provideErrorFeedback(descArea);
ioe.printStackTrace();
}
}
else { // Assume simple function name text, like in c.xml
// FIXME: This is really a hack, and we assume we can find the
// linked-to item in the same CompletionProvider.
AutoCompletePopupWindow parent =
(AutoCompletePopupWindow)getParent();
CompletionProvider p = parent.getSelection().getProvider();
if (p instanceof AbstractCompletionProvider) {
String name = e.getDescription();
List<Completion> l = ((AbstractCompletionProvider)p).
getCompletionByInputText(name);
if (l!=null && !l.isEmpty()) {
// Just use the 1st one if there's more than 1
Completion c = l.get(0);
setDescriptionFor(c, true);
}
else {
UIManager.getLookAndFeel().provideErrorFeedback(descArea);
}
}
}
}
/**
* Enables or disables the back and forward actions as appropriate.
*/
private void setActionStates() {
// TODO: Localize this text!
String desc = null;
if (historyPos>0) {
backAction.setEnabled(true);
desc = "Back to " + history.get(historyPos-1);
}
else {
backAction.setEnabled(false);
}
backAction.putValue(Action.SHORT_DESCRIPTION, desc);
if (historyPos>-1 && historyPos<history.size()-1) {
forwardAction.setEnabled(true);
desc = "Forward to " + history.get(historyPos+1);
}
else {
forwardAction.setEnabled(false);
desc = null;
}
forwardAction.putValue(Action.SHORT_DESCRIPTION, desc);
}
/**
* Sets the description displayed in this window. This clears the
* history.
*
* @param item The item whose description you want to display.
*/
public void setDescriptionFor(Completion item) {
setDescriptionFor(item, false);
}
/**
* Sets the description displayed in this window.
*
* @param item The item whose description you want to display.
* @param addToHistory Whether to add this page to the page history
* (as opposed to clearing it and starting anew).
*/
protected void setDescriptionFor(Completion item, boolean addToHistory) {
setDescriptionFor(item, null, addToHistory);
}
/**
* Sets the description displayed in this window.
*
* @param item The item whose description you want to display.
* @parma anchor The anchor to jump to, or <code>null</code> if none.
* @param addToHistory Whether to add this page to the page history
* (as opposed to clearing it and starting anew).
*/
protected void setDescriptionFor(Completion item, String anchor,
boolean addToHistory) {
timer.stop();
timerAction.setCompletion(item, anchor, addToHistory);
timer.start();
}
private void setDisplayedDesc(Completion completion, final String anchor,
boolean addToHistory) {
String desc = completion==null ? null : completion.getSummary();
if (desc==null) {
desc = "<html><em>" + getString("NoDescAvailable") + "</em>";
}
descArea.setText(desc);
if (anchor!=null) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
descArea.scrollToReference(anchor);
}
});
}
else {
descArea.setCaretPosition(0); // In case of scrolling
}
if (!addToHistory) {
// Remove everything first if this is going to be the only
// thing in history.
clearHistory();
}
addToHistory(new HistoryEntry(completion, desc, null));
}
/**
* {@inheritDoc}
*/
@Override
public void setVisible(boolean visible) {
if (!visible) {
clearHistory();
}
super.setVisible(visible);
}
/**
* Callback for custom <code>ExternalURLHandler</code>s.
*
* @param completion The completion to display.
* @param anchor The anchor in the HTML to jump to, or <code>null</code>
* if none.
*/
public void showSummaryFor(Completion completion, String anchor) {
setDescriptionFor(completion, anchor, true);
}
/**
* Called by the parent completion popup window the LookAndFeel is updated.
*/
public void updateUI() {
SwingUtilities.updateComponentTreeUI(this);
// Update editor pane for new font, bg, selection colors, etc.
TipUtil.tweakTipEditorPane(descArea);
scrollPane.setBackground(descArea.getBackground());
scrollPane.getViewport().setBackground(descArea.getBackground());
((JPanel)getContentPane()).setBorder(TipUtil.getToolTipBorder());
}
/**
* A completion and its cached summary text.
*/
private static class HistoryEntry {
public Completion completion;
public String summary;
public String anchor;
public HistoryEntry(Completion completion, String summary,
String anchor) {
this.completion = completion;
this.summary = summary;
this.anchor = anchor;
}
/**
* Overridden to display a short name for the completion, since it's
* used in the tool tips for the "back" and "forward" buttons.
*
* @return A string representation of this history entry.
*/
@Override
public String toString() {
return completion.getInputText();
}
}
/**
* Action that actually updates the summary text displayed.
*/
private class TimerAction extends AbstractAction {
private Completion completion;
private String anchor;
private boolean addToHistory;
/**
* Called when the timer is fired.
*/
public void actionPerformed(ActionEvent e) {
setDisplayedDesc(completion, anchor, addToHistory);
}
public void setCompletion(Completion c, String anchor,
boolean addToHistory) {
this.completion = c;
this.anchor = anchor;
this.addToHistory = addToHistory;
}
}
/**
* Action that moves to the previous description displayed.
*/
class ToolBarBackAction extends AbstractAction {
public ToolBarBackAction(boolean ltr) {
String img = "org/fife/ui/autocomplete/arrow_" +
(ltr ? "left.png" : "right.png");
ClassLoader cl = getClass().getClassLoader();
Icon icon = new ImageIcon(cl.getResource(img));
putValue(Action.SMALL_ICON, icon);
}
public void actionPerformed(ActionEvent e) {
if (historyPos>0) {
HistoryEntry pair = history.get(--historyPos);
descArea.setText(pair.summary);
if (pair.anchor!=null) {
//System.out.println("Scrolling to: " + pair.anchor);
descArea.scrollToReference(pair.anchor);
}
else {
descArea.setCaretPosition(0);
}
setActionStates();
}
}
}
/**
* Action that moves to the previous description displayed.
*/
class ToolBarForwardAction extends AbstractAction {
public ToolBarForwardAction(boolean ltr) {
String img = "org/fife/ui/autocomplete/arrow_" +
(ltr ? "right.png" : "left.png");
ClassLoader cl = getClass().getClassLoader();
Icon icon = new ImageIcon(cl.getResource(img));
putValue(Action.SMALL_ICON, icon);
}
public void actionPerformed(ActionEvent e) {
if (history!=null && historyPos<history.size()-1) {
HistoryEntry pair = history.get(++historyPos);
descArea.setText(pair.summary);
if (pair.anchor!=null) {
//System.out.println("Scrolling to: " + pair.anchor);
descArea.scrollToReference(pair.anchor);
}
else {
descArea.setCaretPosition(0);
}
setActionStates();
}
}
}
}