user5399283
user5399283

Reputation: 51

StyledEditorKit text align not working properly

I am trying to implement user selectable text alignment in an html document produced in Java. I have tried:

JMenuItem leftAlignMenuItem = 
  new JMenuItem(new StyledEditorKit.AlignmentAction("Left Align", StyleConstants.ALIGN_LEFT));
JMenuItem centerMenuItem = 
  new JMenuItem(new StyledEditorKit.AlignmentAction("Center", StyleConstants.ALIGN_CENTER));
JMenuItem rightAlignMenuItem = 
  new JMenuItem(new StyledEditorKit.AlignmentAction("Right Align", StyleConstants.ALIGN_RIGHT));

and various variations on this theme. Selecting the menu items causes the text to align correctly in the text pane, and adds the appropriate html tag to the document that is saved. The problem is, once the tag is added, clicking another align menu item doesn't change it, so it is not possible to change the text alignment from the default (left) more than once and save the change.

I know I'm not the first person to have this problem but I haven't found any solutions so far, so any help would be most appreciated.

Here is my "M"CVE, which unfortunately is still massive, but I can't remove any more code or it will not demonstrate the problem:

package aligntest;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.JFrame;

public class AlignTest extends JFrame implements ActionListener {

    private HTMLDocument doc; // Stores the formatted text.
    private JTextPane textPane = new JTextPane(); // The Pane itself.
    String FilePath = "";  // Stores the file path.

        public AlignTest() { // This method is called automatically when the app is launched.
            HTMLEditorKit editorKit = new HTMLEditorKit();
            doc = (HTMLDocument)editorKit.createDefaultDocument();  
            init(); // Calls interface method below.
    }

    public static void main(String[] args) {
        AlignTest editor = new AlignTest();
    }
        public void init(){

            JMenuBar menuBar = new JMenuBar();
            getContentPane().add(menuBar, BorderLayout.NORTH);
            JMenu fileMenu = new JMenu("File"); 
            JMenu alignMenu = new JMenu("Text Align");

            menuBar.add(fileMenu);
            menuBar.add(alignMenu);

            JMenuItem openItem = new JMenuItem("Open"); //
            JMenuItem saveItem = new JMenuItem("Save"); //

            openItem.addActionListener(this);
            saveItem.addActionListener(this);

            fileMenu.add(openItem);
            fileMenu.add(saveItem);

            JMenuItem leftAlignMenuItem = new JMenuItem(new StyledEditorKit.AlignmentAction("Left Align", StyleConstants.ALIGN_LEFT));
            JMenuItem centerMenuItem = new JMenuItem(new StyledEditorKit.AlignmentAction("Center", StyleConstants.ALIGN_CENTER));
            JMenuItem rightAlignMenuItem = new JMenuItem(new StyledEditorKit.AlignmentAction("Right Align", StyleConstants.ALIGN_RIGHT));

            leftAlignMenuItem.setText("Left");
            centerMenuItem.setText("Center");
            rightAlignMenuItem.setText("Right");

            alignMenu.add(leftAlignMenuItem);
            alignMenu.add(centerMenuItem);
            alignMenu.add(rightAlignMenuItem);

            textPane = new JTextPane(doc); // Create object from doc and set this as value of textPane.
            textPane.setContentType("text/html"); // textPane holds html.
            JScrollPane scrollPane = new JScrollPane(textPane); // textPane in JScrollPane to allow scrolling if more text than space.
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); // Get screen size to use below.
            Dimension scrollPaneSize = new Dimension(1*screenSize.width/2,1*screenSize.height/2); // Together with next line, sets dimensions of textPane relative to screen size.
            scrollPane.setPreferredSize(scrollPaneSize);
            getContentPane().add(scrollPane, BorderLayout.SOUTH);

