/* * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License * at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jsyntaxpane.actions; import java.awt.Component; import java.awt.Frame; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.KeyEvent; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.JComboBox; import javax.swing.JEditorPane; import javax.swing.MutableComboBoxModel; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.EditorKit; import javax.swing.text.Element; import javax.swing.text.JTextComponent; import javax.swing.text.PlainDocument; import jsyntaxpane.DefaultSyntaxKit; import jsyntaxpane.SyntaxDocument; import jsyntaxpane.Token; /** * Various utility methods to work on JEditorPane and its SyntaxDocument * for use by Actions * * @author Ayman Al-Sairafi */ public class ActionUtils { private ActionUtils() { } private static ActionUtils instance = null; /** * Get the Singleton instance. Will be created lazily. * @return */ public static synchronized ActionUtils getInstance() { if(instance == null) { instance = new ActionUtils(); } return instance; } /** * Get the indentation of a line of text. This is the subString from * beginning of line to the first non-space char * @param line the line of text * @return indentation of line. */ public static String getIndent(String line) { if (line == null || line.length() == 0) { return ""; } int i = 0; while (i < line.length() && line.charAt(i) == ' ') { i++; } return line.substring(0, i); } /** * Return the lines that span the selection (split as an array of Strings) * if there is no selection then current line is returned. * * Note that the strings returned will not contain the terminating line feeds * If the document is empty, then an empty string array is returned. So * you can always iterate over the returned array without a null check * * The text component will then have the full lines set as selection * @param target * @return String[] of lines spanning selection / or line containing dot */ public static String[] getSelectedLines(JTextComponent target) { String[] lines = null; try { PlainDocument pDoc = (PlainDocument) target.getDocument(); int start = pDoc.getParagraphElement(target.getSelectionStart()).getStartOffset(); int end; if (target.getSelectionStart() == target.getSelectionEnd()) { end = pDoc.getParagraphElement(target.getSelectionEnd()).getEndOffset(); } else { // if more than one line is selected, we need to subtract one from the end // so that we do not select the line with the caret and no selection in it end = pDoc.getParagraphElement(target.getSelectionEnd() - 1).getEndOffset(); } target.select(start, end); lines = pDoc.getText(start, end - start).split("\n"); target.select(start, end); } catch (BadLocationException ex) { Logger.getLogger(ActionUtils.class.getName()).log(Level.SEVERE, null, ex); lines = EMPTY_STRING_ARRAY; } return lines; } /** * Return the line of text at the TextComponent's current position * @param target * @return */ public static String getLine(JTextComponent target) { return getLineAt(target, target.getCaretPosition()); } /** * Return the line of text at the given position. The returned value may * be null. It will not contain the trailing new-line character. * @param target the text component * @param pos char position * @return */ public static String getLineAt(JTextComponent target, int pos) { String line = null; Document doc = target.getDocument(); if (doc instanceof PlainDocument) { PlainDocument pDoc = (PlainDocument) doc; int start = pDoc.getParagraphElement(pos).getStartOffset(); int end = pDoc.getParagraphElement(pos).getEndOffset(); try { line = doc.getText(start, end - start); if (line != null && line.endsWith("\n")) { line = line.substring(0, line.length() - 1); } } catch (BadLocationException ex) { Logger.getLogger(ActionUtils.class.getName()).log(Level.SEVERE, null, ex); } } return line; } /** * Returns the Frame that contains this component or null if the component * is not within a Window or the containing window is not a frame * @param comp * @return */ public static Frame getFrameFor(Component comp) { Window w = SwingUtilities.getWindowAncestor(comp); if (w != null && w instanceof Frame) { Frame frame = (Frame) w; return frame; } return null; } /** * Returns the the Token at pos as a String * @param doc * @param pos * @return */ public static String getTokenStringAt( SyntaxDocument doc, int pos) { String word = ""; Token t = doc.getTokenAt(pos); if (t != null) { try { word = doc.getText(t.start, t.length); } catch (BadLocationException ex) { Logger.getLogger(ActionUtils.class.getName()).log(Level.SEVERE, null, ex); } } return word; } /** * A helper function that will return the SyntaxDocument attached to the * given text component. Return null if the document is not a * SyntaxDocument, or if the text component is null * @param component * @return */ public static SyntaxDocument getSyntaxDocument(JTextComponent component) { if (component == null) { return null; } Document doc = component.getDocument(); if (doc instanceof SyntaxDocument) { return (SyntaxDocument) doc; } else { return null; } } /** * Gets the Line Number at the give position of the editor component. * The first line number is ZERO * @param editor * @param pos * @return line number * @throws javax.swing.text.BadLocationException */ public static int getLineNumber(JTextComponent editor, int pos) throws BadLocationException { if (getSyntaxDocument(editor) != null) { SyntaxDocument sdoc = getSyntaxDocument(editor); return sdoc.getLineNumberAt(pos); } else { Document doc = editor.getDocument(); return doc.getDefaultRootElement().getElementIndex(pos); } } /** * Gets the column number at given position of editor. The first column is * ZERO * @param editor * @param pos * @return the 0 based column number * @throws javax.swing.text.BadLocationException */ public static int getColumnNumber(JTextComponent editor, int pos) throws BadLocationException { // speedup if the pos is 0 if(pos == 0) { return 0; } Rectangle r = editor.modelToView(pos); int start = editor.viewToModel(new Point(0, r.y)); int column = pos - start; return column; } /** * Get the closest position within the document of the component that * has given line and column. * @param editor * @param line the first being 1 * @param column the first being 1 * @return the closest positon for the text component at given line and * column */ public static int getDocumentPosition(JTextComponent editor, int line, int column) { int lineHeight = editor.getFontMetrics(editor.getFont()).getHeight(); int charWidth = editor.getFontMetrics(editor.getFont()).charWidth('m'); int y = line * lineHeight; int x = column * charWidth; Point pt = new Point(x, y); int pos = editor.viewToModel(pt); return pos; } public static int getLineCount(JTextComponent pane) { SyntaxDocument sdoc = getSyntaxDocument(pane); if (sdoc != null) { return sdoc.getLineCount(); } int count = 0; try { int p = pane.getDocument().getLength() - 1; if (p > 0) { count = getLineNumber(pane, p); } } catch (BadLocationException ex) { Logger.getLogger(ActionUtils.class.getName()).log(Level.SEVERE, null, ex); } return count; } /** * Insert the given item into the combo box, and set it as first selected * item. If the item already exists, it is removed, so there are no * duplicates. * @param combo * @param item the item to insert. if it's null, then nothing is inserted */ public static void insertIntoCombo(JComboBox combo, Object item) { if(item == null) { return; } MutableComboBoxModel model = (MutableComboBoxModel) combo.getModel(); if (model.getSize() == 0) { model.insertElementAt(item, 0); return; } Object o = model.getElementAt(0); if (o.equals(item)) { return; } model.removeElement(item); model.insertElementAt(item, 0); combo.setSelectedIndex(0); } public static void insertMagicString(JTextComponent target, String result) { try { insertMagicString(target, target.getCaretPosition(), result); } catch (BadLocationException ex) { Logger.getLogger(ActionUtils.class.getName()).log(Level.SEVERE, null, ex); } } /** * Repeat the string source repeat times. * If repeats == 0 then empty String is returned * if source is null, then empty string is returned * @param source * @param repeat * @return source String repeated repeat times. */ public static String repeatString(String source, int repeat) { if (repeat < 0) { throw new IllegalArgumentException("Cannot repeat " + repeat + " times."); } if (repeat == 0 || source == null || source.length() == 0) { return ""; } StringBuffer buffer = new StringBuffer(); for (int i = 0; i < repeat; i++) { buffer.append(source); } return buffer.toString(); } /** * Checks if the given string is null, empty or contains whitespace only * @param string * @return true if string is null, empty or contains whitespace only, false * otherwise. */ public static boolean isEmptyOrBlanks(String string) { if (string == null || string.length() == 0) { return true; } for (int i = 0; i < string.length(); i++) { char c = string.charAt(i); if (!Character.isWhitespace(c)) { return false; } } return true; } /** * Return the TabStop property for the given text component, or 0 if not * used * @param text * @return */ public static int getTabSize(JTextComponent text) { Integer tabs = (Integer) text.getDocument().getProperty(PlainDocument.tabSizeAttribute); return (null == tabs) ? 0 : tabs.intValue(); } /** * Insert the given String into the textcomponent. If the string contains * the | vertical BAr char, then it will not be inserted, and the cursor will * be set to its location. * If there are TWO vertical bars, then the text between them will be selected * If the toInsert String is multiLine, then indentation of all following lines * will be the same as the first line. TAB characters will be replaced by * the number of spaces in the document's TAB property. * @param target * @param dot * @param toInsert * @throws javax.swing.text.BadLocationException */ public static void insertMagicString(JTextComponent target, int dot, String toInsert) throws BadLocationException { Document doc = target.getDocument(); String[] lines = toInsert.split("\n"); if (lines.length > 1) { // multi line strings will need to be indented String tabToSpaces = getTab(target); String currentLine = getLineAt(target, dot); String currentIndent = getIndent(currentLine); StringBuilder sb = new StringBuilder(toInsert.length()); boolean firstLine = true; for (String l : lines) { if (!firstLine) { sb.append(currentIndent); } firstLine = false; // replace tabs with spaces. sb.append(l.replace("\t", tabToSpaces)); sb.append("\n"); } toInsert = sb.toString(); } if (toInsert.indexOf('|') >= 0) { int ofst = toInsert.indexOf('|'); int ofst2 = toInsert.indexOf('|', ofst + 1); toInsert = toInsert.replace("|", ""); doc.insertString(dot, toInsert, null); dot = target.getCaretPosition(); int strLength = toInsert.length(); if (ofst2 > 0) { // note that we already removed the first |, so end offset is now // one less than what it was. target.select(dot + ofst - strLength, dot + ofst2 - strLength - 1); } else { target.setCaretPosition(dot + ofst - strLength); } } else { doc.insertString(dot, toInsert, null); } } /** * Expand the string template and replaces the selection with the expansion * of the template. The template String may contain any of the following * special tags. * * <li>{@code #{selection}} replaced with the selection, if any. If there is * no selection, then the {@code #{selection}} tag will be removed. * <li>{@code #{p:any text}} will be replaced by {@code any text} and then * set selection to {@code any text} * * This method properly handles indentation as follows: * The indentation of the whole block will match the indentation of the caret * line, or the line with the beginning of the selection, if the selection is * in whole line, i.e.e one or more lines of selected text. {@see selectLines()} * * @param target JEditorCOmponent to be affected * @param templateLines template split as a String array of lines. * * @see insertLinesTemplate */ public static void insertLinesTemplate(JTextComponent target, String[] templateLines) { // get some stuff we'll need: String thisIndent = getIndent(getLineAt(target, target.getSelectionStart())); String[] selLines = getSelectedLines(target); int selStart = -1, selEnd = -1; StringBuffer sb = new StringBuffer(); for (String tLine : templateLines) { int selNdx = tLine.indexOf("#{selection}"); if (selNdx >= 0) { // for each of the selected lines: for (String selLine : selLines) { sb.append(tLine.subSequence(0, selNdx)); sb.append(selLine); sb.append('\n'); } } else { sb.append(thisIndent); // now check for any ptags Matcher pm = PTAGS_PATTERN.matcher(tLine); int lineStart = sb.length(); while (pm.find()) { selStart = pm.start() + lineStart; pm.appendReplacement(sb, pm.group(1)); selEnd = sb.length(); } pm.appendTail(sb); sb.append('\n'); } } int ofst = target.getSelectionStart(); target.replaceSelection(sb.toString()); if (selStart >= 0) { // target.setCaretPosition(selStart); target.select(ofst + selStart, ofst + selEnd); } } /** * Expand the string template and replaces the selection with the expansion * of the template. The template String may contain any of the following * special tags. * * <li>{@code #{selection}} replaced with the selection, if any. If there is * no selection, then the {@code #{selection}} tag will be removed. * <li>{@code #{p:AnyText}} will be replaced by {@code any text} and then * set the text selection to {@code AnyText} * * This methood does NOT perform any indentation and the template should * generally span one line only * * @param target * @param template */ public static void insertSimpleTemplate(JTextComponent target, String template) { String selected = target.getSelectedText(); selected = (selected == null) ? "" : selected; StringBuffer sb = new StringBuffer(template.length()); Matcher pm = PTAGS_PATTERN.matcher(template.replace(TEMPLATE_SELECTION, selected)); int selStart = -1, selEnd = -1; int lineStart = 0; while (pm.find()) { selStart = pm.start() + lineStart; pm.appendReplacement(sb, pm.group(1)); selEnd = sb.length(); } pm.appendTail(sb); // String expanded = template.replace(TEMPLATE_SELECTION, selected); if (selStart >= 0) { selStart += target.getSelectionStart(); selEnd += target.getSelectionStart(); } target.replaceSelection(sb.toString()); if (selStart >= 0) { // target.setCaretPosition(selStart); target.select(selStart, selEnd); } } /** * If the selection is multi lined, then the full lines are selected, * otherwise, nothing is done. * @param target * @return true if the selection is multi-line, or a whole line */ public static boolean selectLines(JTextComponent target) { if (target.getSelectionStart() == target.getSelectionEnd()) { return false; } PlainDocument pDoc = (PlainDocument) target.getDocument(); Element es = pDoc.getParagraphElement(target.getSelectionStart()); // if more than one line is selected, we need to subtract one from the end // so that we do not select the line with the caret and no selection in it Element ee = pDoc.getParagraphElement(target.getSelectionEnd() - 1); if (es.equals(ee) && ee.getEndOffset() != target.getSelectionEnd()) { return false; } int start = es.getStartOffset(); int end = ee.getEndOffset(); target.select(start, end - 1); return true; } /** * Sets the caret position of the given target to the given line and column * @param target * @param line the first being 1 * @param column the first being 1 */ public static void setCaretPosition(JTextComponent target, int line, int column) { int p = getDocumentPosition(target, line, column); target.setCaretPosition(p); } /** * Return a string with number of spaces equal to the tab-stop of the TextComponent * @param target * @return */ public static String getTab(JTextComponent target) { return SPACES.substring(0, getTabSize(target)); } /** * Searches all actions of a JTextComponent for ab action of the given class and returns * the first one that matches that class, or null if no Action is found * @param <T> * @param target * @param aClass * @return Action object of that class or null */ public static <T extends Action> T getAction(JTextComponent target, Class<T> aClass) { for (Object k : target.getActionMap().allKeys()) { Action a = target.getActionMap().get(k); if (aClass.isInstance(a)) { @SuppressWarnings("unchecked") T t = (T) a; return t; } } return null; } /** * Return the DefaultSyntaxKit of this target, or null if the target does not * have a DefaultSyntaxKit * @param target * @return kit or null */ public static DefaultSyntaxKit getSyntaxKit(JTextComponent target) { DefaultSyntaxKit kit = null; if (target instanceof JEditorPane) { JEditorPane jEditorPane = (JEditorPane) target; EditorKit k = jEditorPane.getEditorKit(); if (k instanceof DefaultSyntaxKit) { kit = (DefaultSyntaxKit) k; } } return kit; } /** * Create and send a KeyPress KeyEvent to the component given * @param target Editor to get the action * @param v_key from KeyEvent.V_ constants * @param modifiers from KeyEvent.*MASK constants */ public static void sendKeyPress(JTextComponent target, int v_key, int modifiers) { KeyEvent ke = new KeyEvent(target, KeyEvent.KEY_PRESSED, System.currentTimeMillis(), modifiers, v_key, KeyEvent.CHAR_UNDEFINED); target.dispatchEvent(ke); } // This is used internally to avoid NPE if we have no Strings final static String[] EMPTY_STRING_ARRAY = new String[0]; // This is used to quickly create Strings of at most 16 spaces (using substring) final static String SPACES = " "; /** * The Pattern to use for PTags in insertSimpleTemplate */ public static final Pattern PTAGS_PATTERN = Pattern.compile("\\#\\{p:([^}]*)\\}"); public static final String TEMPLATE_SELECTION = "#{selection}"; }