2

I'm using a JTextArea in a JFrame. I would like the tab key to insert four spaces instead of a tab.

The method setTabSize does not work, as it puts a tab ('\t') in the contents of the text area.

How can I have JTextArea insert four spaces instead of a tab whenever I press the tab key? That way the getText() method will return indentations of four spaces for every tab.

ktm5124
  • 11,861
  • 21
  • 74
  • 119
  • 2
    Check out: https://stackoverflow.com/a/3103601/131872 for a implementation using a DocumentFilter. – camickr Feb 12 '22 at 20:36
  • @camickr: sorry, I didn't realize that there was a duplicate. 1+ to your answer – DontKnowMuchBut Getting Better Feb 12 '22 at 20:39
  • @DontKnowMuchButGettingBetter well it wasn't really a duplicate since I just added the code. I see you have now added code as well. – camickr Feb 12 '22 at 20:43
  • @AlexeyIvanov *using tabs in this case could still break the columns* Maybe you want something like this: https://stackoverflow.com/a/33557782/131872 – camickr Feb 13 '22 at 03:33
  • @AlexeyIvanov, this is NOT your question. The OP explicitly asked how to replace a tab with 4 spaces. The answer given here does exactly that. *The solution in the answer doesn't take into account the possibilities where the tab is converted to less than four spaces* - because that was NOT the question. If that is an actual requirement, then yes the answer will change. – camickr Feb 13 '22 at 15:14

2 Answers2

4

I would avoid using KeyListeners (as a general rule with JTextComponents) and even Key Bindings, since while Key Bindings would work for keyboard input, it wouldn't work for copy-and-paste.

In my mind, the best way is to use a DocumentFilter set on the JTextArea's Document (which is a PlainDocument, by the way). This way, even if you copy and paste text into the JTextAreas, one with tabs, then all the tabs will automatically be converted to 4 spaces on insertion.

For example:

import javax.swing.*;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.PlainDocument;

public class TestTextArea {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JTextArea textArea = new JTextArea(20, 50);
            JScrollPane scrollPane = new JScrollPane(textArea);

            int spaceCount = 4;
            ((PlainDocument) textArea.getDocument()).setDocumentFilter(new ChangeTabToSpacesFilter(spaceCount));

            JFrame frame = new JFrame("GUI");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(scrollPane);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
    
    private static class ChangeTabToSpacesFilter extends DocumentFilter {
        private int spaceCount;
        private String spaces = "";
        
        public ChangeTabToSpacesFilter(int spaceCount) {
            this.spaceCount = spaceCount;
            for (int i = 0; i < spaceCount; i++) {
                spaces += " ";
            }
        }

        @Override
        public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
                throws BadLocationException {
            string = string.replace("\t", spaces);
            super.insertString(fb, offset, string, attr);
        }
        
        @Override
        public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
            super.remove(fb, offset, length);
        }
        
        @Override
        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
                throws BadLocationException {
            text = text.replace("\t", spaces);
            super.replace(fb, offset, length, text, attrs);
        }
        
    }

}

So now, even if I copy and paste a document with tabs within it, into the JTextArea, all tabs will be automatically replaced with spaceCount spaces.

  • 2
    (1+) for added functionality to control the number of spaces. – camickr Feb 12 '22 at 20:41
  • What if the tab is not at the start of the string? In this case the number of spaces inserted depends on the amount of text to the left. – Alexey Ivanov Feb 13 '22 at 00:00
  • 1
    @AlexeyIvanov: this code would work no matter where tabs are located in the Strings. It is "start agnostic" – DontKnowMuchBut Getting Better Feb 13 '22 at 00:02
  • Yes, the code will work but it will always insert 4 spaces which will break the intended alignment. Consider `println("ab\tcde\tf\nb\tc")`, 'c' has to remain in the same column in the first and second lines. However, this case wasn't stated in the question. – Alexey Ivanov Feb 13 '22 at 00:08
  • @AlexeyIvanov: If alignment were paramount, I would never use tabs but rather `String.format` or `System.out.printf` – DontKnowMuchBut Getting Better Feb 13 '22 at 00:16
  • My example could easily be how data are printed to stdout. I agree using tabs in this case could still break the columns, my point was that it's not uncommon to encounter tabs in the middle of a string. I believe the question implies the tabs at the start of a line, it's the most common case, and it's handled perfectly. – Alexey Ivanov Feb 13 '22 at 00:26
  • @AlexeyIvanov You mean something like [this](https://stackoverflow.com/questions/25922360/how-to-preserve-command-prompts-formatting-inside-a-jtextarea-or-some-other-ty/25922802#25922802)? Then the issue isn’t with the tab size, it’s with your font. You should be using a fixed width font – MadProgrammer Feb 13 '22 at 00:27
  • @AlexeyIvanov We can only answer the specific question, not guess what you are really asking. *using tabs in this case could still break the columns* - so if your question is about how to use a tab for column alignment then maybe you want something like this: https://stackoverflow.com/a/33557782/131872. Of course you will still have problems if the text in one column extends into the next column. If you really need column data then you should probably use a JTable. – camickr Feb 13 '22 at 03:41
  • `+=` should never be used on Strings in a loop. May I suggest `spaces = String.format("%" + spaceCount + "s", " ");` instead of that loop? – VGR Feb 13 '22 at 06:36
  • @MadProgrammer Not really. The sample there uses monospaced font and the output used formatting to align the data in the columns. I assume the use of monospace font is implied in the question. The tab is four spaces. In this `"111\t"` string, the tab is expands to 1 space; `"22\t"` — to 2 spaces; `"3\t"` — to 3 spaces; finally, `"\t"` expands to four spaces. The solution in the answer doesn't take into account the possibilities where the tab is converted to less than four spaces. It is how it works in any text editor. – Alexey Ivanov Feb 13 '22 at 11:04
  • @camickr It's a completely different thing and for this to work the tabs *must be in the text*. By the way, the tabs expanded to `TabStop`s preserve aligned columns and allow for additional formatting (left/center/right aligned) even with proportional fonts. `TabStop`s allow for table-like text structure without using tables. This approach is common in text processors. – Alexey Ivanov Feb 13 '22 at 11:10
  • @camickr _We can only answer the specific question, not guess what you are really asking._ It's true. Yet the OP_ may not realise_ that tabs maybe inside the string (it's not uncommon) and in that case the simplest solution `replace("\t", " ")` doesn't work as expected. I brought it up. – Alexey Ivanov Feb 13 '22 at 11:15
  • @alexeyivanov Don’t assume anything, in my experimentation the font was variable width. You also seem be describing a “tab stop” instead of a “tab space” (but it’s first thing in the morning) – MadProgrammer Feb 13 '22 at 20:05
  • @MadProgrammer There are always tab stops only; with monospace font these can be replaced with spaces, and the default is 8 spaces (columns) for tab. – Alexey Ivanov Feb 14 '22 at 11:44
