org.fife.ui.rtextarea.RTextAreaUI Maven / Gradle / Ivy
/*
* 04/25/2007
*
* RTextAreaUI.java - UI used by instances of RTextArea.
*
* This library is distributed under a modified BSD license. See the included
* LICENSE file for details.
*/
package org.fife.ui.rtextarea;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.border.Border;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.*;
/**
* The UI used by instances of RTextArea
. This UI takes into
* account all of the "extras" involved in an RTextArea
, including
* having a special caret (for insert and overwrite), background images,
* highlighting the current line, etc.
*
* @author Robert Futrell
* @version 0.5
*/
public class RTextAreaUI extends BasicTextAreaUI {
private static final String SHARED_ACTION_MAP_NAME = "RTextAreaUI.actionMap";
private static final String SHARED_INPUT_MAP_NAME = "RTextAreaUI.inputMap";
protected RTextArea textArea; // The text area for which we are the UI.
private static final EditorKit DEFAULT_KIT = new RTextAreaEditorKit();
private static final TransferHandler DEFAULT_TRANSFER_HANDLER =
new RTATextTransferHandler();
private static final String RTEXTAREA_KEYMAP_NAME = "RTextAreaKeymap";
/**
* Creates a UI for an RTextArea.
*
* @param textArea A text area.
* @return The UI.
*/
public static ComponentUI createUI(JComponent textArea) {
return new RTextAreaUI(textArea);
}
/**
* Constructor.
*
* @param textArea An instance of RTextArea
.
* @throws IllegalArgumentException If textArea
is not an
* instance of RTextArea
.
*/
public RTextAreaUI(JComponent textArea) {
if (!(textArea instanceof RTextArea)) {
throw new IllegalArgumentException("RTextAreaUI is for " +
"instances of RTextArea only!");
}
this.textArea = (RTextArea)textArea;
}
/**
* The Nimbus LAF (and any Synth laf might have similar issues) doesn't set
* many UIManager properties that BasicLAF UI's look for. This causes
* problems for custom Basic-based UI's such as RTextAreaUI. This method
* attempts to detect if Nimbus has been installed, and if so, sets proper
* values for some editor properties.
*
* @param editor The text area.
*/
private void correctNimbusDefaultProblems(JTextComponent editor) {
// Don't check UIManager.getLookAndFeel().getName() for "Nimbus",
// as other Synth-based LaFs might have not set these properties,
// in which case we'll need to use our fallback values.
// Check for null, but not for UIResource, for these properties,
// because if Nimbus was installed these values would all be given
// null values. Another laf might have successfully installed
// UIResource values, which we don't want to override.
Color c = editor.getCaretColor();
if (c==null) {
editor.setCaretColor(RTextArea.getDefaultCaretColor());
}
c = editor.getSelectionColor();
if (c==null) {
c = UIManager.getColor("nimbusSelectionBackground");
if (c==null) { // Not Nimbus, but still need a value - fallback
c = UIManager.getColor("textHighlight");
if (c==null) {
c = new ColorUIResource(Color.BLUE);
}
}
editor.setSelectionColor(c);
}
c = editor.getSelectedTextColor();
if (c==null) {
c = UIManager.getColor("nimbusSelectedText");
if (c==null) { // Not Nimbus, but still need a value - fallback
c = UIManager.getColor("textHighlightText");
if (c==null) {
c = new ColorUIResource(Color.WHITE);
}
}
editor.setSelectedTextColor(c);
}
c = editor.getDisabledTextColor();
if (c==null) {
c = UIManager.getColor("nimbusDisabledText");
if (c==null) { // Not Nimbus, but still need a value - fallback
c = UIManager.getColor("textInactiveText");
if (c==null) {
c = new ColorUIResource(Color.DARK_GRAY);
}
}
editor.setDisabledTextColor(c);
}
Border border = editor.getBorder();
if (border==null) {
editor.setBorder(new BasicBorders.MarginBorder());
}
Insets margin = editor.getMargin();
if (margin==null) {
editor.setMargin(new InsetsUIResource(2, 2, 2, 2));
}
}
/**
* Creates the view for an element. Returns a WrappedPlainView or
* PlainView.
*
* @param elem The element.
* @return The view.
*/
@Override
public View create(Element elem) {
if (textArea.getLineWrap()) {
return new WrappedPlainView(elem, textArea.getWrapStyleWord());
}
else {
return new PlainView(elem);
}
}
/**
* Returns the default caret for an RTextArea
. This caret is
* capable of displaying itself differently for insert/overwrite modes.
*
* @return The caret.
*/
@Override
protected Caret createCaret() {
Caret caret = new ConfigurableCaret();
caret.setBlinkRate(500);
return caret;
}
@Override
protected Highlighter createHighlighter() {
return new RTextAreaHighlighter();
}
/**
* Creates the keymap for this text area. This takes the super class's
* keymap, but sets the default keystroke to be RTextAreaEditorKit's
* DefaultKeyTypedAction. This must be done to override the default
* keymap's default key-typed action.
*
* @return The keymap.
*/
@Override
protected Keymap createKeymap() {
// Load the keymap we'll be using (it's saved by
// JTextComponent.addKeymap).
Keymap map = JTextComponent.getKeymap(RTEXTAREA_KEYMAP_NAME);
if (map==null) {
Keymap parent = JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP);
map = JTextComponent.addKeymap(RTEXTAREA_KEYMAP_NAME, parent);
map.setDefaultAction(new RTextAreaEditorKit.DefaultKeyTypedAction());
}
return map;
}
/**
* Creates a default action map. This action map contains actions for all
* basic text area work - cut, copy, paste, select, caret motion, etc.
*
* This isn't named createActionMap()
because there is a
* package-private member by that name in BasicTextAreaUI
,
* and some compilers will give warnings that we are not overriding that
* method since it is package-private.
*
* @return The action map.
*/
protected ActionMap createRTextAreaActionMap() {
// Get the actions of the text area (which in turn gets them from its
// DefaultEditorKit).
ActionMap map = new ActionMapUIResource();
Action[] actions = textArea.getActions();
int n = actions.length;
for (Action a : actions) {
map.put(a.getValue(Action.NAME), a);
}
// Not sure if we need these; not sure they are ever called
// (check their NAMEs).
map.put(TransferHandler.getCutAction().getValue(Action.NAME),
TransferHandler.getCutAction());
map.put(TransferHandler.getCopyAction().getValue(Action.NAME),
TransferHandler.getCopyAction());
map.put(TransferHandler.getPasteAction().getValue(Action.NAME),
TransferHandler.getPasteAction());
return map;
}
/**
* Returns the name to use to cache/fetch the shared action map. This
* should be overridden by subclasses if the subclass has its own custom
* editor kit to install, so its actions get picked up.
*
* @return The name of the cached action map.
*/
protected String getActionMapName() {
return SHARED_ACTION_MAP_NAME;
}
/**
* Fetches the EditorKit for the UI.
*
* @param tc the text component for which this UI is installed
* @return the editor capabilities
* @see TextUI#getEditorKit
*/
@Override
public EditorKit getEditorKit(JTextComponent tc) {
return DEFAULT_KIT;
}
/**
* Returns the text area for which we are the UI.
*
* @return The text area.
*/
public RTextArea getRTextArea() {
return textArea;
}
/**
* Returns an action map to use by a text area.
*
* This method is not named getActionMap()
because there is
* a package-private method in BasicTextAreaUI
with that name.
* Thus, creating a new method with that name causes certain compilers to
* issue warnings that you are not actually overriding the original method
* (since it is package-private).
*
* @return The action map.
* @see #createRTextAreaActionMap()
*/
private ActionMap getRTextAreaActionMap() {
// Get the UIManager-cached action map; if this is the first
// RTextArea created, create the action map and cache it.
ActionMap map = (ActionMap)UIManager.get(getActionMapName());
if (map==null) {
map = createRTextAreaActionMap();
UIManager.put(getActionMapName(), map);
}
ActionMap componentMap = new ActionMapUIResource();
componentMap.put("requestFocus", new FocusAction());
if (map != null) {
componentMap.setParent(map);
}
return componentMap;
}
/**
* Get the InputMap to use for the UI.
*
* This method is not named getInputMap()
because there is
* a package-private method in BasicTextAreaUI
with that name.
* Thus, creating a new method with that name causes certain compilers to
* issue warnings that you are not actually overriding the original method
* (since it is package-private).
*/
protected InputMap getRTextAreaInputMap() {
InputMap map = new InputMapUIResource();
InputMap shared = (InputMap)UIManager.get(SHARED_INPUT_MAP_NAME);
if (shared==null) {
shared = new RTADefaultInputMap();
UIManager.put(SHARED_INPUT_MAP_NAME, shared);
}
//KeyStroke[] keys = shared.allKeys();
//for (int i=0; i " + shared.get(keys[i]));
map.setParent(shared);
return map;
}
/**
* Gets the allocation to give the root View. Due
* to an unfortunate set of historical events this
* method is inappropriately named. The Rectangle
* returned has nothing to do with visibility.
* The component must have a non-zero positive size for
* this translation to be computed.
*
* @return the bounding box for the root view
*/
@Override
protected Rectangle getVisibleEditorRect() {
Rectangle alloc = textArea.getBounds();
if ((alloc.width > 0) && (alloc.height > 0)) {
alloc.x = alloc.y = 0;
Insets insets = textArea.getInsets();
alloc.x += insets.left;
alloc.y += insets.top;
alloc.width -= insets.left + insets.right;
alloc.height -= insets.top + insets.bottom;
return alloc;
}
return null;
}
@Override
protected void installDefaults() {
super.installDefaults();
JTextComponent editor = getComponent();
editor.setFont(RTextAreaBase.getDefaultFont());
// Nimbus (and possibly other Synth lafs) doesn't play by BasicLaf
// rules and doesn't set properties needed by custom BasicTextAreaUI's.
correctNimbusDefaultProblems(editor);
editor.setTransferHandler(DEFAULT_TRANSFER_HANDLER);
}
/**
* {@inheritDoc}
*/
@Override
protected void installKeyboardActions() {
// NOTE: Don't call super.installKeyboardActions(), as that causes
// JTextAreas to stop responding to certain keystrokes if an RTextArea
// is the first-instantiated text area. This is because of the code
// path installKeyboardActions() -> getActionMap() -> createActionMap().
// In BasicTextUI#createActionMap(), "editor.getActions()" is called,
// and the current editor's returned Actions are used to create the
// ActionMap, which is then cached and used in all future J/RTextAreas.
// Unfortunately, RTextArea actions don't worn in JTextAreas.
//super.installKeyboardActions();
RTextArea textArea = getRTextArea();
// backward compatibility support... keymaps for the UI
// are now installed in the more friendly input map.
textArea.setKeymap(createKeymap());
// Since BasicTextUI.getInputMap() is package-private, instead use
// our own version here.
InputMap map = getRTextAreaInputMap();
SwingUtilities.replaceUIInputMap(textArea,JComponent.WHEN_FOCUSED,map);
// Same thing here with action map.
ActionMap am = getRTextAreaActionMap();
if (am!=null) {
SwingUtilities.replaceUIActionMap(textArea, am);
}
}
/**
* Installs this UI to the given text component.
*/
@Override
public void installUI(JComponent c) {
if (!(c instanceof RTextArea)) {
throw new Error("RTextAreaUI needs an instance of RTextArea!");
}
super.installUI(c);
}
@Override
protected void paintBackground(Graphics g) {
// Only fill in the background if an image isn't being used.
Color bg = textArea.getBackground();
if (bg!=null) {
g.setColor(bg);
//g.fillRect(0, 0, textArea.getWidth(), textArea.getHeight());
Rectangle r = g.getClipBounds();
g.fillRect(r.x,r.y, r.width,r.height);
}
paintEditorAugmentations(g);
}
/**
* Paints the highlighted current line, if it is enabled.
*
* @param g The graphics context with which to paint.
* @param visibleRect The visible rectangle of the text area.
*/
protected void paintCurrentLineHighlight(Graphics g, Rectangle visibleRect) {
if (textArea.getHighlightCurrentLine()) {
Caret caret = textArea.getCaret();
if (caret.getDot()==caret.getMark()) {
Color highlight = textArea.getCurrentLineHighlightColor();
// NOTE: We use the getLineHeight() method below instead
// of currentCaretRect.height because of a bug where
// currentCaretRect.height is incorrect when an RSyntaxTextArea
// is first displayed (it is initialized with the text area's
// font.getHeight() (via RTextArea), but isn't changed to
// account for the syntax styles before it is displayed).
//int height = textArea.currentCaretRect.height);
int height = textArea.getLineHeight();
if (textArea.getFadeCurrentLineHighlight()) {
Graphics2D g2d = (Graphics2D)g;
Color bg = textArea.getBackground();
GradientPaint paint = new GradientPaint(
visibleRect.x,0, highlight,
visibleRect.x+visibleRect.width,0,
bg==null ? Color.WHITE : bg);
g2d.setPaint(paint);
g2d.fillRect(visibleRect.x,textArea.currentCaretY,
visibleRect.width, height);
}
else {
g.setColor(highlight);
g.fillRect(visibleRect.x,textArea.currentCaretY,
visibleRect.width, height);
}
}
}
}
/**
* Paints editor augmentations added by RTextArea: highlighted lines,
* current line highlight, and margin line.
*
* @param g The graphics context with which to paint.
*/
protected void paintEditorAugmentations(Graphics g) {
Rectangle visibleRect = textArea.getVisibleRect();
paintLineHighlights(g);
paintCurrentLineHighlight(g, visibleRect);
paintMarginLine(g, visibleRect);
}
/**
* Paints any line highlights.
*
* @param g The graphics context.
*/
protected void paintLineHighlights(Graphics g) {
LineHighlightManager lhm = textArea.getLineHighlightManager();
if (lhm!=null) {
lhm.paintLineHighlights(g);
}
}
/**
* Draws the "margin line" if enabled.
*
* @param g The graphics context to paint with.
* @param visibleRect The visible rectangle of this text area.
*/
protected void paintMarginLine(Graphics g, Rectangle visibleRect) {
if (textArea.isMarginLineEnabled()) {
g.setColor(textArea.getMarginLineColor());
Insets insets = textArea.getInsets();
int marginLineX = textArea.getMarginLinePixelLocation() +
(insets==null ? 0 : insets.left);
g.drawLine(marginLineX,visibleRect.y,
marginLineX,visibleRect.y+visibleRect.height);
}
}
@Override
protected void paintSafely(Graphics g) {
// Paint editor augmentations if editor is not opaque because
// paintBackground() is not called in this case
if (!textArea.isOpaque()) {
paintEditorAugmentations(g);
}
super.paintSafely(g);
}
/**
* Returns the y-coordinate of the specified line.
*
* The default implementation is equivalent to:
*
* int startOffs = textArea.getLineStartOffset(line);
* return yForLineContaining(startOffs);
*
*
* Subclasses that can calculate this value more quickly than traditional
* {@link #modelToView(JTextComponent, int)} calls should override this
* method to do so. This method may be used when the entire bounding box
* isn't needed, to speed up rendering.
*
* @param line The line number.
* @return The y-coordinate of the top of the line, or -1
if
* this text area doesn't yet have a positive size or the line is
* hidden (i.e. from folding).
* @throws BadLocationException If line
isn't a valid line
* number for this document.
*/
public int yForLine(int line) throws BadLocationException {
int startOffs = textArea.getLineStartOffset(line);
return yForLineContaining(startOffs);
}
/**
* Returns the y-coordinate of the line containing an offset.
*
* The default implementation is equivalent to:
*
* int line = textArea.getLineOfOffset(offs);
* int startOffs = textArea.getLineStartOffset(line);
* return modelToView(startOffs).y;
*
*
* Subclasses that can calculate this value more quickly than traditional
* {@link #modelToView(JTextComponent, int)} calls should override this
* method to do so. This method may be used when the entire bounding box
* isn't needed, to speed up rendering.
*
* @param offs The offset info the document.
* @return The y-coordinate of the top of the offset, or -1
if
* this text area doesn't yet have a positive size or the line is
* hidden (i.e. from folding).
* @throws BadLocationException If offs
isn't a valid offset
* into the document.
*/
public int yForLineContaining(int offs) throws BadLocationException {
Rectangle r = modelToView(textArea, offs);
return r!=null ? r.y : -1;
}
/**
* Registered in the ActionMap.
*/
class FocusAction extends AbstractAction {
@Override
public void actionPerformed(ActionEvent e) {
textArea.requestFocus();
}
@Override
public boolean isEnabled() {
return textArea.isEditable();
}
}
}