            pack();
            setLocationRelativeTo(null);        
            show(); // Actually displays the interface.

        }

        public void actionPerformed(ActionEvent ae) { // Method called with action commands from interface objects above.  Which action depends on the text of the interface element.
            String actionCommand = ae.getActionCommand();           
        if (actionCommand.compareTo("Open") == 0){ // Calls method when action command received.
            openDocument();
        } else if (actionCommand.compareTo("Save") == 0){
            saveDocument();
                }
        }

        public void saveDocument(){

            String FP = FilePath;  // This paragraph calls Save As instead of Save if file not already saved.
            String unsaved = "";
            int saved = FP.compareTo(unsaved);
            if (saved == 0) {
                saveDocumentAs();
            } else {
                save();
            }
        }

        public void saveDocumentAs(){                
            JFileChooser SaveDialog = new javax.swing.JFileChooser();
            int returnVal = SaveDialog.showSaveDialog(this);

            if (returnVal == JFileChooser.APPROVE_OPTION) {
                java.io.File saved_file = SaveDialog.getSelectedFile();
                FilePath = saved_file.toString();

                save();
            }
        }

        public void save(){
            try {
                WriteFile objPane = new WriteFile(FilePath, false);
                String PaneText = textPane.getText();  // Gets text from Title Pane.
                objPane.writeToFile(PaneText);
            }
            catch (Exception ex) {
            }
        }

        public void openDocument(){

            JFileChooser OpenDialog = new javax.swing.JFileChooser(); // Creates file chooser object.
            int returnVal = OpenDialog.showOpenDialog(this);  // Defines 'returnVal' according to what user clicks in file chooser.

            if (returnVal == JFileChooser.APPROVE_OPTION) { // Returns value depending on whether user clicks 'yes' or 'OK' etc.
                java.io.File file = OpenDialog.getSelectedFile(); // Gets path of selected file.
                FilePath = file.toString( ); // Converts path of selected file to String.

// The problem seems to be related to the code that starts here...
                try {
                    ReadFile readPane = new ReadFile(FilePath);  // Creates "readPane" object from "FilePath" string, using my ReadFile class.
                    String[] aryPane = readPane.OpenFile();  // Creates string array "aryPane" from "readPane" object.

                    int i;  // Creates integer variable "i".
                    String PaneText = "";

                    for (i=0; i < aryPane.length; i++) {  //  Creates a for loop with starting "i" value of 0, adding 1 to i each time round and ending when i = the number of lines in the aryLines array.
                        PaneText = PaneText + aryPane[i];  //  Add present line to "PaneText".
                    }
                    textPane.setText(PaneText);  // Displays "PaneText" in "TextPane".

                } catch (Exception ex) {
// and ends here.  This code also calls ReadFile, so code in that class may be at fault.

                }
                }
            }
}

It also has to call methods in the following two classes in order to work:

package aligntest;

import java.io.IOException;
import java.io.FileReader;
import java.io.BufferedReader;

public class ReadFile {

    private String path;

    public ReadFile(String file_path) {
        path = file_path;
    }

    public String[] OpenFile() throws IOException {

        FileReader fr = new FileReader(path);
        BufferedReader textReader = new BufferedReader(fr);

        int numberOfLines = readLines( );
        String[] textData = new String[numberOfLines];

        int i;

        for (i=0; i < numberOfLines; i++) {
            textData[i] = textReader.readLine();
        }

        textReader.close( );
            return textData;
    }


    int readLines() throws IOException {

        FileReader file_to_read = new FileReader(path);
        BufferedReader bf = new BufferedReader(file_to_read);

        String aLine;
        int numberOfLines = 0;

        while ((aLine = bf.readLine()) != null) {
            numberOfLines++;
        }
        bf.close();

        return numberOfLines;
    }

}

&

package aligntest;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.IOException;

public class WriteFile {

    private String path;
    private boolean append_to_file = false;

    public WriteFile(String file_path) {
        path = file_path;
    }

    public WriteFile(String file_path, boolean append_value) {
        path = file_path;
    }

    public WriteFile(File SectionPath, boolean success) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
    public void writeToFile( String textLine ) throws IOException {
        FileWriter write = new FileWriter(path, append_to_file);
        PrintWriter print_line = new PrintWriter(write);

        print_line.printf( "%s" + "%n" , textLine);

        print_line.close();
}
}

The problem appears to be related to opening the document (lines 126 - 138), or the 'ReadFile' class: when viewing the saved file in another program I can see that the tags are changing until the document is closed and then opened again with 'AlignTest'. After this any alignment changes are not reflected in the html.

Hoping someone can help.

Edit: Here is some html produced by 'AlignTest'. If this is pasted into a text file and then opened in 'AlignTest' it should reproduce the problem: 'AlignTest' is unable to change the align tag.

<html>
  <head>
    <meta id="_moz_html_fragment">

  </head>
  <body>
    <p align="right" style="margin-top: 0pt">
      Another
    </p>
  </body>
</html>

Upvotes: 1

Views: 992

Answers (3)