3

This is one of those “I wonder if…” moments.

Personally, I’d try a tackle the problem more directly, at the source. This means “trapping” the Tab event some how and “replacing” it’s functionality.

So, I started by modifying http://www.java2s.com/Tutorial/Java/0260__Swing-Event/ListingtheKeyBindingsinaComponent.htm, which could list the key bindings for component, for this, I found that the JTextArea was using insert-tab as the action map key.

I then created my own Action designed to insert spaces at the current caret position, for example…

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
//            insert-tab
            JTextArea ta = new JTextArea(10, 20);
            add(new JScrollPane(ta));

            ActionMap am = ta.getActionMap();
            am.put("insert-tab", new SpacesTabAction());
        }

        public class SpacesTabAction extends AbstractAction {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (!(e.getSource() instanceof JTextArea)) {
                    return;
                }
                JTextArea textArea = (JTextArea) e.getSource();
                int caretPosition = textArea.getCaretPosition();
                Document document = textArea.getDocument();
                try {
                    document.insertString(caretPosition, "  ", null);
                } catch (BadLocationException ex) {
                    ex.printStackTrace();
                }
            }

        }

    }
}

Admittable limitations

This will not cover pasting the text into the component, which would otherwise be covered by a DocumentFilter, but, I like to think about scenarios where a DocumentFilter might not be usable (such as having one already installed)

More investigations

The method setTabSize does not work, as it puts a tab ('\t') in the contents of the text area.

Is this one of those “spaces vs tabs” flame wars :P

I did some fiddling and discovered that, most of the inconsistencies with setTabSize came about from not using a fixed width font, for example…

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());
            Font font = Font.decode("Courier New").deriveFont(12);
            JTextArea ta = new JTextArea(10, 40);
            ta.setFont(font);
            ta.setText("---|----|----|----|----|----|----|----|\n\tHello");
            ta.setTabSize(0);
            add(new JScrollPane(ta));

            DefaultComboBoxModel<Integer> model = new DefaultComboBoxModel<>(new Integer[] {0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24});
            JComboBox<Integer> tabSizes = new JComboBox<>(model);
            add(tabSizes, BorderLayout.NORTH);
            tabSizes.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Integer tabSize = (Integer)tabSizes.getSelectedItem();
                    ta.setTabSize(tabSize);
                }
            });
        }

    }
}

When I set the font to “Courier New”, I was able to get a consistent indentation which was in align with the set tab size

enter image description hereenter image description hereenter image description hereenter image description hereenter image description here

I'm obviously missing something...

The tab is four spaces. In this "111\t" string, the tab is expands to 1 space; "22\t" — to 2 spaces; "3\t" — to 3 spaces; finally, "\t" expands to four spaces.

Yes, isn't this the expected behaviour?!

JTextArea, tabSize of 4, mono spaced font enter image description here

