package edu.princeton.swing; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import java.util.Hashtable; import javax.swing.*; import javax.swing.border.*; import edu.princeton.swing.text.*; import edu.princeton.toy.*; import edu.princeton.toy.lang.*; /** * PHighlightedTextArea is a multi-purpose text area which supports syntax highlighting, line * highlighting, and column markers. It's important to note that the PHighlightedTextArea is * a JScrollPane, so it is unnecessary (and probably undesireable) to put it in a JScrollPane. * * @author btsang * @version 7.1 */ public class PHighlightedTextArea extends JScrollPane implements TextListener, ClipboardTarget { /** * The value that highlightedLine should take so that the line highlight will follow the * caret. A TOptionFrame will change this to NO_HIGHLIGHT if it is called for. */ public static final int FOLLOW_CARET = -3; /** * The value that highlightedLine should take so that no line is highlighted. A TOptionFrame * will never change this value even if it is called for. */ public static final int NEVER_HIGHLIGHT = -2; /** * The value that highlightedLine should take so that no line is highlighted. A TOptionFrame * will change this to FOLLOW_CARET if it is called for. */ public static final int NO_HIGHLIGHT = -1; /** * The value to pass to select(int, int, int) which will be interpreted as the command * not to change a specified value. */ public static final int NO_CHANGE = HighlightedDocument.PositionTriplet.NO_CHANGE; /** * A static instance of DefaultAutoCompleter for general usage. */ public static final AutoCompleter DEFAULT_AUTO_COMPLETER = new DefaultAutoCompleter(); /** * The default foreground color. */ public static final Color DEFAULT_FOREGROUND = Color.black; /** * The default background color. */ public static final Color DEFAULT_BACKGROUND = Color.white; private static final String CHARACTER_STRINGS[] = new String[128]; private static final Toolkit TOOLKIT = Toolkit.getDefaultToolkit(); private static final Hashtable fontInfoTable = new Hashtable(); private InternalNumberedArea numberedArea; private InternalTextArea textArea; private HighlightStyle highlightStyles[]; private Font plainFont; private Font highlightFonts[]; private boolean supressReaction; private boolean monospaced; private int maxCharacterWidth; private int plainCharacterWidths[]; private int boldCharacterWidths[]; private int italicCharacterWidths[]; private int boldItalicCharacterWidths[]; private int characterHeight; private int lineSpacing; private boolean showLineNumbers; private int columnMarkers[]; private boolean insertMode; private boolean editable; private Insets margin; private HighlightedDocument document; private AutoCompleter autoCompleter; private HighlightedDocument.PositionTriplet selection; private int highlightedLine; private int rows; private int columns; private Color caretColor; private Color lineNumberColor; private Color disabledTextColor; private Color selectedTextColor; private Color selectionColor; private Color highlightedTextColor; private Color highlightColor; private Color markerColor; private JPopupMenu popupMenu; private boolean caretVisible; private boolean keepCaretVisible; private Thread runner; /** * Initialize the CHARACTER_STRINGS array. */ static { for (int ctr = 0; ctr < CHARACTER_STRINGS.length; ctr++) CHARACTER_STRINGS[ctr] = String.valueOf((char)ctr); } /** * Constructs a new PHighlightedTextArea. */ public PHighlightedTextArea() { this(new DefaultHighlightedDocument(), 10, 10); } /** * Constructs a new PHighlightedTextArea. */ public PHighlightedTextArea(HighlightedDocument document) { this(document, 10, 10); } /** * Constructs a new PHighlightedTextArea. */ public PHighlightedTextArea(int rows, int columns) { this(new DefaultHighlightedDocument(), rows, columns); } /** * Constructs a new PHighlightedTextArea. */ public PHighlightedTextArea(HighlightedDocument document, int rows, int columns) { super(VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_AS_NEEDED); setDoubleBuffered(true); if (document == null) throw new NullPointerException(); if (rows <= 0 || columns <= 0) throw new IllegalArgumentException(); supressReaction = false; numberedArea = new InternalNumberedArea(); textArea = new InternalTextArea(); setRowHeaderView(numberedArea); setViewportView(textArea); setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel()); setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel()); insertMode = true; editable = true; this.rows = rows; this.columns = columns; margin = new Insets(6, 6, 6, 6); columnMarkers = new int[0]; showLineNumbers = true; highlightedLine = FOLLOW_CARET; highlightStyles = new HighlightStyle[0]; highlightFonts = new Font[0]; setDocument(document); autoCompleter = DEFAULT_AUTO_COMPLETER; setFont(new Font("Monospaced", Font.PLAIN, 12)); setForeground(DEFAULT_FOREGROUND); setBackground(DEFAULT_BACKGROUND); caretColor = null; lineNumberColor = new Color(148, 148, 148); disabledTextColor = new Color(148, 148, 148); selectedTextColor = null; selectionColor = new Color(204, 204, 255); highlightedTextColor = null; highlightColor = new Color(186, 186, 255); markerColor = new Color(206, 206, 206); ClipboardTargetManager.installMappings(this); popupMenu = ClipboardTargetManager.createDefaultPopupMenu(); recalculateSizes(); } /** * Stops the caret-blinking thread. */ public void finalize() throws Throwable { runner = null; super.finalize(); } /** * Converts a (column, line) coordinate in the text to an actual pixel coordinate in the * PHighlightedTextArea. * * @param p The point in the text (where x is the column and y is the line). * @return The corresponding point in the PHighlightedTextArea. */ public Point modelToView(Point p) { return modelToView(p.x, p.y); } /** * Converts a (column, line) coordinate in the text to an actual pixel coordinate in the * PHighlightedTextArea. * * @param column The column of the text coordinate. * @param line The line of the text coordinate. * @return The corresponding point in the PHighlightedTextArea. */ public Point modelToView(int column, int line) { return new Point( margin.left + column * maxCharacterWidth, margin.top + (line + 1) * characterHeight + line * lineSpacing ); } /** * Converts an actual pixel coordinate in the PHighlightedTextArea to a (column, line) * coordinate in the text. * * @param p The point in the PHighlightedTextArea. * @return The corresponding point in the text (where x is the column and y is the line). */ public Point viewToModel(Point p) { return viewToModel(p.x, p.y); } /** * Converts an actual pixel coordinate in the PHighlightedTextArea to a (column, line) * coordinate in the text. * * @param x The x component of the pixel coordinate. * @param y The y component of the pixel coordinate. * @return The corresponding point in the text (where x is the column and y is the line). */ public Point viewToModel(int x, int y) { return new Point( (x - margin.left + maxCharacterWidth / 2) / maxCharacterWidth, (y - margin.top + lineSpacing - characterHeight / 2) / (characterHeight + lineSpacing) ); } /** * Gets the popup menu for this component. * * @return The popup menu for the component. Null is returned if this component does not have * a popup menu. */ public JPopupMenu getPopupMenu() { return popupMenu; } /** * Sets the popup menu for this component. * * @param popupMenu The popup menu for the component. A null value indicates that this * component should not have a popup menu. */ public void setPopupMenu(JPopupMenu popupMenu) { if (popupMenu != this.popupMenu) { if (popupMenu != null && this.popupMenu.isVisible()) { this.popupMenu.setVisible(false); } JPopupMenu oldValue = this.popupMenu; this.popupMenu = popupMenu; firePropertyChange("popupMenu", oldValue, popupMenu); } } /** * Override the setEnabled() method to intercept any changes concerning the enabled status of * the component. */ public void setEnabled(boolean enabled) { if (isEnabled() != enabled) { super.setEnabled(enabled); if (editable && isEnabled()) { setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } ClipboardTargetManager.targetAbilityChanged(textArea); } } /** * Gets the color of the caret. A null value indicates that the foreground color should be * used to color the caret. * * @return The color of the caret. */ public Color getCaretColor() { return caretColor; } /** * Sets the color of the caret. A null value indicates that the foreground color should be * used to color the caret. * * @param caretColor The new color of the caret. */ public void setCaretColor(Color caretColor) { if (caretColor != this.caretColor && (caretColor == null || !caretColor.equals(this.caretColor))) { Color oldValue = this.caretColor; this.caretColor = caretColor; firePropertyChange("caretColor", oldValue, caretColor); repaint(); } } /** * Gets the color of the line numbers. A null value indicates that the numbers will take * on the foreground color. * * @return The color of the line numbers. */ public Color getLineNumberColor() { return lineNumberColor; } /** * Sets the color of the line numbers. A null value indicates that the numbers should take * on the foreground color. * * @param lineNumberColor The color of the line numbers. */ public void setLineNumberColor(Color lineNumberColor) { if (lineNumberColor != this.lineNumberColor && (lineNumberColor == null || !lineNumberColor.equals(this.lineNumberColor))) { Color oldValue = this.lineNumberColor; this.lineNumberColor = lineNumberColor; firePropertyChange("lineNumberColor", oldValue, lineNumberColor); repaint(); } } /** * Gets the color for the text if this component is disabled. A null value indicates that the * text should be colored the same wheter or not this component is disabled. * * @return The color for the text if this component is disabled. */ public Color getDisabledTextColor() { return disabledTextColor; } /** * Sets the color for the text if this component is disabled. A null value indicates that the * text should be colored the same wheter or not this component is disabled. * * @param disabledTextColor The color for the text if this component is disabled. */ public void setDisabledTextColor(Color disabledTextColor) { if (disabledTextColor != this.disabledTextColor && (disabledTextColor == null || !disabledTextColor.equals(this.disabledTextColor))) { Color oldValue = this.disabledTextColor; this.disabledTextColor = disabledTextColor; firePropertyChange("disabledTextColor", oldValue, disabledTextColor); repaint(); } } /** * Indicates wheter or not the lines are numbered. * * @return True iff the line numbers are being shown. */ public boolean getShowLineNumbers() { return this.showLineNumbers; } /** * Sets wheter or not the lines should be numbered. * * @param showLineNumbers True iff the lines should be numbered. */ public void setShowLineNumbers(boolean showLineNumbers) { if (showLineNumbers != this.showLineNumbers) { boolean oldValue = this.showLineNumbers; this.showLineNumbers = showLineNumbers; if (showLineNumbers) setRowHeaderView(numberedArea); else setRowHeaderView(null); firePropertyChange("showLineNumbers", oldValue, showLineNumbers); } } /** * Returns wheter or not this component is editable by the user. * * @return True iff the user can edit this component by clicking on it and typing. */ public boolean isEditable() { return editable; } /** * Sets wheter or not this component is editable by the user. * * @param editable The new value for editable. */ public void setEditable(boolean editable) { if (this.editable != editable) { boolean oldValue = this.editable; this.editable = editable; if (editable && isEnabled()) { textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); } else { textArea.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } firePropertyChange("editable", oldValue, editable); ClipboardTargetManager.targetAbilityChanged(textArea); repaint(); } } /** * Returns wheter or not this component is in insert mode. * * @return True iff the user this component is in insert mode. */ public boolean getInsertMode() { return insertMode; } /** * Sets wheter or not this component is in insert mode. * * @param insertMode The new value for insertMode. */ public void setInsertMode(boolean insertMode) { if (insertMode != this.insertMode) { boolean oldValue = this.insertMode; this.insertMode = insertMode; firePropertyChange("insertMode", oldValue, insertMode); caretVisible = true; keepCaretVisible = true; scrollToCaret(); } } /** * Gets the color for the column marker. A null value indicates that the marker should not be * drawn. * * @return The color for the column marker. */ public Color getMarkerColor() { return markerColor; } /** * Sets the color for the column marker. A null value indicates that the marker should not be * drawn. * * @param markerColor The color for the column marker. */ public void setMarkerColor(Color markerColor) { if (markerColor != this.markerColor && (markerColor == null || !markerColor.equals(this.markerColor))) { Color oldValue = this.markerColor; this.markerColor = markerColor; firePropertyChange("markerColor", oldValue, markerColor); repaint(); } } /** * Gets the location of the column markers. * * @return The columns at which markers are present. A copy of the internal array will be * returned. */ public int[] getColumnMarkers() { int answer[] = new int[columnMarkers.length]; for (int ctr = 0; ctr < columnMarkers.length; ctr++) answer[ctr] = columnMarkers[ctr]; return answer; } /** * Sets the location of the column markers. A null value will result in a NullPointerException. * A negative column will result in an IllegalArgumentException. * * @param columnMarkers The columns at which to put markers. */ public void setColumnMarkers(int columnMarkers[]) { if (columnMarkers != this.columnMarkers) { int oldValue[] = this.columnMarkers; this.columnMarkers = new int[columnMarkers.length]; for (int ctr = 0; ctr < columnMarkers.length; ctr++) { if (columnMarkers[ctr] < 0) throw new IllegalArgumentException(); this.columnMarkers[ctr] = columnMarkers[ctr]; } firePropertyChange("columnMarkers", oldValue, columnMarkers); repaint(); } } /** * Returns the location of the caret. * * @return The offset of the caret with respect to the start of the text. */ public int getCaretPosition() { return selection.getCaretPositionOffset(); } /** * Sets the location of the caret. This will cause the current selection to be lost. * * @param caretPosition The new offset of the caret with respect to the start of the text. * A NO_CHANGE value will cause the entire selection to move to the caret's current position. */ public void setCaretPosition(int caretPosition) { if (caretPosition == NO_CHANGE) caretPosition = selection.getCaretPositionOffset(); select(caretPosition, caretPosition, caretPosition); } /** * Gets the color for the text which is in the selected range. A null value indicates that the * text should be colored the same wheter or not it is selected. * * @return The color for the text which is selected. */ public Color getSelectedTextColor() { return selectedTextColor; } /** * Sets the color for the text which is in the selected range. A null value indicates that the * text should be colored the same wheter or not it is selected. * * @param selectedTextColor The color for the text which is selected. */ public void setSelectedTextColor(Color selectedTextColor) { if (selectedTextColor != this.selectedTextColor && (selectedTextColor == null || !selectedTextColor.equals(this.selectedTextColor))) { Color oldValue = this.selectedTextColor; this.selectedTextColor = selectedTextColor; firePropertyChange("selectedTextColor", oldValue, selectedTextColor); repaint(); } } /** * Gets the color for the background which is in the selected range. A null value indicates * that the background should be colored the same wheter or not it is selected. * * @return The color for the background which is selected. */ public Color getSelectionColor() { return selectionColor; } /** * Sets the color for the background which is in the selected range. A null value indicates * that the background should be colored the same wheter or not it is selected. * * @param selectionColor The color for the background which is selected. */ public void setSelectionColor(Color selectionColor) { if (selectionColor != this.selectionColor && (selectionColor == null || !selectionColor.equals(this.selectionColor))) { Color oldValue = this.selectionColor; this.selectionColor = selectionColor; firePropertyChange("selectionColor", oldValue, selectionColor); repaint(); } } /** * Gets the start of the selection. If nothing is selected, getSelectionStart() will return * the same number as getSelectionEnd(), but they might not be the same as getCaretPosition(). * * @return The offset at which the selection begins. */ public int getSelectionStart() { return selection.getSelectionStartOffset(); } /** * Gets the end of the selection. If nothing is selected, getSelectionEnd() will return * the same number as getSelectionStart(), but they might not be the same as getCaretPosition(). * * @return The offset at which the selection ends. */ public int getSelectionEnd() { return selection.getSelectionEndOffset(); } /** * Gets the end of the selection which is supposed to 'move'. This is generally the same * as the caret's position, but not necessarily so. * * @return The offset at which the selection begins. */ public int getSelectionDot() { return selection.getSelectionDotOffset(); } /** * Gets the end of the selection which is supposed to be 'anchored'. * * @return The offset at which the selection ends. */ public int getSelectionMark() { return selection.getSelectionMarkOffset(); } /** * Returns the selected text. * * @return The text between the start of the selection and the end of the selection. */ public String getSelectedText() { return document.getText( selection.getSelectionStartOffset(), selection.getSelectionEndOffset() ); } /** * Returns wheter or not the selectAll operation can be performed on this ClipboardTarget. * * @return True iff a call to selectAll() should be permitted. */ public boolean canSelectAll() { return isEnabled(); } /** * Selects all of the text. */ public void selectAll() { select(0, Integer.MAX_VALUE, Integer.MAX_VALUE); } /** * Selects the specified range of text. The caret will be moved to the offset specified by * selectionDot. * * @param selectionDot The new position of the selection's dot. A value of NO_CHANGE will just * move the caret to the dot's current position. * @param selectionMark The new position of the selection's mark. */ public void select(int selectionDot, int selectionMark) { if (selectionDot == NO_CHANGE) selectionDot = selection.getSelectionDotOffset(); select(selectionDot, selectionMark, selectionDot); } /** * Selects the specified range of text and moves the caret to a specified location. * * @param selectionDot The new position of the selection's dot. * @param selectionMark The new position of the selection's mark. * @param caretPosition The new position of the caret. */ public void select(int selectionDot, int selectionMark, int caretPosition) { if (selectionDot != NO_CHANGE || selectionMark != NO_CHANGE || caretPosition != NO_CHANGE) { selection.set(selectionDot, selectionMark, caretPosition); // Todo: Generate a caret event? caretVisible = true; keepCaretVisible = true; scrollToCaret(); } } /** * Gets the color for the text in a line which is highlighted. A null value indicates that the * text should be colored the same wheter or not it is highlighted. * * @return The color for the text in a line which is highlighted. */ public Color getHighlightedTextColor() { return highlightedTextColor; } /** * Sets the color for the text in a line which is highlighted. A null value indicates that the * text should be colored the same wheter or not it is highlighted. * * @param highlightedTextColor The color for the text in a line which is highlighted. */ public void setHighlightedTextColor(Color highlightedTextColor) { if (highlightedTextColor != this.highlightedTextColor && (highlightedTextColor == null || !highlightedTextColor.equals(this.highlightedTextColor))) { Color oldValue = this.highlightedTextColor; this.highlightedTextColor = highlightedTextColor; firePropertyChange("highlightedTextColor", oldValue, highlightedTextColor); repaint(); } } /** * Gets the color for the background in a line which is highlighted. A null value indicates * that the the background should be colored the same wheter or not it is highlighted. * * @return The color for the background in a line which is highlighted. */ public Color getHighlightColor() { return highlightColor; } /** * Sets the color for the background in a line which is highlighted. A null value indicates * that the the background should be colored the same wheter or not it is highlighted. * * @param highlightColor The color for the background in a line which is highlighted. */ public void setHighlightColor(Color highlightColor) { if (highlightColor != this.highlightColor && (highlightColor == null || !highlightColor.equals(this.highlightColor))) { Color oldValue = this.highlightColor; this.highlightColor = highlightColor; firePropertyChange("highlightColor", oldValue, highlightColor); repaint(); } } /** * Gets the line which is to be highlighted. Note that FOLLOW_CARET and NO_HIGHLIGHT are * special values that this can take. * * @return The line which is to be highlighted. */ public int getHighlightedLine() { return highlightedLine; } /** * Sets the line which is to be highlighted. Note that FOLLOW_CARET and NO_HIGHLIGHT are * special values that this can take. * * @param highlightedLine The line which is to be highlighted. If the specified line is neither * FOLLOW_CARET nor a line that exists in the text, then no line will be highlighted. */ public void setHighlightedLine(int highlightedLine) { if (highlightedLine != this.highlightedLine) { int oldValue = this.highlightedLine; this.highlightedLine = highlightedLine; firePropertyChange("highlightedLine", oldValue, highlightedLine); repaint(); } } // checks if the mouse is currently on an automatically generated pseudocomment private boolean isInPseudoCode() { int offset = selection.getSelectionDotOffset(); // get the caret position Point coordinate = document.offsetToCoordinate(offset); // get the current line String line = document.getText(offset - coordinate.x, document.coordinateToOffset(Integer.MAX_VALUE, coordinate.y)); int length = line.length(); // Check if the line already has an instruction + pseudocode or not boolean hasInstruction = false; if (length >= 8) hasInstruction = TWord.isCommand(line.substring(0, 8)); // Check if some text is selected (highlited) Point selStart = document.offsetToCoordinate(getSelectionStart()); boolean startIn = selStart.x >= 8 && selStart.x < TProgramDocument.COMMENT_COLUMN; return hasInstruction && startIn; } /** * Returns wheter or not the cut operation can be performed on this ClipboardTarget. * * @return True iff a call to cut() should be permitted. */ public boolean canCut() { return editable && isEnabled(); } /** * Performs the cut operation on the selected text. */ public void cut() { if (!canCut() || isInPseudoCode()) { System.out.println("Cant Cut"); java.awt.Toolkit.getDefaultToolkit().beep(); return; } int selectionStartOffset = selection.getSelectionStartOffset(); int selectionEndOffset = selection.getSelectionEndOffset(); supressReaction = true; if (selectionStartOffset != selectionEndOffset) { try { Clipboard clipboard = TOOLKIT.getSystemClipboard(); StringSelection stringSelection = new StringSelection( document.getText(selectionStartOffset, selectionEndOffset) ); clipboard.setContents(stringSelection, stringSelection); document.remove( selectionStartOffset, selectionEndOffset - selectionStartOffset, false ); selectionStartOffset = selection.getSelectionStartOffset(); select(selectionStartOffset, selectionStartOffset, selectionStartOffset); } catch (Exception e) { } } recalculateSizes(); caretVisible = true; keepCaretVisible = true; scrollToCaret(); supressReaction = false; } /** * Returns wheter or not the copy operation can be performed on this ClipboardTarget. * * @return True iff a call to copy() should be permitted. */ public boolean canCopy() { return isEnabled(); } /** * Performs the copy operation on the selected text. */ public void copy() { if (!isEnabled()) return; int selectionStartOffset = selection.getSelectionStartOffset(); int selectionEndOffset = selection.getSelectionEndOffset(); if (selectionStartOffset != selectionEndOffset) { try { Clipboard clipboard = TOOLKIT.getSystemClipboard(); StringSelection selection = new StringSelection( document.getText(selectionStartOffset, selectionEndOffset) ); clipboard.setContents(selection, selection); } catch (Exception e) { } } } /** * Returns wheter or not the paste operation can be performed on this ClipboardTarget. * * @return True iff a call to paste() should be permitted. */ public boolean canPaste() { return editable && isEnabled(); } /** * Performs the paste operation on the selected text. */ public void paste() { if (!canPaste() || isInPseudoCode()) { System.out.println("Cant paste"); java.awt.Toolkit.getDefaultToolkit().beep(); return; } int selectionStartOffset = selection.getSelectionStartOffset(); int selectionEndOffset = selection.getSelectionEndOffset(); supressReaction = true; try { Clipboard clipboard = TOOLKIT.getSystemClipboard(); String string = (String)(clipboard.getContents(this)).getTransferData( DataFlavor.stringFlavor ); if (selectionStartOffset != selectionEndOffset) { document.replace( selectionStartOffset, selectionEndOffset - selectionStartOffset, string, false ); } else { if (insertMode) { document.insertString(selectionStartOffset, string, false); } else { document.replace(selectionStartOffset, string.length(), string, false); } } selectionStartOffset = selection.getSelectionStartOffset(); select(selectionStartOffset, selectionStartOffset, selectionStartOffset); } catch (Exception e) { } recalculateSizes(); caretVisible = true; keepCaretVisible = true; scrollToCaret(); supressReaction = false; } /** * Returns the text of the document. * * @return The text of the document. */ public String getText() { return document.getText(); } /** * Sets the text of the document. * * @param text The new text of the document. */ public void setText(String text) { document.setText(text); select(0, 0, 0); } /** * Returns the document that this PHighlightedTextArea represents. * * @return The document of this component. */ public HighlightedDocument getDocument() { return document; } /** * Sets the document that this PHighlightedTextArea represents. * * @param document The new document that this PHighlightedTextArea should represent. */ public void setDocument(HighlightedDocument document) { if (document == null) throw new NullPointerException(); if (document != this.document) { if (this.document != null) { this.document.removeTextListener(this); this.document.freePositionTriplet(selection); } HighlightedDocument oldValue = this.document; this.document = document; document.addTextListener(this); selection = document.createPositionTriplet(); int styleCount = document.getStyleCount(); if (highlightStyles.length != styleCount) { highlightStyles = new HighlightStyle[styleCount]; highlightFonts = new Font [styleCount]; } for (int ctr = 0; ctr < styleCount; ctr++) { highlightStyles[ctr] = HighlightStyle.DEFAULT_STYLE; highlightFonts[ctr] = plainFont; } select(0, 0, 0); firePropertyChange("document", oldValue, document); recalculateSizes(); } } /** * Override the setFont function to capture changes in the font. Note that a null font is no * longer acceptable. */ public void setFont(Font font) { if (highlightStyles == null) return; if (font == null) throw new NullPointerException(); String name = font.getName(); int size = font.getSize(); margin.top = size / 2; margin.left = size / 2; margin.bottom = size / 2; margin.right = size / 2; plainFont = new Font( name, HighlightStyle.DEFAULT_STYLE.getStyleMask(), size ); FontInfo fontInfo = (FontInfo)fontInfoTable.get(plainFont); if (fontInfo == null) { fontInfo = new FontInfo(plainFont, HighlightedDocument.CHARACTER_ALLOWED); fontInfoTable.put(plainFont, fontInfo); } monospaced = fontInfo.isMonospaced(); maxCharacterWidth = fontInfo.getMaxCharacterWidth(); plainCharacterWidths = fontInfo.getPlainCharacterWidths(); boldCharacterWidths = fontInfo.getBoldCharacterWidths(); italicCharacterWidths = fontInfo.getItalicCharacterWidths(); boldItalicCharacterWidths = fontInfo.getBoldItalicCharacterWidths(); characterHeight = fontInfo.getCharacterHeight(); lineSpacing = fontInfo.getLineSpacing(); for (int ctr = 0; ctr < highlightStyles.length; ctr++) { highlightFonts[ctr] = new Font( name, highlightStyles[ctr].getStyleMask(), size ); } super.setFont(font); recalculateSizes(); } /** * Returns the number of styles represented in this JHighlightTextArea. * * @return The number of styles in the document. */ public int getStyleCount() { return highlightStyles.length; } /** * Gets the style for a specified index. * * @param index The index of the style. * @return The style at that index. */ public HighlightStyle getStyle(int index) { return highlightStyles[index]; } /** * Gets the style for a specified index. * * @param style The new style for that index. * @param index The index of the style. */ public void setStyle(HighlightStyle style, int index) { if (style == null) throw new NullPointerException(); if (highlightStyles[index] != style) { HighlightStyle oldValue = highlightStyles[index]; highlightStyles[index] = style; highlightFonts[index] = new Font( getFont().getName(), style.getStyleMask(), getFont().getSize() ); firePropertyChange("highlightStyles[" + index + "]", oldValue, style); repaint(); } } /** * Returns the AutoCompleter for this PHighlightedTextArea. * * @return The AutoCompleter for this PHighlightedTextArea. */ public AutoCompleter getAutoCompleter() { return autoCompleter; } /** * Sets the AutoCompleter for this PHighlightedTextArea. * * @param autoCompleter The new AutoCompleter for this PHighlightedTextArea. */ public void setAutoCompleter(AutoCompleter autoCompleter) { if (autoCompleter == null) throw new NullPointerException(); if (this.autoCompleter != autoCompleter) { AutoCompleter oldValue = this.autoCompleter; this.autoCompleter = autoCompleter; firePropertyChange("autoCompleter", oldValue, autoCompleter); } } /** * Recalculates the preferred/minimum sizes of the InternalNumberedArea and InternalTextArea. */ public void recalculateSizes() { int lineCount = document.getLineCount(); int maxLineLength = document.getMaxLineLength(); Dimension textAreaPreferredSize = textArea.getPreferredSize(); Dimension textAreaMinimumSize = textArea.getMinimumSize(); textAreaMinimumSize.width = columns * maxCharacterWidth + margin.left + margin.right; textAreaMinimumSize.height = rows * (characterHeight + lineSpacing) - lineSpacing + margin.top + margin.bottom; textAreaPreferredSize.width = maxCharacterWidth * (maxLineLength + 1) + margin.left + margin.right; if (textAreaPreferredSize.width < textAreaMinimumSize.width) textAreaPreferredSize.width = textAreaMinimumSize.width; textAreaPreferredSize.height = (characterHeight + lineSpacing) * lineCount + lineSpacing + margin.top + margin.bottom; if (textAreaPreferredSize.height < textAreaMinimumSize.height) textAreaPreferredSize.height = textAreaMinimumSize.height; textArea.setPreferredSize(textAreaPreferredSize); textArea.setMinimumSize(textAreaMinimumSize); numberedArea.lineCount = lineCount; int digitCount = 1; while (lineCount >= 10) { lineCount /= 10; digitCount++; } // force the number of digits to be at least 2 digitCount = digitCount >= 2 ? digitCount : 2; numberedArea.digitCount = digitCount; Dimension numberedAreaPreferredSize = numberedArea.getPreferredSize(); Dimension numberedAreaMinimumSize = numberedArea.getMinimumSize(); numberedAreaPreferredSize.width = maxCharacterWidth * (digitCount + 1) + margin.left + margin.right; numberedAreaPreferredSize.height = textAreaPreferredSize.height; numberedAreaMinimumSize.width = numberedAreaPreferredSize.width; numberedAreaMinimumSize.height = textAreaMinimumSize.height; numberedArea.setPreferredSize(numberedAreaPreferredSize); numberedArea.setMinimumSize(numberedAreaMinimumSize); textArea.revalidate(); numberedArea.revalidate(); } /** * If this PHighlightedTextArea is in a JViewport, the viewport's viewPosition will be changed * so that the caret (or the highlighted line, if the caret is not visible) is showing. If * this PHighlightedTextArea is not in a viewport or if neither a caret nor a highlighted line * is visible, then repaint() will be called and the method will terminate. */ public void scrollToCaret() { if ((textArea.hasFocus() || highlightedLine == FOLLOW_CARET || highlightedLine >= 0 && highlightedLine < document.getLineCount())) { Rectangle significantRect; if (textArea.hasFocus()) { significantRect = new Rectangle( modelToView(document.offsetToCoordinate(selection.getCaretPositionOffset())) ); } else if (highlightedLine == FOLLOW_CARET) { significantRect = new Rectangle( modelToView( 0, document.offsetToCoordinate(selection.getCaretPositionOffset()).y ) ); } else { significantRect = new Rectangle( modelToView(0, highlightedLine) ); } significantRect.y -= characterHeight; significantRect.height = characterHeight + lineSpacing; significantRect.x -= maxCharacterWidth / 2; significantRect.width = maxCharacterWidth; textArea.scrollRectToVisible(significantRect); } repaint(); } /** * Implement TextListener to recalculate things whenever the document is edited. */ public void textValueChanged(TextEvent e) { if (!supressReaction) { recalculateSizes(); scrollToCaret(); } } /** * InternalNumberedArea is the subcomponent of PHighlightedTextArea which shows the * line numbers. * * @version 7.1 * @author btsang */ protected class InternalNumberedArea extends JComponent { private int anchorLine; protected int lineCount; protected int digitCount; protected InternalNumberedArea() { enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } /** * @todo Stop using this method when more people switch to 1.4. * @see Component#isFocusTraversable() */ public boolean isFocusTraversable() { return false; } /** * Override the paintComponent() function of JComponent. */ public void paintComponent(Graphics g) { Rectangle clip = g.getClipBounds(); Point p; char chars[] = new char[digitCount]; // Localize the demi-constants boolean monospaced = PHighlightedTextArea.this.monospaced; int maxCharacterWidth = PHighlightedTextArea.this.maxCharacterWidth; int characterHeight = PHighlightedTextArea.this.characterHeight; int lineSpacing = PHighlightedTextArea.this.lineSpacing; int caretPositionOffset = selection.getCaretPositionOffset(); Point caretPositionPoint = document.offsetToCoordinate(caretPositionOffset); int actualHighlightedLine = highlightedLine; if (actualHighlightedLine == FOLLOW_CARET) { actualHighlightedLine = caretPositionPoint.y; } else if (actualHighlightedLine < 0 || actualHighlightedLine >= lineCount) { actualHighlightedLine = NO_HIGHLIGHT; } // Paint background { Color background = PHighlightedTextArea.this.getBackground(); if (background != null) g.setColor(background); else g.setColor(DEFAULT_BACKGROUND); } g.fillRect(clip.x, clip.y, clip.width, clip.height); // Paint highlighted line if (actualHighlightedLine != NO_HIGHLIGHT && highlightColor != null) { int y = modelToView(0, actualHighlightedLine).y; g.setColor(highlightColor); g.drawLine( clip.x, y - characterHeight, clip.x + clip.width, y - characterHeight ); g.drawLine( clip.x, y + lineSpacing, clip.x + clip.width, y + lineSpacing ); } // Paint the line numbers int lineStart = Math.max(0, viewToModel(clip.x, clip.y).y); int lineEnd = Math.min(lineCount, viewToModel(clip.x, clip.y + clip.height).y + 1); if (lineNumberColor != null) g.setColor(lineNumberColor); else { Color foreground = PHighlightedTextArea.this.getForeground(); if (foreground != null) g.setColor(foreground); else g.setColor(DEFAULT_FOREGROUND); } Font font = PHighlightedTextArea.this.getFont(); g.setFont(font); p = modelToView(0, lineStart); for (int lineCtr = lineStart; lineCtr < lineEnd; lineCtr++) { // Temporarily change the foreground color if we need to highlight the line if (lineCtr == actualHighlightedLine && highlightedTextColor != null) g.setColor(highlightedTextColor); // Render the line number onto a set of characters. int tempInt = lineCtr + 1; for (int ctr = digitCount - 1; ctr >= 0; ctr--) { if (tempInt == 0) { chars[ctr] = ' '; } else { chars[ctr] = (char)('0' + tempInt % 10); tempInt /= 10; } } // Draw the line number if (monospaced) { g.drawChars(chars, 0, digitCount, p.x, p.y); } else { int characterWidths[]; if (font.isBold()) { if (font.isItalic()) characterWidths = boldItalicCharacterWidths; else characterWidths = boldCharacterWidths; } else { if (font.isItalic()) characterWidths = italicCharacterWidths; else characterWidths = plainCharacterWidths; } for (int ctr = 0; ctr < digitCount; ctr++) { g.drawChars( chars, ctr, 1, p.x + ctr * maxCharacterWidth + (maxCharacterWidth - characterWidths[chars[ctr]]) / 2, p.y ); } } // Increment p.y += characterHeight + lineSpacing; // Restore the the forground color if we had changed it for the highlight if (lineCtr == actualHighlightedLine && highlightedTextColor != null) { if (lineNumberColor != null) g.setColor(lineNumberColor); else { Color foreground = PHighlightedTextArea.this.getForeground(); if (foreground != null) g.setColor(foreground); else g.setColor(DEFAULT_FOREGROUND); } } } } /** * Intercept mouse clicks to perform the usual action. */ protected void processMouseEvent(MouseEvent e) { if (!PHighlightedTextArea.this.isEnabled()) return; boolean passedToSuper = false; if (popupMenu != null && e.isPopupTrigger()) { if (!textArea.hasFocus()) textArea.requestFocus(); int x = e.getX(); int y = e.getY(); Point componentLocation = getLocationOnScreen(); Dimension screenSize = TOOLKIT.getScreenSize(); Dimension preferredSize = popupMenu.getPreferredSize(); if (componentLocation.x + x + preferredSize.width > screenSize.width) x -= preferredSize.width; if (componentLocation.y + y + preferredSize.height > screenSize.height) y -= preferredSize.height; e.consume(); passedToSuper = true; super.processMouseEvent(e); popupMenu.show(this, x, y); repaint(); } else if (e.getID() == MouseEvent.MOUSE_PRESSED && (e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { if (!textArea.hasFocus()) textArea.requestFocus(); Point coordinate = viewToModel(e.getX(), e.getY()); int offset; anchorLine = coordinate.y; offset = document.coordinateToOffset(0, coordinate.y); select( offset, document.coordinateToOffset(Integer.MAX_VALUE, coordinate.y), offset ); caretVisible = true; keepCaretVisible = true; scrollToCaret(); e.consume(); } if (!passedToSuper) super.processMouseEvent(e); } /** * Intercept mouse dragging to perform the usual action. */ protected void processMouseMotionEvent(MouseEvent e) { if (!PHighlightedTextArea.this.isEnabled()) return; if (e.getID() == MouseEvent.MOUSE_DRAGGED && (e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { Point coordinate = viewToModel(e.getX(), e.getY()); int offset; if (coordinate.y <= anchorLine) { offset = document.coordinateToOffset(0, coordinate.y); select( offset, document.coordinateToOffset(Integer.MAX_VALUE, anchorLine), offset ); } else { select( document.coordinateToOffset(0, anchorLine), document.coordinateToOffset(Integer.MAX_VALUE, coordinate.y), document.coordinateToOffset(0, coordinate.y) ); } caretVisible = true; keepCaretVisible = true; scrollToCaret(); e.consume(); } super.processMouseMotionEvent(e); } } /** * InternalTextArea is the subcomponent of PHighlightedTextArea which shows the editable * program code. * * @version 7.1 * @author btsang */ protected class InternalTextArea extends JComponent implements Runnable, HighlightedDocumentRenderer, Scrollable, ClipboardTarget { protected InternalTextArea() { setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); enableEvents(AWTEvent.FOCUS_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK); } /** * @todo Stop using this method when more people switch to 1.4. * @see JComponent#isManagingFocus() */ public boolean isManagingFocus() { return true; } /** * @todo Stop using this method when more people switch to 1.4. * @see Component#isFocusTraversable() */ public boolean isFocusTraversable() { return true; } /** * Override the paintComponent() function of JComponent. */ public void paintComponent(Graphics g) { document.allowRender(this, g); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public boolean canCut() { return PHighlightedTextArea.this.canCut(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public void cut() { PHighlightedTextArea.this.cut(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public boolean canCopy() { return PHighlightedTextArea.this.canCopy(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public void copy() { PHighlightedTextArea.this.copy(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public boolean canPaste() { return PHighlightedTextArea.this.canPaste(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public void paste() { PHighlightedTextArea.this.paste(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public boolean canSelectAll() { return PHighlightedTextArea.this.canSelectAll(); } /** * Implement ClipboardTarget to pass clipboard requests to the JHighlghtedTextArea. */ public void selectAll() { PHighlightedTextArea.this.selectAll(); } /** * Paints the TextArea using the protected data of the document. * * @param chars The characters in the document. * @param charStyles The style index of each character in the document. * @param charCount The number of characters in the document. * @param lineOffsets The offsets of the first character of each line. * @param lineCount The number of lines in the document. * @param extraInfo The graphics object on which to paint. * @see HighlightedDocument#allowRender(HighlightedDocumentRenderer, Object) */ public void doRender(char chars[], byte charStyles[], int charCount, int lineOffsets[], int lineCount, Object extraInfo) { Graphics g = (Graphics)extraInfo; Rectangle clip = g.getClipBounds(); Point p; // Localize the demi-constants boolean monospaced = PHighlightedTextArea.this.monospaced; int maxCharacterWidth = PHighlightedTextArea.this.maxCharacterWidth; int characterHeight = PHighlightedTextArea.this.characterHeight; int lineSpacing = PHighlightedTextArea.this.lineSpacing; // Get the important points/offsets/lines int caretPositionOffset = selection.getCaretPositionOffset(); Point caretPositionPoint = document.offsetToCoordinate(caretPositionOffset); int selectionStartOffset = selection.getSelectionStartOffset(); Point selectionStartPoint = document.offsetToCoordinate(selectionStartOffset); int selectionEndOffset = selection.getSelectionEndOffset(); Point selectionEndPoint = document.offsetToCoordinate(selectionEndOffset); int actualHighlightedLine = highlightedLine; if (actualHighlightedLine == FOLLOW_CARET) { actualHighlightedLine = caretPositionPoint.y; } else if (actualHighlightedLine < 0 || actualHighlightedLine >= lineCount) { actualHighlightedLine = NO_HIGHLIGHT; } // Paint background { Color background = PHighlightedTextArea.this.getBackground(); if (background != null) g.setColor(background); else g.setColor(DEFAULT_BACKGROUND); } g.fillRect(clip.x, clip.y, clip.width, clip.height); // Paint the column markers if (columnMarkers.length > 0 && markerColor != null) { g.setColor(markerColor); for (int ctr = 0; ctr < columnMarkers.length; ctr++) { p = modelToView(columnMarkers[ctr], 0); g.drawLine( p.x, clip.y, p.x, clip.y + clip.height ); } } // Paint selection background if (selectionStartOffset != selectionEndOffset && selectionColor != null) { Point selectionStartPixel = modelToView(selectionStartPoint); Point selectionEndPixel = modelToView(selectionEndPoint); g.setColor(selectionColor); if (selectionStartPoint.y == selectionEndPoint.y) { g.fillRect( selectionStartPixel.x, selectionStartPixel.y - characterHeight, selectionEndPixel.x - selectionStartPixel.x, characterHeight + lineSpacing ); } else { g.fillRect( selectionStartPixel.x, selectionStartPixel.y - characterHeight, clip.x + clip.width - selectionStartPixel.x, characterHeight + lineSpacing ); g.fillRect( Math.max(margin.left, clip.x), selectionStartPixel.y + lineSpacing, clip.width, (selectionEndPoint.y - selectionStartPoint.y - 1) * (characterHeight + lineSpacing) ); g.fillRect( margin.left, selectionEndPixel.y - characterHeight, selectionEndPixel.x - margin.left, characterHeight + lineSpacing ); } } // Paint highlighted line if (actualHighlightedLine != NO_HIGHLIGHT && highlightColor != null) { int y = modelToView(0, actualHighlightedLine).y; g.setColor(highlightColor); g.drawLine( clip.x, y - characterHeight, clip.x + clip.width, y - characterHeight ); g.drawLine( clip.x, y + lineSpacing, clip.x + clip.width, y + lineSpacing ); } // Paint the text, line by line int lineStart = Math.max(0, viewToModel(clip.x, clip.y).y); int lineEnd = Math.min(lineCount, viewToModel(clip.x, clip.y + clip.height).y + 1); for (int lineCtr = lineStart; lineCtr < lineEnd; lineCtr++) { int charStart = lineOffsets[lineCtr]; int charEnd; if (lineCtr + 1 < lineCount) charEnd = lineOffsets[lineCtr + 1] - 1; else charEnd = charCount; p = modelToView(0, lineCtr); int charCtr = charStart; while (charCtr < charEnd) { // Only bother printing out printable ASCII characters if (chars[charCtr] >= 33 && chars[charCtr] <= 126) { byte styleIndex = charStyles[charCtr]; // Try to grab a run of text which has the same style throughout and is // composed of only printable ASCII characters and spaces int runEnd = charCtr + 1; int maxRunEnd; if (charCtr < selectionStartOffset) { maxRunEnd = Math.min(charEnd, selectionStartOffset); } else if (charCtr < selectionEndOffset) { maxRunEnd = Math.min(charEnd, selectionEndOffset); } else { maxRunEnd = charEnd; } while (runEnd < maxRunEnd && charStyles[runEnd] == styleIndex && chars[runEnd] >= 32 && chars[runEnd] <= 126) { runEnd++; } // Set the color if (charCtr >= selectionStartOffset && charCtr < selectionEndOffset && selectedTextColor != null) { g.setColor(selectedTextColor); } else if (lineCtr == actualHighlightedLine && highlightedTextColor != null) { g.setColor(highlightedTextColor); } else if (!PHighlightedTextArea.this.isEnabled() && disabledTextColor != null) { g.setColor(disabledTextColor); } else { g.setColor(highlightStyles[styleIndex].getColor()); } // Set the font Font font = highlightFonts[styleIndex]; g.setFont(font); // Draw the character if (monospaced) { g.drawChars(chars, charCtr, runEnd - charCtr, p.x, p.y); } else { int characterWidths[]; if (font.isBold()) { if (font.isItalic()) characterWidths = boldItalicCharacterWidths; else characterWidths = boldCharacterWidths; } else { if (font.isItalic()) characterWidths = italicCharacterWidths; else characterWidths = plainCharacterWidths; } for (int ctr = charCtr; ctr < runEnd; ctr++) { g.drawChars( chars, ctr, 1, p.x + (ctr - charCtr) * maxCharacterWidth + (maxCharacterWidth - characterWidths[chars[ctr]]) / 2, p.y ); } } // Increment p.x += maxCharacterWidth * (runEnd - charCtr); charCtr = runEnd; } else { // If the character isn't printable, just increment p.x += maxCharacterWidth; charCtr++; } } } // Paint the caret if (PHighlightedTextArea.this.isEnabled() && editable && (caretVisible && hasFocus() || popupMenu != null && popupMenu.isVisible())) { p = modelToView(caretPositionPoint); if (caretColor != null) g.setColor(caretColor); else { Color foreground = PHighlightedTextArea.this.getForeground(); if (foreground != null) g.setColor(foreground); else g.setColor(DEFAULT_FOREGROUND); } if (insertMode) { g.drawLine(p.x, p.y - characterHeight, p.x, p.y + lineSpacing - 1); } else { g.drawLine( p.x, p.y + lineSpacing - 2, p.x + maxCharacterWidth - 1, p.y + lineSpacing - 2 ); g.drawLine( p.x, p.y + lineSpacing - 1, p.x + maxCharacterWidth - 1, p.y + lineSpacing - 1 ); } } } /** * Implement runnable to blink the caret. */ public void run() { Thread thisThread = Thread.currentThread(); while (runner == thisThread) { if (PHighlightedTextArea.this.isEnabled() && editable && hasFocus()) { if (keepCaretVisible) { caretVisible = true; keepCaretVisible = false; } else { caretVisible = !caretVisible; } Point p = modelToView( document.offsetToCoordinate(selection.getCaretPositionOffset()) ); repaint( p.x - 2, p.y - characterHeight - 2, maxCharacterWidth + 4, characterHeight + lineSpacing + 4 ); } else { if (caretVisible) { caretVisible = false; Point p = modelToView( document.offsetToCoordinate(selection.getCaretPositionOffset()) ); repaint( p.x - 2, p.y - characterHeight - 2, maxCharacterWidth + 4, characterHeight + lineSpacing + 4 ); } } try { Thread.sleep(500); } catch (Exception e) { //whatever } } Point p = modelToView( document.offsetToCoordinate(selection.getCaretPositionOffset()) ); repaint( p.x - 2, p.y - characterHeight - 2, maxCharacterWidth + 4, characterHeight + lineSpacing + 4 ); } /** * Implement scrollable to make the viewport's preferred size that of the * InternalTextArea's. */ public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } /** * Implement scrollable to make the unit increment a tenth of a page. */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { switch(orientation) { case SwingConstants.VERTICAL: return visibleRect.height / 10; case SwingConstants.HORIZONTAL: return visibleRect.width / 10; default: throw new IllegalArgumentException("Invalid orientation: " + orientation); } } /** * Implement scrollable to make the block increment a full page. */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { switch(orientation) { case SwingConstants.VERTICAL: return visibleRect.height; case SwingConstants.HORIZONTAL: return visibleRect.width; default: throw new IllegalArgumentException("Invalid orientation: " + orientation); } } /** * Implement scrollable to make the InternalTextArea's size the greater of its own size * and its viewport's size. */ public boolean getScrollableTracksViewportWidth() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getWidth() > getPreferredSize().width); } return false; } /** * Implement scrollable to make the InternalTextArea's size the greater of its own size * and its viewport's size. */ public boolean getScrollableTracksViewportHeight() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getHeight() > getPreferredSize().height); } return false; } /** * Intercept FocusEvents to notify the ClipboardTargetManager of the event. */ protected void processFocusEvent(FocusEvent e) { if (e.getID() == FocusEvent.FOCUS_GAINED) { ClipboardTargetManager.targetGainedFocus(this); runner = new Thread(this); runner.start(); } else if (e.getID() == FocusEvent.FOCUS_LOST) { ClipboardTargetManager.targetLostFocus(this); runner = null; } super.processFocusEvent(e); } /** * Intercept KeyEvents to perform the usual action. */ protected void processKeyEvent(KeyEvent e) { if (!PHighlightedTextArea.this.isEnabled()) return; supressReaction = true; if (autoCompleter.interceptKeyEvent(PHighlightedTextArea.this, e)) { recalculateSizes(); scrollToCaret(); e.consume(); return; } if (e.getID() == KeyEvent.KEY_PRESSED) { Point p; int pageSize; int offset; int caretPositionOffset = selection.getCaretPositionOffset(); int selectionStartOffset = selection.getSelectionStartOffset(); int selectionEndOffset = selection.getSelectionEndOffset(); boolean handled = true; switch (e.getKeyCode()) { case KeyEvent.VK_DELETE: if (selectionStartOffset != selectionEndOffset) { // Clear the selected text document.remove( selectionStartOffset, selectionEndOffset - selectionStartOffset, false ); selectionStartOffset = selection.getSelectionStartOffset(); select( selectionStartOffset, selectionStartOffset, selectionStartOffset ); } else { // Remove the character following the caret document.remove(caretPositionOffset, 1, true); } recalculateSizes(); break; case KeyEvent.VK_INSERT: setInsertMode(!insertMode); break; case KeyEvent.VK_BACK_SPACE: if (selectionStartOffset != selectionEndOffset) { // Clear the selected text document.remove( selectionStartOffset, selectionEndOffset - selectionStartOffset, false ); selectionStartOffset = selection.getSelectionStartOffset(); select( selectionStartOffset, selectionStartOffset, selectionStartOffset ); } else { // Remove one character before the caret if (caretPositionOffset > 0) { document.remove(caretPositionOffset - 1, 1, true); } } recalculateSizes(); break; case KeyEvent.VK_LEFT: offset = caretPositionOffset - 1; if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_RIGHT: offset = caretPositionOffset + 1; if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_UP: p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(p.x, p.y - 1); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_DOWN: p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(p.x, p.y + 1); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_PAGE_UP: pageSize = getViewport().getVisibleRect().height / (characterHeight + lineSpacing); p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(p.x, p.y - pageSize); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_PAGE_DOWN: pageSize = getViewport().getVisibleRect().height / (characterHeight + lineSpacing); p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(p.x, p.y + pageSize); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_HOME: p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(0, p.y); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; case KeyEvent.VK_END: p = document.offsetToCoordinate(caretPositionOffset); offset = document.coordinateToOffset(Integer.MAX_VALUE, p.y); if (!e.isShiftDown() || (caretPositionOffset != selectionStartOffset && caretPositionOffset != selectionEndOffset)) { select(offset, offset, offset); } else { select(offset, selection.NO_CHANGE, offset); } break; default: handled = false; } if (handled) { e.consume(); caretVisible = true; keepCaretVisible = true; scrollToCaret(); } } if (e.getID() == KeyEvent.KEY_TYPED) { char ch = e.getKeyChar(); // Handle keytyping events for keys which have printable ASCII characters, but // not backspace, escape, or any keys with modifiers other than shift if (((ch >= 32 && ch <= 126) || ch == '\r' || ch == '\n' || ch == '\t') && !e.isAltDown() && !e.isControlDown() && !e.isMetaDown() && !e.isAltGraphDown()) { if (editable) { int caretPositionOffset = selection.getCaretPositionOffset(); int selectionStartOffset = selection.getSelectionStartOffset(); int selectionEndOffset = selection.getSelectionEndOffset(); String string = CHARACTER_STRINGS[(int)ch]; // Replace the selected text if (selectionStartOffset != selectionEndOffset) { document.replace( selectionStartOffset, selectionEndOffset - selectionStartOffset, string, false ); } else { if (insertMode) { document.insertString( selectionStartOffset, string, true ); } else { document.replace( selectionStartOffset, string.length(), string, true ); } } selectionStartOffset = selection.getSelectionStartOffset(); select( selectionStartOffset, selectionStartOffset, selectionStartOffset ); caretVisible = true; keepCaretVisible = true; recalculateSizes(); scrollToCaret(); } e.consume(); } } // Prevent any focus changes if (e.getKeyCode() == KeyEvent.VK_TAB || e.getKeyChar() == '\t') { e.consume(); } supressReaction = false; super.processKeyEvent(e); } /** * Intercept mouse clicks to perform the usual action. */ protected void processMouseEvent(MouseEvent e) { if (!PHighlightedTextArea.this.isEnabled()) return; boolean passedToSuper = false; if (popupMenu != null && e.isPopupTrigger()) { if (!hasFocus()) requestFocus(); // If nothing is selected, move the caret too if (selection.getSelectionDotOffset() == selection.getSelectionMarkOffset()) { int offset = document.coordinateToOffset(viewToModel(e.getX(), e.getY())); select(offset, offset, offset); } int x = e.getX(); int y = e.getY(); Point componentLocation = getLocationOnScreen(); Dimension screenSize = TOOLKIT.getScreenSize(); Dimension preferredSize = popupMenu.getPreferredSize(); if (componentLocation.x + x + preferredSize.width > screenSize.width) x -= preferredSize.width; if (componentLocation.y + y + preferredSize.height > screenSize.height) y -= preferredSize.height; e.consume(); passedToSuper = true; super.processMouseEvent(e); popupMenu.show(this, x, y); repaint(); } else if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { if (e.getID() == MouseEvent.MOUSE_PRESSED) { if (!hasFocus()) requestFocus(); int offset = document.coordinateToOffset(viewToModel(e.getX(), e.getY())); if (e.isShiftDown()) { select(offset, selection.NO_CHANGE, offset); } else { select(offset, offset, offset); } caretVisible = true; keepCaretVisible = true; scrollToCaret(); e.consume(); } if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() > 1) { if (e.getClickCount() % 2 == 0) { // An even-numbered click requires that we highlight the word int wordBounds[] = document.getWordBounds( document.coordinateToOffset(viewToModel(e.getX(), e.getY())) ); select( wordBounds[0], wordBounds[1], wordBounds[1] ); } else { // An odd-numbered click requires that we highlight the line int line = viewToModel(e.getX(), e.getY()).y; int offset = document.coordinateToOffset(0, line); select( offset, document.coordinateToOffset(Integer.MAX_VALUE, line), offset ); } caretVisible = true; keepCaretVisible = true; scrollToCaret(); e.consume(); } } if (!passedToSuper) super.processMouseEvent(e); } /** * Intercept mouse dragging to perform the usual action. */ protected void processMouseMotionEvent(MouseEvent e) { if (!PHighlightedTextArea.this.isEnabled()) return; if (e.getID() == MouseEvent.MOUSE_DRAGGED && (e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { int offset = document.coordinateToOffset(viewToModel(e.getX(), e.getY())); select(offset, selection.NO_CHANGE, offset); caretVisible = true; keepCaretVisible = true; scrollToCaret(); e.consume(); } super.processMouseMotionEvent(e); } } }