shaman74
shaman74

Reputation: 127

I've tried to implement the solution suggested by @user1803551 but like I've said in my comment above, I could not find a way to use removeAttribute() on the read only paragraph AttributeSet.

I've implemented a different version of the suggested solution, cloning all the paragraph's AttributeSet except the alignment related attributes. Then I override the current attributes, using setParagraphAttributes with replace=true and I apply the requested modification, using setParagraphAttributes with replace=false. And it seems to work pretty well.

public class ExtendedHTMLDocument extends HTMLDocument {

    @Override
    public void setParagraphAttributes(int offset, int length, AttributeSet attr, boolean replace) {
        AttributeSet paragraphAttributes = this.getParagraphElement(offset).getAttributes();
        MutableAttributeSet to =  new SimpleAttributeSet();
        Enumeration<?> keys = paragraphAttributes.getAttributeNames();
        String value = "";
        while (keys.hasMoreElements()) {
            Object key = keys.nextElement();
            if (key instanceof CSS.Attribute) {
                if (!key.equals(CSS.Attribute.TEXT_ALIGN)) {
                    value = value + " " + key + "=" + paragraphAttributes.getAttribute(key) + ";";
                }
            }
            else {
                if (!key.equals(HTML.Attribute.ALIGN)) {
                    to.addAttribute(key, paragraphAttributes.getAttribute(key));
                }
            }
        }
        if (value.length() > 0) {
            to.addAttribute(HTML.Attribute.STYLE, value);
        }
        super.setParagraphAttributes(offset, length, to, true);
        super.setParagraphAttributes(offset, length, attr, replace);
    }

}

Upvotes: 0

user1803551
user1803551

Reputation: 13427

This proved to be more difficult then I had thought. Let me explain what happens behind the scenes and then I'll give a few scenarios.

AlignmentAction's action calls the document's setParagraphAttributes with the boolean replace set to false. In setParagraphAttributes, the given attribute (Alignment.XXX) is added to the current list of attributes of the paragraph tag via MutableAttributeSet.addAttributes. Here is the effect:

  1. If the paragraph tag did not have any alignment instruction, an HTML align="xxx" is added. The file is saved with this new attribute.
  2. If the paragraph tag had only an HTML attribute, an inline CSS attribute is added: text-align=xxx. The file is saved only with the HTML attribute (the CSS attribute is discarded, I don't know why, could be that " needs to be replaced with ').
  3. If the paragraph tag had an HTML and a CSS attribute, the CSS one is modified. The files is saved only the the HTML one.
  4. If the paragraph tag had only a CSS attribute, it is modified. The file is with a new HTML attribute one converted from the CSS one.

The summary is that, for some reason, only the HTML attribute can be saved regardless of which attributes exist in runtime. Since it is not modified, it has to be removed first, and then the new attribute added. It could be that a different writer needs to be used.

One attempt at a solution would be creating your own alignment action and setting the replace value to true. The problem is that it replaces the whole paragraph element:

<html>
  <head>
    <meta id="_moz_html_fragment">

  </head>
  <body>
    <body align="center">
      Another
    </body>
  </body>
</html>

What you need to do is access the element and replace the attribute "manually". Create a class extending HTMLDocument and @override its setParagraphAttributes so that it contains the line

// attr is the current attribute set of the paragraph element
attr.removeAttribute(HTML.Attribute.ALIGN); // remove the HTML attribute

before

attr.addAttributes(s); // s is the given attributes containing the Alignment.XXX style.

Then saving the file will keep the right alignment as per the above 1-4 scenarios.

Eventually you will want to use an HTML parser, like jsoup; just Google for Java HTML parsers. See also Which HTML Parser is the best?

Upvotes: 1

user5399283
user5399283

Reputation: 51

Here is what I have come up with to change text alignment in a JTextPane (may also work in other Swing components):

public void alignLeft() {
        String text = textPane.getText();
        text = text.replace("<p style=\"margin-top: 0\">", "<p align=left style=\"margin-top: 0\">");
        text = text.replace("align=\"center\"", "align=\"left\"");
        text = text.replace("align=\"right\"", "align=\"left\"");
        textPane.setText(text);

and equivalents for center and right align.

If anyone else is thinking about using please be aware:

  • I have not tested it thoroughly
  • it will change alignment of all text in the JTextPane - there is no way for the user to define which text is aligned.

Upvotes: 0

Related Questions