Why replaceSelection in JTextPane is not behaving safely?

L

lenyado

Hi all, I am now doing my Java homework as a new Java beginner. I've
found the following code from the internet, which I've modified a bit
to meet my need. The purpose is to create a console-like window to
display the UI (as Java's console has very very limited functions,
which cannot be operated as C or C++). But now I've got a mysterious
problem, which happens from time to time and I can never predict when
it will happen -- the text pane seems to "freeze" at replaceSelection
at method "write" (as I've tested to place some console output before
and after this line). The problem is that it seems to work perfectly
right but then it hangs. I have no idea what goes wrong, as when I
debug it in the Eclipse, all the thread are still in running status.
And strangely, I got this exception: "Exception in thread "AWT-
EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 0 >= 0"
occasionally, but the program keeps working.

I am pretty annoyed by this, as there seems no tools that I can used
to debug the program (such as dump the JVM status). As
replaceSelection is stated as "safe" explicitly in APIs, why it's not
behaving safely?

Thanks a million in advanced! (specially Rene Ghosh :p)

-- Class TextConsole.java --
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.StyleConstants;

/**
* The TextConsole component. An extension of the
* JTextPane class for user interaction in text
* form.
*/
class TextConsole extends JTextPane {

/**
*
*/
private static final long serialVersionUID = -5329149879890129297L;
/**
* Default parameters
for basic font name and size.
*/
private static final int DEFAULT_FONT_SIZE = 20;
private static final String DEFAULT_FONT_NAME = "Courier New";
private static final int DEFAULT_WIDTH_CHARS = 80;
private static final int DEFAULT_HEIGHT_CHARS = 25;
private static final Color DEFAULT_BACKGROUND_COLOR = Color.BLACK;
private static final Color DEFAULT_FOREGROUND_COLOR = Color.WHITE;

private Font font = null;
private int lastSubmitKey = -1;
private StringBuffer clearBuffer;
private String blankLine = null;
private int[] submitKeys = new int[0];
private Color backgroundColor, foregroundColor;

/**
* flag to set to true when the form is submitted
*/
private volatile boolean finished = false;
MutableAttributeSet attrs = getInputAttributes();
int widthChars = DEFAULT_WIDTH_CHARS;
int heightChars = DEFAULT_HEIGHT_CHARS;

/**
* Maximum number of characters that will hold
* in the console window == width * height
*/
int maxLength = pointToInt(widthChars, heightChars);

/**
* The list of keys that should be processed
* by the JTextPane superclass.
*/
int[] processable = new int[] {
KeyEvent.VK_UP,
KeyEvent.VK_DOWN,
KeyEvent.VK_LEFT,
KeyEvent.VK_RIGHT,
KeyEvent.VK_HOME,
KeyEvent.VK_END};

/**
* List of zones that support user input.
*/
private List formRanges = new ArrayList();

/**
* Basic constructor: uses default values for
* font size (20) and name (Courier New). Input parameters include
* width and height.
*/
public TextConsole(int width, int height) {
this(width, height, DEFAULT_FONT_SIZE, DEFAULT_FONT_NAME,
DEFAULT_BACKGROUND_COLOR, DEFAULT_FOREGROUND_COLOR);
}

/**
* Full constructor taking as input the width and height in number
* of characters, font size and font name.
*/
public TextConsole(int width, int height, int fontSize, String
fontName) {
this(width, height, fontSize, fontName, DEFAULT_BACKGROUND_COLOR,
DEFAULT_FOREGROUND_COLOR);
}

public TextConsole(int width, int height, int fontSize, String
fontName, Color backgroundColor, Color foregroundColor) {
this.font = new Font(fontName, Font.PLAIN, fontSize);
this.widthChars = width;
this.heightChars = height;
this.backgroundColor = backgroundColor;
this.foregroundColor = foregroundColor;
this.maxLength = pointToInt(widthChars, heightChars);
setFont(font);
FontRenderContext fontRenderContext = new FontRenderContext(null,
false, true);
Rectangle2D stringBounds = font.getStringBounds(new char[] { 'W' },
0, 1, fontRenderContext);
setPreferredSize(new Dimension(
(int) (5+ (widthChars + 1) * stringBounds.getWidth()),
(int) (5+ (heightChars + 1) * stringBounds.getHeight())));
setForeground(this.foregroundColor);
setBackground(this.backgroundColor);
setCaretColor(this.foregroundColor);
/**
* Construct a blank buffer to clear the screen.
*/
clearBuffer = new StringBuffer();
for (int j = 0; j < heightChars; j++) {
for (int i = 0; i < widthChars; i++) {
clearBuffer.append(" ");
}
if (j < heightChars - 1) {
clearBuffer.append("\n");
}
}
fill();
}

/**
* Rendering method. Overrides the paint() method
* on the superclass to stop antialiasing to the output
* screen.
*/
/* public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_SPEED);
super.paint(g);
}*/


/**
* Method to fill the screen with blank characters (space)
*/
private void fill() {
setText(clearBuffer.toString());
StyleConstants.setBackground(attrs, backgroundColor);
StyleConstants.setForeground(attrs, foregroundColor);
StyleConstants.setUnderline(attrs, false);
getStyledDocument().setCharacterAttributes(0, maxLength, attrs,
true);
}

/**
* Convert an X-Y position into a sequential index
* of a character in the text pane.
* @throws Exception
*/
private int pointToInt(int i, int j) {
if (i < 0 || i > widthChars || j < 0 || j > heightChars)
return 0;
int ret = ((j -1 ) * (widthChars + 1)) + (i - 1);
return ret;
}

/**
* set the cursor position on screen
*/
public void gotoPosition(int x, int y) {
int start = pointToInt(x, y);
setCaretPosition(start);
}

/**
* set the cursor position on the first
* form field on screen
*/
public void gotoFirstField() {
if (formRanges.size() > 0) {
FormRange firstRange = (FormRange)formRanges.get(0);
setCaretPosition(firstRange.start);
}
}

/**
* Clear the screen
*/
public void clear() {
formRanges.clear();
fill();
reset();
}

/**
* set the prescribed color to all characters in a given range
*/
public void color(int i, int j, Color color) {
StyleConstants.setForeground(attrs, color);
getStyledDocument().setCharacterAttributes(i, j - i, attrs, true);
}

public int getLastSubmitKey() {
int lsk = lastSubmitKey;
lastSubmitKey = -1;
return lsk;
}

public void setSubmitKeys(int[] keys) {
submitKeys = keys;
}

/**
* Key processing function. Certain key events
* are delegated to the superclass. Others submit
* the user input to the calling object or react to the input
* internally.
*/
protected void processKeyEvent(KeyEvent e) {
char keyChar = e.getKeyChar();
int keyCode = e.getKeyCode();
String text = "";
int start, pos;
int id = e.getID();
System.out.println("I am here!");
if (keyCode == KeyEvent.VK_ENTER) {
finished = true;
lastSubmitKey = KeyEvent.VK_ENTER;
return;
}
if (id == KeyEvent.KEY_PRESSED) {
for (int i = 0; i < submitKeys.length; i++) {
if (submitKeys == keyCode) {
finished = true;
lastSubmitKey = keyCode;
return;
}
}
}
boolean needProcess = false;
for (int i = 0; i < processable.length; i++) {
if (processable == keyCode) {
needProcess = true;
break;
}
}
if (!needProcess) {
int caretPosition = getCaretPosition();
for (Iterator iter = formRanges.iterator(); iter.hasNext();) {
FormRange range = (FormRange) iter.next();
int checkPosition = caretPosition;
if (keyCode == KeyEvent.VK_BACK_SPACE) {
checkPosition = caretPosition - 1;
}
if (range.isInRange(checkPosition)) {
try {
text = getText(range.start, range.end - range.start);
} catch (BadLocationException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
if (id == KeyEvent.KEY_PRESSED) {
//if (e.getID() == KeyEvent.KEY_RELEASED) {
setLocalColor(range.color);
if (keyChar == KeyEvent.VK_SPACE) {
write(" ");
} else if(keyCode == KeyEvent.VK_DELETE) {
pos = getCaretPosition();
start = pos - range.start;
if (start > 0)
text = text.substring(0, start) + text.substring(start + 1) +
" ";
else
text = text.substring(1) + " ";
setCaretPosition(range.start);
write(text);
setCaretPosition(pos);
} else if (keyCode == KeyEvent.VK_BACK_SPACE) {
pos = getCaretPosition();
start = pos - range.start;
if (start > 0)
text = text.substring(0, start - 1) + text.substring(start) +
" ";
else
text = text.substring(1) + " ";
setCaretPosition(range.start);
write(text);
setCaretPosition(pos - 1);
}
else if (Character.isLetterOrDigit(keyChar) || keyChar == '.' ||
keyChar == '+' || keyChar == '-') {
write("" + keyChar);
}
}
}
}
if ((keyCode == KeyEvent.VK_TAB)
&& (e.getID() == KeyEvent.KEY_PRESSED)) {
boolean found = false;
if (e.isShiftDown()) {
Collections.reverse(formRanges);
}
for (Iterator iter = formRanges.iterator(); iter.hasNext()
&& (!found);) {
FormRange hotRange = (FormRange) iter.next();
start = hotRange.start;
if (e.isShiftDown()) {
if (start < caretPosition) {
setCaretPosition(start);
found = true;
}
} else {
if (start > caretPosition) {
setCaretPosition(start);
found = true;
}
}
}
if (e.isShiftDown()) {
Collections.sort(formRanges);
}
if (!found && (formRanges.size() > 0)) {
setCaretPosition(((FormRange) formRanges.get(0)).start);
}
}
}
if (needProcess) {
super.processKeyEvent(e);
}
}

/**
* Set the foreground font color to that
* of the foreground text under the cursor
*/
private void setLocalColor(Color color) {
setForeground(color);
}

/**
* reset the "finished" flag to false. This flag remains false
* so long as the user has not submitted the form and blocks the
getValues()
* method from returning.
*/
public void reset() {
finished = false;
}

public void clearLine(int y) {
int i = 0;
if (y >= 1 && y < heightChars) {
//gotoPosition(1, y);
if (blankLine == null) {
blankLine = "";
for (i = 0; i < widthChars; i++) {
blankLine += " ";
}
}
write(blankLine, 0, y);
StyleConstants.setBackground(attrs, backgroundColor);
StyleConstants.setForeground(attrs, foregroundColor);
StyleConstants.setUnderline(attrs, false);
getStyledDocument().setCharacterAttributes(((y -1 ) * (widthChars +
1)), widthChars, attrs, true);
}
}

/**
* Write text to screen with current foreground color
*/
public void write(String string) {
int caretPosition = getCaretPosition();
if (caretPosition + string.length() > maxLength) {
string = string.substring(0, maxLength - caretPosition + 1);
}
int start = caretPosition;
int end = caretPosition + string.length();
setSelectionStart(start);
setSelectionEnd(end);
color(start, end, getForeground());
replaceSelection(string);
setSelectionStart(getCaretPosition());
}

/**
* Set the foreground to prescribed color and
* write text to screen
*/
public void write(String string, Color color) {
setForeground(color);
write(string);
}

/**
* Move cursor to prescribed position and write
* text to screen
*/
public void write(String string, int x, int y) {
gotoPosition(x, y);
write(string);
}

/**
* Move cursor to prescribed position, set foreground color
* to prescribed color and write text to screen
*/
public void write(String string, int x, int y, Color color) {
gotoPosition(x, y);
setForeground(color);
write(string);
}

/**
* Return the values map to the calling object.
* Each value in the map is associated to a key added
* during the call to addFormField().
*/
public Map getValues() {
while (!finished) {
//do nothing until not finished
}
Map map = new HashMap();
for (Iterator iter = formRanges.iterator(); iter.hasNext();) {
FormRange range = (FormRange) iter.next();
try {
String text = getText(range.start, range.end - range.start);
map.put(range.name, text.trim());
} catch (BadLocationException e) {
e.printStackTrace();
}
}
reset();

return map;
}

/**
* Add a form field to the screen with prescribed
* name key and in a given width. the getValues()
* method will return the value associated to this key in
* the input map.
*/
public void addFormField(String fieldName, int width) {
int start = getCaretPosition();
int end = getCaretPosition() + width;
addFormRange(fieldName, start, end);
}

/**
* Add a form field in a prescribed color.
* @see addFormField()
*/
public void addFormField(String fieldName, int width, Color color) {
setForeground(color);
addFormField(fieldName, width);
}

/**
* Move the cursor to a prescribed position on the screen and
* add a form field.
* @see addFormField()
*/
public void addFormField(String fieldName, int x, int y, int width) {
int start = pointToInt(x, y);
int end = pointToInt(x + width, y);
addFormRange(fieldName, start, end);
}

/**
* Move the cursor to a prescribed position on the screen,
* set the foreground color to the prescribed color and
* add a form field.
* @see addFormField()
*/
public void addFormField(String fieldName, int x, int y, int width,
Color color) {
setForeground(color);
int start = pointToInt(x, y);
int end = pointToInt(x + width, y);
addFormRange(fieldName, start, end);
}

/**
* Add a range to the list of form ranges, from the prescribed
* start parameter to the end parameter
*/
private void addFormRange(String fieldName, int start, int end) {
FormRange range = new FormRange(fieldName, start, end, getForeground
());
formRanges.add(range);
Collections.sort(formRanges);
StyleConstants.setUnderline(attrs, true);
color(start, end, getForeground());
getStyledDocument().setCharacterAttributes(start, end - start,
attrs, true);
}

/**
* Form range container class.
* Contains information about a form range, including
* - form field key name
* - start of range
* - end of range
* - color of range
* @author Rene Ghosh
* 6 oct. 2007
*/
class FormRange implements Comparable {
private String name;
private int start;
private int end;
private Color color;

/**
* Constructor using name, start, end and color
* to set into the object
*/
public FormRange(String name, int start, int end, Color color) {
this.name = name;
this.start = start;
this.end = end;
this.color = color;
}

/**
* Returns "true" if the given int is in the
* [start, end] range
*/
public boolean isInRange(int i) {
return (i >= start) && (i < end);
}

/**
* Method to enable sorting on a list of ranges.
*/
public int compareTo(Object other) {
FormRange otherRange = (FormRange) other;
return new Integer(start).compareTo(new Integer(otherRange.start));
}
}
}

-- Program to use the TextConsole --
....
public static void main(String[] args) {
// TODO Auto-generated method stub
A tcApp = new A();
tcApp.start();
}

public void start() {
frame = new JFrame("Testing");
console = new TextConsole(80, 25, 15, "Lucida Console", Color.BLACK,
Color.WHITE);
frame.getContentPane().add(console);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.pack();
frame.setVisible(true);
while (!stop) {
opt = showMainScreen();
switch (opt) {
case 1:
showMainScreen();
break;
case 2:
....
case 6:
stop = true;
break;
}
}
frame.dispose();
}
....
 
J

John B. Matthews

lenyado said:
Hi all, I am now doing my Java homework as a new Java beginner. I've
found the following code from the internet, which I've modified a bit
to meet my need. The purpose is to create a console-like window to
display the UI (as Java's console has very very limited functions,
which cannot be operated as C or C++). But now I've got a mysterious
problem, which happens from time to time and I can never predict when
it will happen -- the text pane seems to "freeze" at replaceSelection
at method "write" (as I've tested to place some console output before
and after this line). The problem is that it seems to work perfectly
right but then it hangs. I have no idea what goes wrong, as when I
debug it in the Eclipse, all the thread are still in running status.

Without trying to debug your long and incomplete example, I see that
you're neglecting to construct your Swing component on the Event
Dispatch Thread (EDT), as suggested here and the page following:

And strangely, I got this exception: "Exception in thread "AWT-
EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 0 >= 0"
occasionally, but the program keeps working.

Yes, the EDT is designed to dispatch events continually.
I am pretty annoyed by this, as there seems no tools that I can used
to debug the program (such as dump the JVM status). As
replaceSelection is stated as "safe" explicitly in APIs, why it's not
behaving safely?

Such sporadic errors suggest incorrect synchronization outside of
replaceSelection(). You don't mention your IDE, but most have useful
displays of the JVM's threads and the call stack in each.
 
L

Lew

lenyado said:
The problem is that it seems to work perfectly
right but then it hangs. I have no idea what goes wrong, as when I
debug it in the Eclipse, all the thread are still in running status.
-- Class TextConsole.java --

For future reference, don't indent code examples for Usenet with TAB
characters. Use a maximum of four spaces for indentation.
import ...

imports and comments elided for brevity.
class TextConsole extends JTextPane {

It's usually a good idea, especially when first learning Java, to declare
classes 'public'.
private static final long serialVersionUID = -5329149879890129297L;

private static final int DEFAULT_FONT_SIZE = 20;
private static final String DEFAULT_FONT_NAME = "Courier New";
private static final int DEFAULT_WIDTH_CHARS = 80;
private static final int DEFAULT_HEIGHT_CHARS = 25;
private static final Color DEFAULT_BACKGROUND_COLOR = Color.BLACK;
private static final Color DEFAULT_FOREGROUND_COLOR = Color.WHITE;

private Font font = null;

It isn't necessary to initialize member variables to 'null'; it only makes the
same initialization happen twice.
private int lastSubmitKey = -1;
private StringBuffer clearBuffer;
private String blankLine = null;
private int[] submitKeys = new int[0];
private Color backgroundColor, foregroundColor;

private volatile boolean finished = false;

It isn't necessary to initialize member variables to 'false'; it only makes
the same initialization happen twice.
MutableAttributeSet attrs = getInputAttributes();

It is necessary to declare 'getInputAttributes()'.
int widthChars = DEFAULT_WIDTH_CHARS;
int heightChars = DEFAULT_HEIGHT_CHARS;

/**
* Maximum number of characters that will hold
* in the console window == width * height
*/
int maxLength = pointToInt(widthChars, heightChars);

Bear in mind that this method call will use the default values for
'widthChars' and 'heightChars', that is, 'DEFAULT_WIDTH_CHARS' and
'DEFAULT_HEIGHT_CHARS', respectively. It will not use the values that are set
later by the constructor. One wonders why you set this value twice, once here
and another time in the explicit constructor.
...
public TextConsole(int width, int height, int fontSize, String
fontName, Color backgroundColor, Color foregroundColor) {
this.font = new Font(fontName, Font.PLAIN, fontSize);
this.widthChars = width;
this.heightChars = height;
this.backgroundColor = backgroundColor;
this.foregroundColor = foregroundColor;
this.maxLength = pointToInt(widthChars, heightChars);
setFont(font);
FontRenderContext fontRenderContext = new FontRenderContext(null,
false, true);
Rectangle2D stringBounds = font.getStringBounds(new char[] { 'W' },
0, 1, fontRenderContext);
setPreferredSize(new Dimension(
(int) (5+ (widthChars + 1) * stringBounds.getWidth()),
(int) (5+ (heightChars + 1) * stringBounds.getHeight())));
setForeground(this.foregroundColor);
setBackground(this.backgroundColor);
setCaretColor(this.foregroundColor);

clearBuffer = new StringBuffer();
for (int j = 0; j < heightChars; j++) {
for (int i = 0; i < widthChars; i++) {
clearBuffer.append(" ");
}
if (j < heightChars - 1) {
clearBuffer.append("\n");
}
}
fill();
}

private void fill() {
setText(clearBuffer.toString());
StyleConstants.setBackground(attrs, backgroundColor);
StyleConstants.setForeground(attrs, foregroundColor);
StyleConstants.setUnderline(attrs, false);
getStyledDocument().setCharacterAttributes(0, maxLength, attrs,
true);
}

private int pointToInt(int i, int j) {
if (i < 0 || i > widthChars || j < 0 || j > heightChars)
return 0;

Aside from the fact that you should have braces around the body part of the
'if', if the width x length were zero then you'd be returning an illegal
location in your int, because the position (0) is >= the length (0). Hmm,
your error message says that the problem is an
"ArrayIndexOutOfBoundsException: 0 >= 0". Coincidence?
int ret = ((j -1 ) * (widthChars + 1)) + (i - 1);
return ret;
}
...

John B. Matthews already pointed out that you need to do all GUI work on the
EDT, and where to learn what that means. As to your concern that "there seems
[sic] no tools that I can used [sic] to debug the program (such as dump the
JVM status)", Eclipse has those tools. It certainly has, as John said,
"useful displays of the JVM's threads and the call stack in each."
 
J

John B. Matthews

[QUOTE="Lew said:
You don't mention your IDE,

He said,
I debug it in the Eclipse,
[/QUOTE]

Egad, you're right! Thanks for catching this. I use the Eclipse
debugger relatively less often, but I see that excellent tutorials are
available [1].

As the OP is starting out, he may want to look at Swing's Threading
Policy [2]. My experience has been that the thread safety of certain
JTextArea methods is a convenience, but not a panacea. As an example,
this demonstration program [3] uses SwingWorker [4] to cleanly separate
background and foreground processing:

[1]<http://www.google.com/search?q=eclipse+debugger+tutorial>
[2]<http://java.sun.com/javase/6/docs/api/javax/swing/package-summary.html>
[3]<http://sites.google.com/site/drjohnbmatthews/randomdata>
[4]<http://java.sun.com/javase/6/docs/api/javax/swing/SwingWorker.html>
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,482
Members
44,901
Latest member
Noble71S45

Latest Threads

Top