0

For a personal project, in need to provide users a simple IDE with syntax highlighting in java swing. I'm using a jTextPane inserted in a jScrollPane as this component can style the text with many colors. As a consequence, I cannot use a jTextArea.

I've chosen to use a custom DocumentFilter to deal with new text and highlight it when needed. However, I've notice that when doing so, typing characters in one line may alter the word wrapping in other lines. This lead to a very weird behavior as several lines are affected by a single character addition.

Here is a MWE that reproduce the problem :

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.mbrebion.jtextpanetest;

import java.awt.Color;
import javax.swing.JTextPane;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;

/**
 *
 * @author mbrebion
 */
public class mainWindow extends javax.swing.JFrame {

    /**
     * Creates new form mainWindow
     */
    public mainWindow() {
        initComponents();
        CustomDocumentFilter cdf = new CustomDocumentFilter(jTextPane1);
        ((AbstractDocument) jTextPane1.getDocument()).setDocumentFilter(cdf);
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jScrollPane1 = new javax.swing.JScrollPane();
        jTextPane1 = new javax.swing.JTextPane();
        jButton1 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jScrollPane1.setViewportView(jTextPane1);

        jButton1.setText("reStyle");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap(55, Short.MAX_VALUE)
                .addComponent(jButton1)
                .addContainerGap(56, Short.MAX_VALUE))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addGap(0, 0, 0)
                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 323, Short.MAX_VALUE)
                .addGap(2, 2, 2)
                .addComponent(jButton1))
        );

        pack();
    }// </editor-fold>                        

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         

    }                                        

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(mainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(mainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(mainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(mainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new mainWindow().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JButton jButton1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JTextPane jTextPane1;
    // End of variables declaration                   
}

class CustomDocumentFilter extends DocumentFilter {

    protected AttributeSet blackAttributeSet;
    protected StyledDocument styledDocument;
    protected JTextPane jtp;

    public CustomDocumentFilter(JTextPane jtp) {
        this.jtp = jtp;
        styledDocument = jtp.getStyledDocument();
        StyleContext styleContext = StyleContext.getDefaultStyleContext();
        blackAttributeSet = styleContext.addAttribute(styleContext.getEmptySet(), StyleConstants.Foreground, Color.BLACK);
    }

    @Override
    public void insertString(FilterBypass fb, int offset, String text, AttributeSet attributeSet) throws BadLocationException {
        super.insertString(fb, offset, text, attributeSet);
        styledDocument.setCharacterAttributes(0, jtp.getText().length(), blackAttributeSet, true);
    }

    @Override
    public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
        super.remove(fb, offset, length);
        styledDocument.setCharacterAttributes(0, jtp.getText().length(), blackAttributeSet, true);
    }

    @Override
    public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attributeSet) throws BadLocationException {
        super.replace(fb, offset, length, text, attributeSet);
        styledDocument.setCharacterAttributes(0, jtp.getText().length(), blackAttributeSet, true);
    }

}

I've observed that the word wrapping strange behavior does not happen when the custom DocumentFilter is not used. So here are my questions :

  • Is there a way to fix this issue ?
  • If not, is it possible (and easy ?) to perform syntax highlighting without document filter ?

P.S. I've read about problem with word wrapping and long words in java>6 but this problem is different.

  • 1) class names should start with an upper case character. 2) *typing characters in one line may alter the word wrapping in other lines.* - which is the way is should work. As you add text, some text might be added to the next line and cause a cascading effect for other lines. 3) *word wrapping strange behavior does not happen when the custom DocumentFilter is not used.* - still happens for me which is what I would expect. A DocumentFilter has nothing to do with how the text is wrapped. It is used to insert the text into the Document. Maybe you change the style of the text making it larger? – camickr Oct 05 '19 at 14:42
  • *is it possible to perform syntax highlighting without document filter* - you could extend the DefaultStyledDocument and override the insertString(...) and remove(...) methods. This are the methods invoked by the DocumentFilter when text is added or removed. Although this is essentially the same as using a DocumentFilter. This is the approach I used here: https://stackoverflow.com/questions/27332210/how-to-handle-multi-line-comments-in-a-live-syntax-highlighter/27338679#27338679 before I knew about a DocumentFilter. – camickr Oct 05 '19 at 14:43
  • When I say strange behavior, i don't man that all following lines are moved down. If you try to type text in a small window, you'll notice that the other wrapped lines are affected in a strange way (the place where the line is broken is changed when a character is added on other lines). – M. Miguel-Brebion Oct 05 '19 at 15:08
  • No I don't notice a problem. The text wraps when there is a space. If you have strange text then post the text you are testing with in your question so we can see the behaviour. – camickr Oct 05 '19 at 18:34
  • I'm experiencing exactly the same problem as the ones described here : https://stackoverflow.com/a/14230668/9762750 – M. Miguel-Brebion Oct 05 '19 at 18:47

1 Answers1

0

I 've finally found someone who encountered the same problem with jtextpane and a working solution may be found here : https://stackoverflow.com/a/14230668/9762750

The idea is about creating a custom EditorKit which enforces the recomputation of offsets in labelviews. Using this trick perfectly solved the problem.