groovy.ui.text.TextEditor Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2003-2012 the original author or authors.
*
* 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 groovy.ui.text;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.print.Pageable;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.util.Calendar;
import java.util.regex.Pattern;
import javax.swing.Action;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.plaf.ComponentUI;
import javax.swing.text.Caret;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Utilities;
/**
* A simple text pane that is printable and wrapping is optional.
*
* @author Evan "Hippy" Slatis
*/
public class TextEditor extends JTextPane implements Pageable, Printable {
public static final String FIND = "Find...";
public static final String FIND_NEXT = "Find Next";
public static final String FIND_PREVIOUS = "Find Previous";
public static final String REPLACE = "Replace...";
public static final String AUTO_INDENT = "AutoIndent";
private static final String TABBED_SPACES = " ";
private static final Pattern TAB_BACK_PATTERN =
Pattern.compile("^(([\t])|( )|( )|( )|( ))", Pattern.MULTILINE);
private static final Pattern LINE_START =
Pattern.compile("^", Pattern.MULTILINE);
private static final JTextPane PRINT_PANE = new JTextPane();
private static final Dimension PRINT_SIZE = new Dimension();
private static boolean isOvertypeMode;
private Caret defaultCaret;
private Caret overtypeCaret;
private static final PageFormat PAGE_FORMAT;
static {
PrinterJob job = PrinterJob.getPrinterJob();
PAGE_FORMAT = job.defaultPage();
}
private int numPages;
private MouseAdapter mouseAdapter =
new MouseAdapter() {
Cursor cursor;
public void mouseEntered(MouseEvent me) {
if (contains(me.getPoint())) {
cursor = getCursor();
Cursor curs = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
getRootPane().getLayeredPane().setCursor(curs);
} else {
getRootPane().getLayeredPane().setCursor(cursor);
}
}
public void mouseExited(MouseEvent me) {
getRootPane().getLayeredPane().setCursor(null);
}
};
private boolean unwrapped;
private boolean tabsAsSpaces;
private boolean multiLineTab;
/**
* Creates a new instance of TextEditor
*/
public TextEditor() {
this(false);
}
/**
* Creates a new instance of TextEditor
*/
public TextEditor(boolean tabsAsSpaces) {
this(tabsAsSpaces, false);
}
/**
* Creates a new instance of TextEditor
*/
public TextEditor(boolean tabsAsSpaces, boolean multiLineTab) {
this(multiLineTab, tabsAsSpaces, false);
}
/**
* Creates a new instance of TextEditor
*/
public TextEditor(boolean tabsAsSpaces, boolean multiLineTab, boolean unwrapped) {
this.tabsAsSpaces = tabsAsSpaces;
this.multiLineTab = multiLineTab;
this.unwrapped = unwrapped;
// remove and replace the delete action to another spot so ctrl H later
// on is strictly for showing the find & replace dialog
ActionMap aMap = getActionMap();
Action action = null;
do {
action = action == null ? aMap.get(DefaultEditorKit.deletePrevCharAction) : null;
aMap.remove(DefaultEditorKit.deletePrevCharAction);
aMap = aMap.getParent();
} while (aMap != null);
aMap = getActionMap();
InputMap iMap = getInputMap();
KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0, false);
iMap.put(keyStroke, "delete");
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, KeyEvent.SHIFT_MASK, false);
iMap.put(keyStroke, "delete");
aMap.put("delete", action);
// set all the actions
action = new FindAction();
aMap.put(FIND, action);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F, KeyEvent.CTRL_MASK, false);
iMap.put(keyStroke, FIND);
aMap.put(FIND_NEXT, FindReplaceUtility.FIND_ACTION);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0, false);
iMap.put(keyStroke, FIND_NEXT);
aMap.put(FIND_PREVIOUS, FindReplaceUtility.FIND_ACTION);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F3, KeyEvent.SHIFT_MASK, false);
iMap.put(keyStroke, FIND_PREVIOUS);
action = new TabAction();
aMap.put("TextEditor-tabAction", action);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false);
iMap.put(keyStroke, "TextEditor-tabAction");
action = new ShiftTabAction();
aMap.put("TextEditor-shiftTabAction", action);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_MASK, false);
iMap.put(keyStroke, "TextEditor-shiftTabAction");
action = new ReplaceAction();
getActionMap().put(REPLACE, action);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_H, KeyEvent.CTRL_MASK, false);
do {
iMap.remove(keyStroke);
iMap = iMap.getParent();
} while (iMap != null);
getInputMap().put(keyStroke, REPLACE);
action = new AutoIndentAction();
getActionMap().put(AUTO_INDENT, action);
keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false);
getInputMap().put(keyStroke, AUTO_INDENT);
setAutoscrolls(true);
defaultCaret = getCaret();
overtypeCaret = new OvertypeCaret();
overtypeCaret.setBlinkRate(defaultCaret.getBlinkRate());
}
public void addNotify() {
super.addNotify();
addMouseListener(mouseAdapter);
FindReplaceUtility.registerTextComponent(this);
}
public int getNumberOfPages() {
Paper paper = PAGE_FORMAT.getPaper();
numPages = (int) Math.ceil(getSize().getHeight() / paper.getImageableHeight());
return numPages;
}
public PageFormat getPageFormat(int pageIndex) throws IndexOutOfBoundsException {
return PAGE_FORMAT;
}
public Printable getPrintable(int param) throws IndexOutOfBoundsException {
return this;
}
public int print(Graphics graphics, PageFormat pageFormat, int page)
throws PrinterException {
if (page < numPages) {
Paper paper = pageFormat.getPaper();
// initialize the PRINT_PANE (need this so that wrapping
// can take place)
PRINT_PANE.setDocument(getDocument());
PRINT_PANE.setFont(getFont());
PRINT_SIZE.setSize(paper.getImageableWidth(),
getSize().getHeight());
PRINT_PANE.setSize(PRINT_SIZE);
// translate the graphics origin upwards so the area of the page we
// want to print is in the origin; the clipping region auto set
// will take care of the rest
double y = -(page * paper.getImageableHeight()) + paper.getImageableY();
((Graphics2D) graphics).translate(paper.getImageableX(), y);
// print the text with its own routines
PRINT_PANE.print(graphics);
// translate the graphics object back to reality in the y dimension
// so we can print a page number
((Graphics2D) graphics).translate(0, -y);
Rectangle rect = graphics.getClipBounds();
graphics.setClip(rect.x, 0, rect.width, (int) paper.getHeight() + 100);
// get the name of the pane (or user name) and the time for the header
Calendar cal = Calendar.getInstance();
String header = cal.getTime().toString().trim();
String name = getName() == null ?
System.getProperty("user.name").trim() : getName().trim();
String pageStr = String.valueOf(page + 1);
Font font = Font.decode("Monospaced 8");
graphics.setFont(font);
FontMetrics fm = graphics.getFontMetrics(font);
int width = SwingUtilities.computeStringWidth(fm, header);
((Graphics2D) graphics).drawString(header,
(float) (paper.getImageableWidth() / 2 - width / 2),
(float) paper.getImageableY() / 2 + fm.getHeight());
((Graphics2D) graphics).translate(0, paper.getImageableY() - fm.getHeight());
double height = paper.getImageableHeight() + paper.getImageableY() / 2;
width = SwingUtilities.computeStringWidth(fm, name);
((Graphics2D) graphics).drawString(name,
(float) (paper.getImageableWidth() / 2 - width / 2),
(float) height - fm.getHeight() / 2);
((Graphics2D) graphics).translate(0, fm.getHeight());
width = SwingUtilities.computeStringWidth(fm, pageStr);
((Graphics2D) graphics).drawString(pageStr,
(float) (paper.getImageableWidth() / 2 - width / 2),
(float) height - fm.getHeight() / 2);
return Printable.PAGE_EXISTS;
}
return Printable.NO_SUCH_PAGE;
}
public boolean getScrollableTracksViewportWidth() {
boolean bool = super.getScrollableTracksViewportWidth();
if (unwrapped) {
Component parent = this.getParent();
ComponentUI ui = this.getUI();
int uiWidth = ui.getPreferredSize(this).width;
bool = (parent == null) || (uiWidth < parent.getSize().width);
}
return bool;
}
/**
* Whether using the tab key indents the selected lines of code
*
* @return true if multiline tabbing is active
*/
public boolean isMultiLineTabbed() {
return multiLineTab;
}
/**
* @return true if overtype mode is active, false for insert mode
*/
public static boolean isOvertypeMode() {
return isOvertypeMode;
}
/**
* @return true if tabs are converted to spaces upon typing
*/
public boolean isTabsAsSpaces() {
return tabsAsSpaces;
}
/**
* @return true if text wrapping is disabled
*/
public boolean isUnwrapped() {
return unwrapped;
}
protected void processKeyEvent(KeyEvent e) {
super.processKeyEvent(e);
// Handle release of Insert key to toggle overtype/insert mode
// unless a modifier is active (eg Shift+Insert for paste or
// Ctrl+Insert for Copy)
if (e.getID() == KeyEvent.KEY_RELEASED &&
e.getKeyCode() == KeyEvent.VK_INSERT &&
e.getModifiersEx() == 0) {
setOvertypeMode(!isOvertypeMode());
}
}
public void removeNotify() {
super.removeNotify();
removeMouseListener(mouseAdapter);
FindReplaceUtility.unregisterTextComponent(this);
}
public void replaceSelection(String text) {
// Implement overtype mode by selecting the character at the current
// caret position
if (isOvertypeMode()) {
int pos = getCaretPosition();
if (getSelectedText() == null && pos < getDocument().getLength()) {
moveCaretPosition(pos + 1);
}
}
super.replaceSelection(text);
}
public void setBounds(int x, int y, int width, int height) {
if (unwrapped) {
Dimension size = this.getPreferredSize();
super.setBounds(x, y,
Math.max(size.width, width),
Math.max(size.height, height));
} else {
super.setBounds(x, y, width, height);
}
}
/**
* @param multiLineTab the new multiLine tab value
*/
public void isMultiLineTabbed(boolean multiLineTab) {
this.multiLineTab = multiLineTab;
}
/**
* @param tabsAsSpaces whether tabs are converted to spaces
*/
public void isTabsAsSpaces(boolean tabsAsSpaces) {
this.tabsAsSpaces = tabsAsSpaces;
}
/**
* Set the caret to use depending on overtype/insert mode
*
* @param isOvertypeMode the new mode; true = overtype
*/
public void setOvertypeMode(boolean isOvertypeMode) {
TextEditor.isOvertypeMode = isOvertypeMode;
int pos = getCaretPosition();
setCaret(isOvertypeMode() ? overtypeCaret : defaultCaret);
setCaretPosition(pos);
}
/**
* @param unwrapped the new unwrapped value
*/
public void setUnwrapped(boolean unwrapped) {
this.unwrapped = unwrapped;
}
private class FindAction extends AbstractAction {
public void actionPerformed(ActionEvent ae) {
FindReplaceUtility.showDialog();
}
}
private class ReplaceAction extends AbstractAction {
public void actionPerformed(ActionEvent ae) {
FindReplaceUtility.showDialog(true);
}
}
private class ShiftTabAction extends AbstractAction {
public void actionPerformed(ActionEvent ae) {
try {
if (multiLineTab && TextEditor.this.getSelectedText() != null) {
int end = Utilities.getRowEnd(TextEditor.this, getSelectionEnd());
TextEditor.this.setSelectionEnd(end);
Element el = Utilities.getParagraphElement(TextEditor.this, getSelectionStart());
int start = el.getStartOffset();
TextEditor.this.setSelectionStart(start);
// remove text and reselect the text
String text = tabsAsSpaces ?
TAB_BACK_PATTERN.matcher(getSelectedText()).replaceAll("") :
getSelectedText().replaceAll("^\t", "");
TextEditor.this.replaceSelection(text);
TextEditor.this.select(start, start + text.length());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private class TabAction extends AbstractAction {
public void actionPerformed(ActionEvent ae) {
try {
Document doc = TextEditor.this.getDocument();
String text = tabsAsSpaces ? TABBED_SPACES : "\t";
if (multiLineTab && getSelectedText() != null) {
int end = Utilities.getRowEnd(TextEditor.this, getSelectionEnd());
TextEditor.this.setSelectionEnd(end);
Element el = Utilities.getParagraphElement(TextEditor.this, getSelectionStart());
int start = el.getStartOffset();
TextEditor.this.setSelectionStart(start);
String toReplace = TextEditor.this.getSelectedText();
toReplace = LINE_START.matcher(toReplace).replaceAll(text);
TextEditor.this.replaceSelection(toReplace);
TextEditor.this.select(start, start + toReplace.length());
} else {
int pos = TextEditor.this.getCaretPosition();
doc.insertString(pos, text, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Paint a horizontal line the width of a column and 1 pixel high
*/
private class OvertypeCaret extends DefaultCaret {
//The overtype caret will simply be a horizontal line one pixel high
// (once we determine where to paint it)
public void paint(Graphics g) {
if (isVisible()) {
try {
JTextComponent component = getComponent();
Rectangle r = component.getUI().modelToView(component, getDot());
Color c = g.getColor();
g.setColor(component.getBackground());
g.setXORMode(component.getCaretColor());
r.setBounds(r.x, r.y,
g.getFontMetrics().charWidth('w'),
g.getFontMetrics().getHeight());
g.fillRect(r.x, r.y, r.width, r.height);
g.setPaintMode();
g.setColor(c);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
}
/*
* Damage must be overridden whenever the paint method is overridden
* (The damaged area is the area the caret is painted in. We must
* consider the area for the default caret and this caret)
*/
protected synchronized void damage(Rectangle r) {
if (r != null) {
JTextComponent component = getComponent();
x = r.x;
y = r.y;
Font font = component.getFont();
width = component.getFontMetrics(font).charWidth('w');
height = r.height;
repaint();
}
}
}
}