Sublime text editor, tabSize of 4, mono spaced font enter image description here

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Font;
import java.util.StringJoiner;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Main {

    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            StringJoiner textJoiner = new StringJoiner("\n");
            for (int row = 0; row < 10; row++) {
                StringJoiner rowJoiner = new StringJoiner("\t");
                for (int col = 0; col < 10; col++) {
                    rowJoiner.add(Integer.toString(col).repeat(row));
                }
                textJoiner.add(rowJoiner.toString());
            }

            setLayout(new BorderLayout());
            JTextArea ta = new JTextArea(10, 40);
            ta.setFont(new Font("Monospaced", Font.PLAIN, 13));
            ta.setText(textJoiner.toString());
            add(new JScrollPane(ta));
        }

        protected String replicate(char value, int times) {
            return new String(new char[times]).replace('\0', value);
        }
    }

}

I think you're thinking of "tab stops", rather than "tab size", for example, in which case, even the DocumentFilter won't do what you're expecting.

I assume the use of monospace font is implied in the question

Don't "assume" we know anything. In my experimentation, the font was NOT monospaced. This is where providing "expected" and "actual" results and a minimal reproducible example, as it removes the ambiguity and doesn't waste everybody's time (especially yours)

It is how it works in any text editor.

I'm obviously using different text editors

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • 1
    (1+) for your always interesting and informative approach. What caught my eye in your text was the statement, "I like to think about scenarios where a DocumentFilter might not be usable (such as having one already installed)", which is true since you `.setXxx` filters, you don't `.addXxx` them. It would make inherent sense if there were a mechanism to chain them, since that is logically how I picture a filter working, but I don't see that as being possible, do you? – DontKnowMuchBut Getting Better Feb 13 '22 at 00:04
  • This is a good idea, thanks. Both solutions are beautiful – ktm5124 Feb 13 '22 at 00:21
  • 1
    @DontKnowMuchButGettingBetter cami it has an example on his WordSpace – MadProgrammer Feb 13 '22 at 00:24
  • 2
    @DontKnowMuchButGettingBetter [Chaining Document Filters](https://tips4java.wordpress.com/2009/10/18/chaining-document-filters/) – MadProgrammer Feb 13 '22 at 00:29
  • _I'm obviously missing something..._ That's exactly the default behaviour which the `DocumentFilter` in [this answer](https://stackoverflow.com/a/71095556/572834) doesn't follow as each tab character gets expanded to four spaces. – Alexey Ivanov Feb 14 '22 at 11:50
  • @AlexeyIvanov But, as I've pointed out, it's also NOT the default behaviour of ALL editors - or at least none of the editors I use do what you're describing – MadProgrammer Feb 14 '22 at 20:55
  • @MadProgrammer How come? Your screenshots under "JTextArea, tabSize of 4, mono spaced font" and "Sublime text editor, tabSize of 4, mono spaced font" demonstrate exactly this. When you use a proportional font, you can't measure the size of one tab in spaces, at least precisely as to align the columns when using tabs vs spaces. In "0\t1", the '\t' is three spaces; in "00\t11", the '\t' is two spaces; if it were "\tx", the '\t' would be four spaces. – Alexey Ivanov Feb 14 '22 at 21:30
  • @AlexeyIvanov Again, this is my point about using fixed width fonts. Even if you use spaces, with proportional fonts, you'll get similar issues, the editor is just "expanding" the tab to spaces anyway. What you seem to want is a "fixed" padding (always 4 spaces) - I fixated on the "setTabSize not working", because, at least from my expectations, it works - this is why I say, don't assume anything ;) – MadProgrammer Feb 14 '22 at 21:38
  • @MadProgrammer No, I don't want the fixed padding. I say _the fixed padding is wrong_ in general. I say it because the solution in [DontKnowMuchBut Getting Better's answer](https://stackoverflow.com/a/71095556/572834) *always* replaces the tab character '\t' with a string of 4 spaces. It works as long as the tabs are at the start of a line, if the tab character is inside, the tab could be expanded to 1, 2, 3, or 4 spaces. All I wanted is to warn the OP or another reader that the provided solution does not handle all the possible cases where the tab character can appear. – Alexey Ivanov Feb 14 '22 at 22:00
  • @AlexeyIvanov I'm sorry, now I'm utterly confused. Using `\t` would, for example do `....` or `1...` or `11..` or `111.` or `1111....`, but you "seem" to be saying that instead you always want `....` or `1....` or `11....` or `111....` or `1111....`, which is what the `DocumentFilter` will do - oh and sorry, I thought you were the OP - What I'm "trying" to do is make sense of what the OP "seems" to want, as I can't seem to understand what's wrong with just using `setTabSize` - BUT, this is getting beyond the scope - most "alignment" issues with text components comes down to font – MadProgrammer Feb 14 '22 at 22:07
  • @MadProgrammer No, this is not what I was saying. I was saying the same thing that you say: the number of spaces per `\t` depends on the length of the string preceding the tab. You've been trying to demonstrate me the same thing that I was talking about. [My 1st comment was](https://stackoverflow.com/questions/71095411/71096812?noredirect=1#comment125679321_71095556): _What if the tab is not at the start of the string? In this case the number of spaces inserted depends on the amount of text to the left._ Because the `DocumentFilter` will output `....`, `1....`, `11....` — **always 4 spaces**. – Alexey Ivanov Feb 14 '22 at 22:15
  • @AlexeyIvanov ... – MadProgrammer Feb 14 '22 at 22:32