/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* GemCodePanel.java
* Creation date: (1/18/01 1:17:15 PM)
* By: Luke Evans
*/
package org.openquark.gems.client;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.TooManyListenersException;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.TransferHandler;
import javax.swing.text.BadLocationException;
import javax.swing.text.Highlighter;
import javax.swing.text.Highlighter.Highlight;
import org.openquark.cal.compiler.CodeAnalyser;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.services.CALWorkspace;
import org.openquark.cal.services.Perspective;
import org.openquark.gems.client.EditorScrollPane.ErrorOffset;
import org.openquark.gems.client.caleditor.AdvancedCALEditor;
import org.openquark.gems.client.caleditor.AdvancedCALEditor.AmbiguityOffset;
import org.openquark.gems.client.utilities.MouseClickDragAdapter;
/**
* The GemCodePanel is a set of components, most importantly containing an instance of the
* CALEditor. It is designed to allow the definition of a new Gem through the use of CAL
* code. The user types only the RHS of a supercombinator definition into the editor, and the
* CodeGemEditor which contains this panel does the rest.
* Creation date: (1/18/01 1:17:15 PM)
* @author Luke Evans
*/
class GemCodePanel extends JPanel {
private static final long serialVersionUID = -47798849290936051L;
/** Whether the output type is considered good. We keep track of this only to persist undo state. */
private boolean outputGood = true;
/** The displayed icon when the output is locked. */
private static final ImageIcon lockedIcon;
/** The display area for variables in the code panel. */
private VariablesDisplay variablesDisplay;
/** The display are for qualifications in the code panel. */
private QualificationsDisplay qualificationsDisplay;
/** The CAL editor into which code is entered. */
private AdvancedCALEditor ivjCALEditorPane = null;
/** The bottom panel that contains the message labels. */
private JPanel ivjBottomPanel = null;
/** The panel that displays the output type. */
private JLabel ivjOutputTypeLabel = null;
/** The scroll pane that contains the editor text area. */
private EditorScrollPane ivjEditorScrollPane = null;
/** The label for the error message. */
private JLabel ivjMessageLabel = null;
/** The split pane that contains the editor pane and the panel displays. */
private JSplitPane ivjJSplitPane1 = null;
/** The split pane that contains the qualifications & variables displays. */
private JSplitPane displaysSplitPane = null;
/** The perspective this panel was initialized with. */
private final Perspective perspective;
static {
lockedIcon = new ImageIcon(GemCodePanel.class.getResource("/Resources/smallkey.gif"));
}
/**
* A class that can hold the different elements needed for saving and restoring the state.
* @author Edward Lam
*/
private static class PanelState {
final String errorMessage;
final String outputTypeText;
final String outputToolTipText;
final boolean outputGood;
final boolean outputLocked;
private PanelState(GemCodePanel gemCodePanel) {
String errorMessage = gemCodePanel.getMessageLabel().getText();
if (errorMessage != null && errorMessage.length() == 0) {
errorMessage = null;
}
this.errorMessage = errorMessage;
JLabel typeLabel = gemCodePanel.getOutputTypeLabel();
String outputTypeText = typeLabel.getText();
if (outputTypeText != null && outputTypeText.length() == 0) {
outputTypeText = null;
}
this.outputTypeText = outputTypeText;
String outputToolTipText = typeLabel.getToolTipText();
if (outputToolTipText != null && outputToolTipText.length() == 0) {
outputToolTipText = null;
}
this.outputToolTipText = outputToolTipText;
this.outputGood = gemCodePanel.outputGood;
outputLocked = (typeLabel.getIcon() != null);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PanelState)) {
return false;
}
PanelState ps = (PanelState)obj;
return (errorMessage != null ? errorMessage.equals(ps.errorMessage) : true) &&
(outputTypeText != null ? outputTypeText.equals(ps.outputTypeText) : true) &&
(outputToolTipText != null ? outputToolTipText.equals(ps.outputToolTipText) : true) &&
outputGood == ps.outputGood &&
outputLocked == ps.outputLocked;
}
@Override
public int hashCode() {
return 325 * (257 + (errorMessage != null ? errorMessage.hashCode() : 0)) +
197 * (145 + (outputTypeText != null ? outputTypeText.hashCode() : 0)) +
101 * (65 + (outputToolTipText != null ? outputToolTipText.hashCode() : 0)) +
37 * (17 + (outputGood? Boolean.TRUE : Boolean.FALSE).hashCode()) +
(outputLocked ? Boolean.TRUE : Boolean.FALSE).hashCode();
}
}
/**
* A specialized CALEditor that displays a tooltip if the mouse hovers over source errors
* @author Frank Worsley
*/
private class GemCodeEditor extends AdvancedCALEditor {
private static final long serialVersionUID = -8391489120285354572L;
public GemCodeEditor(ModuleTypeInfo moduleTypeInfo, CALWorkspace workspace) {
super(moduleTypeInfo, workspace);
setToolTipText("GemCodeEditor");
}
@Override
public String getToolTipText(MouseEvent e) {
try {
int textOffset = getUI().viewToModel(this, e.getPoint());
// See how closely the returned text offset actually matches the cursor position.
// If the mouse hovers anywhere to the right of the last character on a line, then
// the returned offset will be the offset of that character. This is because that
// character is closest to the mouse position.
Rectangle offsetRect = getUI().modelToView(this, textOffset);
// The offset rectangle is very small, we need to make it a
// little bigger so that it is not to difficult to get a tooltip.
offsetRect.x -= 5;
offsetRect.y -= 5;
offsetRect.width += 10;
offsetRect.height += 10;
if (!offsetRect.contains(e.getPoint())) {
// If the mouse is just hovering on empty space to the right
// of a line, then don't show a tooltip.
return null;
}
// Build tooltip if there is an error at this position
List<ErrorOffset> errorOffsets = ivjEditorScrollPane.getErrorOffsets();
for (final ErrorOffset offset : errorOffsets) {
if (textOffset >= offset.getStartOffset() && textOffset <= offset.getEndOffset()) {
return "<html><body>" + ivjEditorScrollPane.getMessageToolTip(offset.getCompilerMessage()) + "</body></html>";
}
}
// There is no error at this position; so show metadata tooltip via superclass
return super.getToolTipText(e);
} catch (BadLocationException ex) {
throw new IllegalStateException("bad location displaying tooltip");
}
}
}
/**
* Default GemCodePanel constructor.
* @param codeGem the related code gem.
* @param gemCodeSyntaxListener a gem code syntax listener for the code panel
* @param perspective the perspective to use to resolve entity names for tooltips
*/
public GemCodePanel(CodeGem codeGem, GemCodeSyntaxListener gemCodeSyntaxListener, Perspective perspective) {
this.perspective = perspective;
// set the code we're editing
String source = codeGem.getVisibleCode();
getCALEditorPane().setText(source);
setLayout(new BorderLayout());
add(getBottomPanel(), "South");
add(getJSplitPane1(), "Center");
// Push focus onto the editor when we receive focus.
addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
getCALEditorPane().requestFocus();
}
});
// Add a style listener
try {
getCALEditorPane().addCALSyntaxStyleListener(gemCodeSyntaxListener);
} catch (TooManyListenersException e) {
throw new IllegalStateException();
}
}
/**
* Return the source code.
* Creation date: (1/26/01 4:46:19 PM)
* @return the source code
*/
public String getCode() {
return getCALEditorPane().getText();
}
/**
* Adds an indicator for the given compiler message. An indicator will put a little
* 'x' into the side panel next to the line the error occurs in and will highlight
* the source that caused the error.
* @param message
*/
void addErrorIndicator(CodeAnalyser.OffsetCompilerMessage message) {
getEditorScrollPane().addErrorIndicator(message);
setErrorMessage(GemCutter.getResourceString("CGE_Broken"));
}
/**
* Removes all error indicators.
*/
void clearErrorIndicators() {
getEditorScrollPane().clearErrorIndicators();
setErrorMessage(null);
}
/**
* Refreshes the ambiguity indicators on the editor panel
*/
public void updateAmbiguityIndicators() {
ivjCALEditorPane.updateAmbiguityIndicators();
}
/**
* Displays the given error message.
* @param message the error message to display (use null to clear)
*/
void setErrorMessage(String message) {
getMessageLabel().setText(message);
getMessageLabel().setToolTipText(ToolTipHelpers.wrapTextToHTMLLines(message, this));
}
/**
* Set the list of analyzed identifiers from the code
* @param sourceIdentifiers (List of AnalysedIdentifier)
*/
void setSourceIdentifiers(List<CodeAnalyser.AnalysedIdentifier> sourceIdentifiers) {
ivjCALEditorPane.setSourceIdentifiers(sourceIdentifiers);
}
/*
* Methods to access GUI components *******************************************************************
*/
/**
* Return the BottomPanel property value.
* @return JPanel
*/
private JPanel getBottomPanel() {
if (ivjBottomPanel == null) {
try {
ivjBottomPanel = new JPanel();
ivjBottomPanel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
ivjBottomPanel.setLayout(new BoxLayout(ivjBottomPanel, BoxLayout.X_AXIS));
ivjBottomPanel.add(getOutputTypeLabel());
ivjBottomPanel.add(Box.createHorizontalGlue());
ivjBottomPanel.add(getMessageLabel());
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjBottomPanel;
}
/**
* Return the CALEditorPane property value.
* @return org.openquark.gems.client.CALEditor
*/
AdvancedCALEditor getCALEditorPane() {
if (ivjCALEditorPane == null) {
try {
ivjCALEditorPane = new GemCodeEditor(perspective.getWorkingModuleTypeInfo(), perspective.getWorkspace());
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjCALEditorPane;
}
/**
* @return EditorScrollPane the scrollpane for the CALEditor.
*/
private EditorScrollPane getEditorScrollPane() {
if (ivjEditorScrollPane == null) {
try {
ivjEditorScrollPane = new EditorScrollPane(getCALEditorPane());
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjEditorScrollPane;
}
/**
* Get the variables display associated with this panel.
* Creation date: (Jul 16, 2002 4:36:52 PM)
* @return VariablesDisplay the associated VariablesDisplay.
*/
VariablesDisplay getVariablesDisplay() {
if (variablesDisplay == null) {
variablesDisplay = new VariablesDisplay();
variablesDisplay.addListFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
// deselect item if focus lost
variablesDisplay.clearSelection();
}
});
}
return variablesDisplay;
}
/**
* Get the qualifications display associated with this panel.
* @return VariablesDisplay the associated VariablesDisplay.
*/
QualificationsDisplay getQualificationsDisplay() {
if (qualificationsDisplay == null) {
qualificationsDisplay = new QualificationsDisplay(perspective.getWorkspace());
qualificationsDisplay.addListFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
// deselect item if focus lost
qualificationsDisplay.clearSelection();
}
});
}
return qualificationsDisplay;
}
/**
* Return the variables JSplitPane1 .
* @return JSplitPane
*/
private JSplitPane getDisplaysSplitPane() {
if (displaysSplitPane == null) {
try {
displaysSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
displaysSplitPane.setName("JSplitPane1");
displaysSplitPane.setOneTouchExpandable(true);
displaysSplitPane.setTopComponent(getVariablesDisplay());
displaysSplitPane.setBottomComponent(getQualificationsDisplay());
displaysSplitPane.setDividerLocation(0.5);
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return displaysSplitPane;
}
/**
* Return the main JSplitPane1 property value.
* @return JSplitPane
*/
private JSplitPane getJSplitPane1() {
if (ivjJSplitPane1 == null) {
try {
ivjJSplitPane1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
ivjJSplitPane1.setName("JSplitPane1");
ivjJSplitPane1.setDividerSize(2);
getJSplitPane1().add(getEditorScrollPane(), "right");
getJSplitPane1().add(getDisplaysSplitPane(), "left");
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjJSplitPane1;
}
/**
* Return the Message property value.
* @return JLabel
*/
private JLabel getMessageLabel() {
if (ivjMessageLabel == null) {
try {
ivjMessageLabel = new JLabel();
ivjMessageLabel.setName("Message");
ivjMessageLabel.setText("");
ivjMessageLabel.setForeground(Color.red);
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjMessageLabel;
}
/**
* Return the OutputTypeLabel property value.
* @return JLabel
*/
private JLabel getOutputTypeLabel() {
if (ivjOutputTypeLabel == null) {
try {
ivjOutputTypeLabel = new JLabel();
ivjOutputTypeLabel.setName("OutputType");
ivjOutputTypeLabel.setText("-> " + GemCutter.getResourceString("CGE_Undefined_Type"));
} catch (Throwable ivjExc) {
handleException(ivjExc);
}
}
return ivjOutputTypeLabel;
}
/**
* Update the label that displays the output type of the code gem.
* @param typeText the text of the label. If non-null, can optionally be wrapped in \<html\> tags
* @param toolTipText the text to display for the tooltip.
* @param locked whether the output is locked
* @param good whether the output is considered good
*/
void updateOutputTypeLabel(String typeText, String toolTipText, boolean locked, boolean good) {
JLabel typeLabel = getOutputTypeLabel();
typeLabel.setIcon(locked ? lockedIcon : null);
typeLabel.setText(typeText);
if (toolTipText == null) {
typeLabel.setToolTipText(typeText);
} else {
typeLabel.setToolTipText(toolTipText);
}
outputGood = good;
if (good) {
typeLabel.setBackground(Color.black);
typeLabel.setForeground(Color.black);
} else {
typeLabel.setBackground(Color.red);
typeLabel.setForeground(Color.red);
}
}
/**
* Called whenever the part throws an exception.
* @param exception Throwable
*/
private void handleException(Throwable exception) {
/* Uncomment the following lines to print uncaught exceptions to stdout */
System.out.println("--------- UNCAUGHT EXCEPTION ---------");
exception.printStackTrace(System.out);
}
/*
* Methods supporting javax.swing.undo.StateEditable ********************************************
*/
/**
* Restore the stored state.
* This will make the panel consistent with the code gem and displayed code gem, so update those first!
* Creation date: (04/02/2002 6:12:00 PM)
* @param state Hashtable the stored state
*/
public void restoreState(Hashtable<?, ?> state) {
PanelState panelState = (PanelState)state.get(this);
if (panelState != null) {
setErrorMessage(panelState.errorMessage);
updateOutputTypeLabel(panelState.outputTypeText, panelState.outputToolTipText,
panelState.outputLocked, panelState.outputGood);
}
}
/**
* Save the state.
* Creation date: (04/02/2002 6:12:00 PM)
* @param state Hashtable the table in which to store the state
*/
public void storeState(Hashtable<Object, Object> state) {
state.put(this, new PanelState(this));
}
}
/**
* A special scrollpane to use for the CALEditor component. It manages the side panel
* that displays little error icons for lines with errors in them.
* @author Frank Worsley
*/
class EditorScrollPane extends JScrollPane {
private static final long serialVersionUID = -3660183287664833150L;
/**
* This class implements a little side panel that displays error icons
* next to the line of code that has an error in it.
* @author Frank Worsley
*/
private class SidePanel extends JPanel {
private static final long serialVersionUID = -5172117175135917290L;
/**
* Mouse handler to receive clicks on error icons.
*/
private class MouseHandler extends MouseClickDragAdapter {
/**
* Constructor for the Mouse Handler
*/
private MouseHandler() {
}
/**
* Surrogate method for mouseClicked. Called only when our definition of click occurs.
*
* Clicking on an ambiguity indicator causes the editor to select the first
* ambiguity on the line, and display a menu for that identifier.
*
* @param e MouseEvent the relevant event
* @return boolean true if the click was a double click
*/
@Override
public boolean mouseReallyClicked(MouseEvent e){
AdvancedCALEditor editor = EditorScrollPane.this.getEditor();
boolean doubleClicked = super.mouseReallyClicked(e);
for (final AmbiguityOffset offset : getEditor().getAmbiguityOffsets()) {
int y = offset.getLineNumber() * lineHeight + lineOffset;
Rectangle iconRect = new Rectangle(2, y+2, ambiguityIcon.getIconWidth()+2, ambiguityIcon.getIconHeight()+2);
if (iconRect.contains(e.getPoint())) {
// Select ambiguity
editor.select(offset.getStartOffset(), offset.getEndOffset());
// Display menu at ambiguity location
AdvancedCALEditor.IdentifierPopupMenuProvider popupProvider = editor.getPopupMenuProvider();
if (popupProvider == null) {
break;
}
Point menuPoint;
try {
Rectangle offsetRect = editor.getUI().modelToView(editor, offset.getStartOffset());
menuPoint = new Point(offsetRect.x, offsetRect.y + offsetRect.height);
} catch (BadLocationException ex) {
throw new IllegalStateException("bad location displaying ambiguity popup");
}
CodeAnalyser.AnalysedIdentifier identifier = offset.getIdentifier();
AdvancedCALEditor.PositionlessIdentifier positionlessIdentifier =
new AdvancedCALEditor.PositionlessIdentifier(identifier.getName(), identifier.getRawModuleName(), identifier.getResolvedModuleName(), identifier.getMinimallyQualifiedModuleName(), identifier.getCategory(), identifier.getQualificationType());
JPopupMenu menu = popupProvider.getPopupMenu(positionlessIdentifier);
menu.show(getEditor(), menuPoint.x, menuPoint.y);
break;
}
}
return doubleClicked;
}
}
/** The height in pixels of a single line of text. */
private final int lineHeight;
/** The offset from the top to draw the error icon at. */
private final int lineOffset;
/**
* Constructor for a new SidePanel.
*/
public SidePanel() {
FontMetrics metrics = getEditor().getFontMetrics(getEditor().getFont());
Insets margin = getEditor().getMargin();
this.lineHeight = metrics.getHeight();
this.lineOffset = margin != null ? margin.top : 0;
setOpaque(true);
setToolTipText("SidePanel");
setBackground(Color.LIGHT_GRAY);
setPreferredSize(new Dimension(errorIcon.getIconWidth(), getEditor().getHeight()));
addMouseListener(new MouseHandler());
// We need to grow to the same size if the editor resizes
getEditor().addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
setPreferredSize(new Dimension(errorIcon.getIconWidth(), getEditor().getHeight()));
}
});
}
/**
* Draws the error and ambiguity for the side panel.
* If multiple errors or ambiguities occur on the same line, the icons
* will indicate multiplicity.
* If ambiguity icons occur on a line, the line will contain typecheck
* errors, but only the ambiguity icons will be displayed.
*
* @param g the graphics object to draw with
*/
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Show ambiguity indicators
List<Integer> ambiguityLines = new ArrayList<Integer>();
for (final AmbiguityOffset offset : getEditor().getAmbiguityOffsets()) {
Integer lineNum = Integer.valueOf(offset.getLineNumber());
int y = offset.getLineNumber() * lineHeight + lineOffset;
if (!ambiguityLines.contains(lineNum)) {
g.drawImage(ambiguityIcon.getImage(), 2, y+2, this);
ambiguityLines.add(lineNum);
} else {
g.drawImage(ambiguityIcon.getImage(), 4, y+4, this);
}
}
// Show error indicators
List<Integer> errorLines = new ArrayList<Integer>();
for (final ErrorOffset offset : errorOffsets) {
Integer lineNum = Integer.valueOf(offset.getLineNumber());
if (!ambiguityLines.contains(lineNum)) {
// Ambiguity does not have
int y = offset.getLineNumber() * lineHeight + lineOffset;
if (!errorLines.contains(lineNum)) {
g.drawImage(errorIcon.getImage(), -1, y, this);
errorLines.add(lineNum);
} else {
g.drawImage(errorIcon.getImage(), 1, y+1, this);
}
}
}
}
/**
* @param e the MouseEvent that triggered the tooltip
* @return the error description within a tooltip if the mouse is over an error icon.
*/
@Override
public String getToolTipText(MouseEvent e) {
return getToolTipText(e.getPoint());
}
/**
* @param mousePoint the location of the mouse
* @return the error description within a tooltip if the point is over an error icon.
*/
private String getToolTipText(Point mousePoint) {
List<String> messages = new ArrayList<String>();
for (final ErrorOffset offset : errorOffsets) {
int y = offset.getLineNumber() * lineHeight + lineOffset;
Rectangle iconRect = new Rectangle(0, y, errorIcon.getIconWidth(), errorIcon.getIconHeight());
if (iconRect.contains(mousePoint)) {
messages.add(getMessageToolTip(offset.getCompilerMessage()));
}
}
if (messages.isEmpty()) {
return null;
} else if (messages.size() == 1) {
return "<html><body>" + messages.get(0) + "</body></html>";
} else {
StringBuilder buffer = new StringBuilder();
buffer.append("<html><body>");
buffer.append("<b>" + GemCutter.getResourceString("CGE_MultipleErrors") + "</b>");
for (final String message : messages) {
buffer.append("<br><br>");
buffer.append(message);
}
buffer.append("</html></body>");
return buffer.toString();
}
}
}
/**
* A convenience class for storing a compiler message and the associated error position.
* @author Frank Worsley
*/
static class ErrorOffset {
private final CodeAnalyser.OffsetCompilerMessage message;
private final AdvancedCALEditor.EditorLocation errorLocation;
private ErrorOffset(CodeAnalyser.OffsetCompilerMessage message, AdvancedCALEditor.EditorLocation offset) {
if (message == null) {
throw new NullPointerException();
}
this.message = message;
this.errorLocation = offset;
}
public int getStartOffset() {
return errorLocation.getStartOffset();
}
public int getEndOffset() {
return errorLocation.getEndOffset();
}
public int getLineNumber() {
return errorLocation.getLineNumber();
}
public CodeAnalyser.OffsetCompilerMessage getCompilerMessage() {
return message;
}
}
/** The icon to draw to indicate there is an error on a line. */
private static final ImageIcon errorIcon = new ImageIcon(GemCodePanel.class.getResource("/Resources/error.gif"));
/** The icon to draw to indicate there is an ambiguity on a line. */
private static final ImageIcon ambiguityIcon = new ImageIcon(GemCodePanel.class.getResource("/Resources/smallquery.gif"));
/** The side panel that displays the error icons. */
private final SidePanel sidePanel = new SidePanel();
/** The set of error offsets that must be indicated. */
private final List<ErrorOffset> errorOffsets = new ArrayList<ErrorOffset>();
/**
* Constructs a new scrollpane for the given editor.
* @param editor the editor
*/
public EditorScrollPane(AdvancedCALEditor editor) {
super (editor);
setRowHeaderView(sidePanel);
setTransferHandler(new TransferHandler("text"));
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
}
/**
* @return the editor this scrollpane is for
*/
public AdvancedCALEditor getEditor() {
return (AdvancedCALEditor) getViewport().getView();
}
/**
* Adds an error indicate for the given compiler message.
* @param message the message to display an indicator for
*/
public void addErrorIndicator(CodeAnalyser.OffsetCompilerMessage message) {
ErrorOffset offset = getErrorOffset(message);
if (offset == null) {
return;
}
errorOffsets.add(offset);
sidePanel.repaint();
try {
Highlighter highlighter = getEditor().getHighlighter();
highlighter.addHighlight(offset.getStartOffset(), offset.getEndOffset(), new ErrorUnderlineHighlightPainter());
} catch (BadLocationException ex) {
throw new IllegalStateException("bad location adding highlight");
}
}
/**
* Clears all messages and removes all error indicators.
*/
public void clearErrorIndicators() {
// We can't simply remove all highlighters, since if the user has text selected,
// there will be a highlighter to indicate the text selection. This fixes a bug
// where selected text becomes invisible after the syntax smarts run, because
// the selection highlight was removed here.
Highlighter highlighter = getEditor().getHighlighter();
Highlight[] highlights = highlighter.getHighlights();
for (final Highlight highlight : highlights) {
if (highlight.getPainter() instanceof ErrorUnderlineHighlightPainter) {
highlighter.removeHighlight(highlight);
}
}
errorOffsets.clear();
sidePanel.repaint();
}
/**
* @return the ErrorOffsets for the error indicators
*/
public List<ErrorOffset> getErrorOffsets() {
return Collections.unmodifiableList(errorOffsets);
}
/**
* @param message the message to build a tooltip for
* @return an HTML formatted tooltip for the given message
*/
public String getMessageToolTip(CodeAnalyser.OffsetCompilerMessage message) {
StringBuilder buffer = new StringBuilder();
buffer.append("<b>");
buffer.append(ToolTipHelpers.wrapTextToHTMLLines(message.getMessage().replaceAll("<", "<").replaceAll(">", ">"), this));
buffer.append("</b>");
Exception ex = message.getException();
if (ex != null) {
// Add caused by message.
buffer.append("<br>");
buffer.append(ToolTipHelpers.wrapTextToHTMLLines(ex.getMessage().replaceAll("<", "<").replaceAll(">", ">"), this));
}
return buffer.toString();
}
/**
* Converts the source position of the error into a text offset, length, and line number.
* @param message the compiler message to convert the source position for.
*/
private ErrorOffset getErrorOffset(CodeAnalyser.OffsetCompilerMessage message) {
if (!message.hasPosition()) {
return null;
}
try {
AdvancedCALEditor.EditorLocation offset = getEditor().getEditorTokenOffset(message.getOffsetLine(), message.getOffsetColumn(), -1);
return new ErrorOffset(message, offset);
} catch (BadLocationException ex) {
throw new IllegalArgumentException("invalid location trying to convert compiler message source position");
}
}
}
/** Highlight painter for errors */
class ErrorUnderlineHighlightPainter extends AdvancedCALEditor.UnderlineHighlightPainter {
private static final Color LINE_COLOR = Color.RED;
@Override
public Color getLineColor() {
return LINE_COLOR;
}
}