Hello, I have a problem with a swing application that contains a JTextPane with syntax highlighting and undo/redo operations.
In the following steps I describe two scenarios.
The first one freezes the whole application, and the second one is working.
But both scenarios are executing the same lines of code.
The freeze:
1. Start the application
2. Type an 'a'
3. Click on the menu item 'Edit->Undo'
4. Click on the menu item 'Edit->Redo'
5. FREEZE
The working one:
1. Start the application
2. Write an 'a'
3. Use the keystroke 'ctrl-Z' to undo
4. Use the keystroke 'ctrl-Y' to redo
5. Everything works fine
I hope somebody can help me, here is the complete test case:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.*;
import java.util.Iterator;
import java.util.LinkedHashMap;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.UndoManager;
public class EditorGUI
{
private JFrame jFrame = null;
private JTextPane jTextPaneEditor = null;
private JMenuBar jMenuBar = null;
private JMenu jMenuEdit = null;
private JMenuItem jMenuItemUndo = null;
private JMenuItem jMenuItemRedo = null;
private UndoManager undoManager = null;
/**
* Constrcuts a new instance of the class <code>EditorGUI</code>.
*/
public EditorGUI()
{
// Set the system look and feel
try
{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (final Exception exception)
{
// Ignore exceptions at this point
}
}
/**
* The main method.
* @param args The application arguments
*/
public static void main(final String[] args)
{
new EditorGUI().getJFrame().setVisible(true);
}
/**
* Returns the main frame.
* @return The main frame
*/
public JFrame getJFrame()
{
if (this.jFrame == null)
{
this.jFrame = new JFrame();
this.jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.jFrame.setSize(800, 600);
this.jFrame.setContentPane(new JPanel());
this.jFrame.setJMenuBar(this.getJMenuBar());
this.jFrame.getContentPane().setLayout(new BorderLayout());
this.jFrame.getContentPane().add(this.getJTextPaneEditor(), BorderLayout.CENTER);
}
return this.jFrame;
}
/**
* Returns the menu bar.
* @return The menu bar
*/
public JMenuBar getJMenuBar()
{
if (this.jMenuBar == null)
{
this.jMenuBar = new JMenuBar();
this.jMenuBar.add(this.getJMenuEdit());
}
return this.jMenuBar;
}
/**
* Returns the menu for edit operations.
* @return The menu for edit operations
*/
public JMenu getJMenuEdit()
{
if (this.jMenuEdit == null)
{
this.jMenuEdit = new JMenu("Edit");
this.jMenuEdit.add(this.getJMenuItemUndo());
this.jMenuEdit.add(this.getJMenuItemRedo());
}
return this.jMenuEdit;
}
/**
* Returns the menu item for redo operations inside of the configuration editor.
* @return The menu item for redo operations
*/
public JMenuItem getJMenuItemRedo()
{
if (this.jMenuItemRedo == null)
{
this.jMenuItemRedo = new JMenuItem("Redo");
final KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK);
this.jMenuItemRedo.setAccelerator(keyStroke);
this.jMenuItemRedo.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(final ActionEvent event)
{
// Fire the redo operation
if (EditorGUI.this.getUndoManager().canRedo())
{
EditorGUI.this.getUndoManager().redo();
}
}
});
}
return this.jMenuItemRedo;
}
/**
* Returns the menu item for undo operations inside of the configuration editor.
* @return The menu item for undo operations
*/
public JMenuItem getJMenuItemUndo()
{
if (this.jMenuItemUndo == null)
{
this.jMenuItemUndo = new JMenuItem("Undo");
final KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK);
this.jMenuItemUndo.setAccelerator(keyStroke);
this.jMenuItemUndo.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(final ActionEvent event)
{
// Fire the undo operation
if (EditorGUI.this.getUndoManager().canUndo())
{
EditorGUI.this.getUndoManager().undo();
}
}
});
}
return this.jMenuItemUndo;
}
/**
* Returns the text pane which realizes the configuration editor.
* @return The text pane which realizes the configuration editor
*/
public JTextPane getJTextPaneEditor()
{
if (this.jTextPaneEditor == null)
{
this.jTextPaneEditor = new JTextPane();
// Create the syntax highlighter
final SyntaxHighlighter syntaxHighlighter = new SyntaxHighlighter(this.jTextPaneEditor, null);
syntaxHighlighter.addHighlight("\"", "\"", new Color(0, 0, 192));
syntaxHighlighter.addHighlight("'", "'", new Color(0, 0, 192));
syntaxHighlighter.addHighlight("/**", "*/", new Color(63, 95, 191));
syntaxHighlighter.addHighlight("/*", "*/", new Color(63, 127, 95));
syntaxHighlighter.addHighlight("//", "\n", new Color(63, 127, 95));
syntaxHighlighter.addHighlight("#", "\n", new Color(63, 127, 95));
// Add the undo manager
final Document document = this.jTextPaneEditor.getDocument();
document.addUndoableEditListener(this.getUndoManager());
}
return this.jTextPaneEditor;
}
/**
* Returns the undo manager for undo/redo operations.
* @return The undo manager for undo/redo operations
*/
public UndoManager getUndoManager()
{
if (this.undoManager == null)
{
this.undoManager = new UndoManager()
{
private static final long serialVersionUID = 998930996883778616L;
@Override
public void undoableEditHappened(final UndoableEditEvent undoableEditEvent)
{
AbstractDocument.DefaultDocumentEvent defaultDocumentEvent;
defaultDocumentEvent = (AbstractDocument.DefaultDocumentEvent) undoableEditEvent.getEdit();
if (defaultDocumentEvent.getType().equals(DocumentEvent.EventType.INSERT)
|| defaultDocumentEvent.getType().equals(DocumentEvent.EventType.REMOVE))
{
this.addEdit(undoableEditEvent.getEdit());
}
}
};
this.getUndoManager().discardAllEdits();
}
return this.undoManager;
}
}
class SyntaxHighlighter implements Runnable, DocumentListener
{
private final LinkedHashMap<String[], Style> highlightMap;
private final StyleContext styleContext;
private final StyledDocument styledDocument;
private final Style mainStyle;
/**
* Constructs a new instance of the class <code>SyntaxHighlighter</code>.
* @param textComponent The text component to use for syntax highlighting
* @param mainStyle The main style to use for the text display
*/
public SyntaxHighlighter(final JTextComponent textComponent, final Style mainStyle)
{
this.highlightMap = new LinkedHashMap<String[], Style>();
this.styleContext = new StyleContext();
this.styledDocument = new DefaultStyledDocument(this.styleContext);
textComponent.setDocument(this.styledDocument);
// Set the main document style
if (mainStyle != null)
{
this.mainStyle = mainStyle;
}
else
{
final Style defaultStyle = this.styleContext.getStyle(StyleContext.DEFAULT_STYLE);
this.mainStyle = this.styleContext.addStyle("mainStyle", defaultStyle);
StyleConstants.setForeground(this.mainStyle, Color.black);
}
// Add the document listener
this.styledDocument.addDocumentListener(this);
}
/**
* Adds a new highlight to this <code>SyntaxHighlighter</code>.
* @param keyword The keyword to be highlighted
* @param color The color to use for highlighting
*/
public void addHighlight(final String keyword, final Color color)
{
final Style style = this.styleContext.addStyle(null, null);
StyleConstants.setForeground(style, color);
this.highlightMap.put(new String[] { keyword, null }, style);
}
/**
* Adds a new highlight to this <code>SyntaxHighlighter</code>.
* @param startTag The start tag of the text to be highlighted
* @param endTag The end tag of the text to be highlighted
* @param color The color to use for highlighting
*/
public void addHighlight(final String startTag, final String endTag, final Color color)
{
final Style style = this.styleContext.addStyle(null, null);
StyleConstants.setForeground(style, color);
this.highlightMap.put(new String[] { startTag, endTag }, style);
}
@Override
public void changedUpdate(final DocumentEvent event)
{
// Empty implementation
}
/**
* Removes all highlights from this <code>SyntaxHighlighter</code>.
*/
public void clearHighlights()
{
this.highlightMap.clear();
}
@Override
public void insertUpdate(final DocumentEvent event)
{
if (event.getDocument().getLength() > 0)
{
SwingUtilities.invokeLater(SyntaxHighlighter.this);
}
}
@Override
public void removeUpdate(final DocumentEvent event)
{
if (event.getDocument().getLength() > 0)
{
SwingUtilities.invokeLater(SyntaxHighlighter.this);
}
}
@Override
public void run()
{
try
{
final String text = this.styledDocument.getText(0, this.styledDocument.getLength());
this.styledDocument.setCharacterAttributes(0, text.length(), this.mainStyle, false);
int documentPointer = 0;
while (documentPointer < text.length())
{
String[] nearestElement = null;
Style nearestStyle = null;
int startPointer = Integer.MAX_VALUE;
// Get the start position of the nearest element with a start
// tag length greater zero
final Iterator<String[]> keyIterator = SyntaxHighlighter.this.highlightMap.keySet().iterator();
final Iterator<Style> styleIterator = SyntaxHighlighter.this.highlightMap.values().iterator();
while (keyIterator.hasNext())
{
final String[] nextElement = keyIterator.next();
final Style nextStyle = styleIterator.next();
final String startTag = nextElement[0];
if (startTag.length() > 0)
{
final int indexOfstartTag = text.indexOf(startTag, documentPointer);
if (indexOfstartTag >= 0 && indexOfstartTag < startPointer)
{
startPointer = indexOfstartTag;
nearestElement = nextElement;
nearestStyle = nextStyle;
}
}
}
// Check for break conditions
if (nearestElement == null || nearestStyle == null)
{
break;
}
// Get the end position of the nearest element to be highlighted
final String startTag = nearestElement[0];
final String endTag = nearestElement[1];
documentPointer = startPointer + startTag.length();
int endPointer = Integer.MAX_VALUE;
int indexOfEndTag = -1;
if (endTag != null)
{
if (endTag.length() > 0)
{
indexOfEndTag = text.indexOf(endTag, documentPointer);
}
else
{
final int indexOfEndTag1 = text.indexOf(" ", documentPointer);
final int indexOfEndTag2 = text.indexOf("\t", documentPointer);
final int indexOfEndTag3 = text.indexOf("\n", documentPointer);
if (indexOfEndTag1 >= 0 && indexOfEndTag2 >= 0)
{
indexOfEndTag = Math.min(indexOfEndTag1, indexOfEndTag2);
}
else if (indexOfEndTag1 >= 0)
{
indexOfEndTag = indexOfEndTag1;
}
else if (indexOfEndTag2 >= 0)
{
indexOfEndTag = indexOfEndTag2;
}
if (indexOfEndTag >= 0 && indexOfEndTag3 >= 0)
{
indexOfEndTag = Math.min(indexOfEndTag, indexOfEndTag3);
}
else if (indexOfEndTag3 >= 0)
{
indexOfEndTag = indexOfEndTag3;
}
}
endPointer = indexOfEndTag + endTag.length();
}
else
{
indexOfEndTag = startPointer;
endPointer = startPointer + startTag.length();
}
// Highlight the next element and set the document pointer behind the highlighted
// element
final int length;
if (indexOfEndTag >= 0)
{
length = endPointer - startPointer;
documentPointer = endPointer;
}
else
{
length = text.length();
documentPointer = text.length();
}
this.styledDocument.setCharacterAttributes(startPointer, length, nearestStyle, false);
}
}
catch (final Throwable throwable)
{
throwable.printStackTrace();
}
}
}