1

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>
user1803551
  • 12,965
  • 5
  • 47
  • 74
  • Post an [MCVE](http://stackoverflow.com/help/mcve). Be sure to copy-paste your code to a *new project* and make sure it compiles and runs before posting it here. – user1803551 Feb 16 '16 at 12:07
  • Thanks user1803551. I have now posted what I hope will pass for an MCVE. – user5399283 Feb 17 '16 at 21:42
  • "*lines 126 - 138*" Which are? – user1803551 Feb 17 '16 at 22:45
  • `aLine` in `readLines` is not used, also `debug` in `AlignTest`. Are they needed for something? – user1803551 Feb 17 '16 at 22:49
  • I'm testing by reading a .txt file and it's working well. Maybe supply your own file on which you reproduce the problem. – user1803551 Feb 17 '16 at 22:58
  • Thanks so much for taking the time to help with this user1803551. I can't see an option to upload a file, so I've pasted the html output into my post above - if this is pasted into a text file it reproduces the problem (for me at least). – user5399283 Feb 18 '16 at 21:35
  • 1
    In response to your other comments: I will go and mark the lines I think might be at fault in a moment (in my haste I hadn't noticed there are no line numbers on here!). aLine is used in the loop that gets the number of lines, debug is needed elsewhere in my actual program but not here - I should have, and will now remove it. – user5399283 Feb 18 '16 at 21:53
  • O.K., I would say that if you are doing a lot of HTML manipulation then you should use a proper HTML parser (can give you some links). If all you want is this alignment thing, then I can write a solution. The main problem here is that you need to modify attributes of elements, and there is nothing that does it for you built-in. – user1803551 Feb 19 '16 at 02:11
  • Well I'd be very interested in the links if it's no trouble? It sounds like the HTML parser will be the way forward in the long run. For the short term I think I have come up with a solution, which I will post as an answer to my question. Thanks ever so much for all your help, I don't think I would have come close to a solution without it! And thanks for the offer of writing some code. – user5399283 Feb 19 '16 at 21:59
  • If an answer here solved please [accept an answer](http://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work/5235#5235) to signify that this is solved. You also get 2 reputation points for doing so. You can also upvote answers regardless. – user1803551 Feb 27 '16 at 03:47

3 Answers3

1

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?

Community
  • 1
  • 1
user1803551
  • 12,965
  • 5
  • 47
  • 74
  • Wow thanks user1803551, that's a lot of effort you've gone to! I can't help thinking the issue of boolean replace being set to false, and of the CSS not being saved are just downright bugs, I mean who doesn't want it to replace the attribute? Even if you wanted to lock it so the user can't change it there would surely be other ways of achieving that. But who am I to judge it, I'm just a newby! I will certainly be looking into the parsers at some point. Did you have any thoughts about the short-term solution I came up with? It seems too simple to be true but seems to work fine, with – user5399283 Feb 22 '16 at 22:23
  • one or two obvious limitations. Feel free to say if it will fail at the first hurdle! Thanks again for all your help! – user5399283 Feb 22 '16 at 22:27
  • @user5399283 I don't know about the bugs because you're supposed to define the alignments in a CSS file anyway. The replace boolean doesn't seem like a bug because it replaces the whole element. It could be that I just didn't do it properly. I can look into it a bit more. – user1803551 Feb 22 '16 at 22:50
  • I know this is a very old entry, but I'm fighting today against this odd attrbiutes behavior. I've tried the suggested solution, extended HTMLDocument, overrided setParagraphAttributes. But the current paragraph attribues is a read only value. I could obtain the set whit a call to AttributeSet attr = this.getParagraphElement(offset).getAttributes(); but I can't change it. And trying to cast attr to MutableAttributeSet will cause an Illegal cas exception. – shaman74 Jul 15 '19 at 07:26
  • @shaman74 You should ask a new question with a [MCVE] and point to this one with the explanation you gave in the comment. – user1803551 Jul 15 '19 at 12:48
0

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.
  • This is not robust and will result in excessive code. You will need to create a method for each align operation and you are messing with attributes other than align. You would do better with a regex to catch the value of the align and replace that only. And yes, it will replace all of the alignment attributes in the document. – user1803551 Feb 22 '16 at 22:42
  • Thanks for your comments. I appreciate what you've said and will make replacing this with something more robust a priority. Thanks for all the suggestions! – user5399283 Feb 26 '16 at 17:03
0

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);
    }

}
shaman74
  • 127
  • 2
  • 8