I am using a JTextPane to display characters and symbols, where the latter are represented by custom painted JComponents. For example, the text pane might show something like this:
The text pane is user editable and it is allowed for the user to add more symbols via a button at any position and as a replacement for selected text. I do this via the JTextPane.insertComponent() method. At some point in the application I need to know what is currently being displayed in the text pane, and by that I mean not only the entered text, but also the exact components contained within.
I went through extensive troubles with Positions and DocumentListeners to manage the content of my text pane, but I kept causing more problems than I was solving. That is why I finally decided, that my troubles are probably due to a design fault on my part, so I decided to see, if I can't get to my components through the text pane.
Searching through the documentation and the source code of AbstractDocument and other related classes, I found the interface javax.swing.text.Element. I then let my application output
for(int i = 0; i < textPane.getDocument().getLength(); i++) {
System.out.println(((StyledDocument) textPane.getDocument()).getCharacterElement(i));
}
which gave me:
LeafElement(content) 0,4
LeafElement(content) 0,4
LeafElement(content) 0,4
LeafElement(content) 0,4
LeafElement(component) 4,5
LeafElement(content) 5,9
LeafElement(content) 5,9
LeafElement(content) 5,9
LeafElement(content) 5,9
LeafElement(component) 9,10
Seeing that the LeafElements that I got do seem to have some kind of information about what is displayed at which position in the Document, I figured that it must be possible to get the actual content at that position. After searching for another half hour how to get the content each of the elements represent, I gave up and decided to post my question here, hoping that some of you might know how to accomplish this!?
I have seen this question where someone tries to access the components through textPane.getComponents(), which returns an array of components with the exact number of components actually contained in the JTextPane, but they are all of the type javax.swing.text.ComponentView$Invalidator, which is obviously of no use to me. Maybe I just don't see how to properly continue from here, because a cast to the original type of my symbol doesn't work.
tl;dr
How do I get a JComponent, which is inside the text of a JTextPane, and its position from the text pane?
You can traverse the text pane's StyledDocument to find elements that represent components or icons, as shown below.
BranchElement(section) 0,7
BranchElement(paragraph) 0,7
LeafElement(content) 0,4
LeafElement(icon) 4,5
class javax.swing.plaf.IconUIResource
LeafElement(component) 5,6
class javax.swing.JLabel
LeafElement(content) 6,7
SSCCE:
/**
* #see http://stackoverflow.com/a/15669307/230513
* #see http://stackoverflow.com/questions/2883413
*/
public class DocumentParse {
private static final String ELEM = AbstractDocument.ElementNameAttribute;
private static final String ICON = StyleConstants.IconElementName;
private static final String COMP = StyleConstants.ComponentElementName;
public static void main(String args[]) throws Exception {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextPane jtp = new JTextPane();
StyledDocument doc = (StyledDocument) jtp.getDocument();
SimpleAttributeSet normal = new SimpleAttributeSet();
StyleConstants.setFontFamily(normal, "Serif");
StyleConstants.setFontSize(normal, 72);
StyleConstants.setForeground(normal, Color.blue);
doc.insertString(doc.getLength(), "Test", normal);
jtp.setSelectionStart(doc.getLength());
jtp.insertIcon(UIManager.getIcon("OptionPane.warningIcon"));
jtp.setSelectionStart(doc.getLength());
jtp.insertComponent(new JLabel("Label"));
jtp.setSelectionStart(doc.getLength());
ElementIterator iterator = new ElementIterator(doc);
Element element;
while ((element = iterator.next()) != null) {
System.out.println(element);
AttributeSet as = element.getAttributes();
if (as.containsAttribute(ELEM, ICON)) {
System.out.println(StyleConstants.getIcon(as).getClass());
}
if (as.containsAttribute(ELEM, COMP)) {
System.out.println(StyleConstants.getComponent(as).getClass());
}
}
f.add(jtp);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
The original component is the first (and only) child of the javax.swing.text.ComponentView$Invalidator as you can see from ComponentView.
You can get list of the invalidators and use their children to acccess inserted components.
Related
I am in the midst of a project (software support for a logic textbook) that requires a simple HTML editor. I cannot use the JavaFX HTMLEditor because it does not support superscript / subscript, and because it forces its toolbars. So I have been using JTextPane with a SwingNode. This has gone well up to line spacing. The code below is a stripped-down version of my problem: Each of the commented lines works to format the paragraph. But the uncommented one to set line spacing does not. As exhibited by the print statement, the paragraph attribute does get set. The problem is that the spacing does not render (and in the underlying document there is no change to the HTML paragraph tag, as there is for the other formatting commands).
public class LineSpaceTest extends JFrame {
JTextPane pane = new JTextPane();
LineSpaceTest() {
pane.setContentType("text/html");
getContentPane().add((pane));
MutableAttributeSet mutableAttributeSet = new SimpleAttributeSet();
// StyleConstants.setLeftIndent(mutableAttributeSet, 36f);
// StyleConstants.setRightIndent(mutableAttributeSet, 35.0f);
// StyleConstants.setSpaceAbove(mutableAttributeSet, 20.0f);
// StyleConstants.setSpaceBelow(mutableAttributeSet, 20.0f);
// StyleConstants.setFirstLineIndent(mutableAttributeSet, 36.0f);
// StyleConstants.setAlignment(mutableAttributeSet, StyleConstants.ALIGN_CENTER);
StyleConstants.setLineSpacing(mutableAttributeSet, 2f);
HTMLDocument doc = (HTMLDocument) pane.getDocument();
doc.setParagraphAttributes(0, doc.getLength(), mutableAttributeSet, false);
System.out.println(StyleConstants.getLineSpacing(doc.getParagraphElement(0).getAttributes()));
this.setMinimumSize(new Dimension(500, 300));
setVisible(true);
}
public static void main(String[] args) {
new LineSpaceTest();
}
}
I have had the problem in both Java 8 and Java 14. There is a related question here How to set the line spacing in a JtextPane?. And there appears to have been a related bug fixed in version 1.4.0_02 https://bugs.openjdk.java.net/browse/JDK-4242645. So far as I can tell the proposed solutions do not solve, but only raise, the question why it does not work for me. Of course, I am new to Java (and this is my first StackOverflow post), so I could be missing something simple. . .
This question already has answers here:
Enforce max characters on Swing JTextArea with a few curve balls
(3 answers)
Closed 5 years ago.
I have one question, which pops up during coding:
I want to make sure, about this question, so I hope you could help me!
So, I'm thinking about that, is the JTextArea length infinite?
Or how many chars can be used max?
I tried to write it manual, but I got bored, about 5000 lines, and 100 000 chars, so what's the limit on the JTextArea?
I'm working on a chat program, and this is important for me, but I've nowhere found the answer.
So, I'm thinking about that, is the JTextArea length infinite? Or how many chars can be used max?
No, JTextArea is not infinite.
We can imply the maximum length based on the fact that JTextArea only returns a String, which has a length which returns a int. This implies that the maximum length of a JTextArea is bound to Integer.MAX_VALUE, but, because of array overheads, is slightly smaller. But in practice, you'll probably find that it's much smaller, due to the need for arrays to be laid in memory in a continuous manner, so it will depend on how much memory the JVM has available and how fragmented it is.
We can further investigate this and have a look at PlainDocument, which is the default Document used by JTextArea, which uses a char[] as it's internal data structure, just like String.
This further concretes the reasoning that the limit of a JTextArea is limited to less then Integer.MAX_VALUE
You can have a look at Do Java arrays have a maximum size?, Why I can't create an array with large size? and Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8? for discussions on why an array can't be declared as Integer.MAX_VALUE
Now, before someone suggests that you could write a linked list implementation of a Document, don't forget that both Document and JTextArea rely on String, which is a key limiting factor
I'm working on a chat program, and this is important for me
The text area supports at least several bibles worth of text (i.e. 'a lot'). Far more than could ever be read by a casual reader and immensely more than should appear in a 'chat program'.
Here is a small example that shows more than 1.1 million lines of output on the names of Unicode characters:
import java.awt.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
public class HowLongTextArea {
private JComponent ui = null;
HowLongTextArea() {
initUI();
}
public void initUI() {
if (ui!=null) return;
ui = new JPanel(new BorderLayout(4,4));
ui.setBorder(new EmptyBorder(4,4,4,4));
JTextArea ta = new JTextArea(15, 40);
StringBuilder sb = new StringBuilder();
String eol = System.getProperty("line.separator");
for (int ii=0; ii<Character.MAX_CODE_POINT; ii++) {
sb.append((ii+1) + "\t" + Character.getName(ii) + eol);
if (ii%10000==0) {
System.out.println("ii: " + ii);
}
}
ta.setText(sb.toString());
ui.add(new JScrollPane(ta));
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception useDefault) {
}
HowLongTextArea o = new HowLongTextArea();
JFrame f = new JFrame(o.getClass().getSimpleName());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setLocationByPlatform(true);
f.setContentPane(o.getUI());
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
}
};
SwingUtilities.invokeLater(r);
}
}
Thanks MadProgrammer for correcting me.
If you would like to set limit of JTextArea:
To implement a document filter, create a subclass of DocumentFilter and then attach it to a document using the setDocumentFilter method defined in the AbstractDocument class. Although it is possible to have documents that do not descend from AbstractDocument, by default Swing text components use AbstractDocument subclasses for their documents.
The TextComponentDemo application has a document filter, DocumentSizeFilter, that limits the number of characters that the text pane can contain. Here is the code that creates the filter and attaches it to the text pane's document:
JTextPane textPane;
AbstractDocument doc;
static final int MAX_CHARACTERS = 300;
...
textPane = new JTextPane();
...
StyledDocument styledDoc = textPane.getStyledDocument();
if (styledDoc instanceof AbstractDocument) {
doc = (AbstractDocument)styledDoc;
doc.setDocumentFilter(new DocumentSizeFilter(MAX_CHARACTERS));
}
source:http://docs.oracle.com/javase/tutorial/uiswing/components/generaltext.html#filter
I'm trying to find out how to get the attributes of some selected text in JTextPane.
I found that the best solution is to do it with getInputAttributes() and CaretListener, but I have some issues with this implementation.
Here's my solution showing the attributes of the text on the last position of the caret, but not on the actual position of the caret. What am I doing wrong? Please.
Here is my SSCCE:
public class Testovani{
static JTextPane pane;
static JLabel label;
public static void main(String[] args) throws BadLocationException {
JFrame frame = new JFrame();
frame.setSize(350, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
pane = new JTextPane();
label = new JLabel();
pane.addCaretListener(new SelectionListener());
MutableAttributeSet attrs = new SimpleAttributeSet();
StyleConstants.setBold(attrs, true);
pane.getDocument().insertString(0, "\n", null);
pane.getDocument().insertString(0, "This is first row non bold", null);
pane.getDocument().insertString(0, "\n", null);
pane.getDocument().insertString(0, "This is second row bold", attrs);
pane.getDocument().insertString(0, "\n", null);
pane.getDocument().insertString(0, "This is third row bold", attrs);
pane.getDocument().insertString(0, "\n", null);
frame.add(pane);
frame.add(label, BorderLayout.SOUTH);
frame.setVisible(true);
}
private static class SelectionListener implements CaretListener{
#Override
public void caretUpdate(CaretEvent e) {
AttributeSet attrs =((StyledEditorKit)pane.getEditorKit()).getInputAttributes();
label.setText("Is bold: " + String.valueOf(StyleConstants.isBold(attrs)));
}
}}
And I have two bonus questions. Is this approach functional for selection or just for the position of caret? And what returns, if there is a selection of text where one part is bold and second part is not?
There is no expected behaviour for getting the attributes of a selection, simply because a selection can have character elements with each different attributes.
You have to understand that there is a difference between getInputAttributes which returns the attributes that the textPane calculated the best for a next input, and getCharacterAttributes that returns the attributes of the current caret position. In the case of a selection, the caret is the position where you ended the selection. It can be the getSelectionStart or getSelectionEnd given if you selected text from left to right or from right to left.
Anyway, what I advice you is to get the StyledDocument of your JTextPane, and then iterate through the character elements from getSelectionStart to getSelectionEnd :
for(int i=jtp.getSelectionStart(); i<jtp.getSelectionEnd(); i++) {
AttributeSet set = jtp.getStyledDocument().getCharacterElement(i).getAttributes();
//here, combine, analyse, do whatever you like with your AttributeSet
//You can use StyleConstants.xxx to analyse the attributes
}
Actually attributes of selection is complicated question and need your understanding of business requirements.
Suppose a fragment of text is selected and you need the selection font size. But
the fragment has 3 different pieces of text with 3 different sizes.
Selection start position is placed in the mid of 10pt text, then piece of text with 12pt size and selection ends in the mid of 14pt size fragment.
What size do you expect? 10, 12, 14 or (multiple)?
The simplest approach is to use inputAttributes.
By default the attributes are copied from caret position but of course you can add a caret listener and on each update check and fill the input attributes as you need according to your business logic (processing the multiple text fragments with different attributes).
UPDATE:
Try to wrap the AttributeSet attrs =((StyledEditorKit)pane.getEditorKit()).getInputAttributes(); in SwingUtilities.invokeLater() call
I used #Sharcoux 's solution with one change: I made sure that there there was always at least one character to iterate over. When there's no selection, getSelectionStart() == getSelectionEnd(). For this reason I would make the slight change of:
int iStart = jtp.getSelectionStart();
int iEnd = jtp.getSelectionEnd();
if(iStart > 0) iStart--;
else if(iEnd < jtp.getDocument().getEndPosition().getOffset()) iEnd++;
for(int i = iStart; i < iEnd; i++) {
AttributeSet set = jtp.getStyledDocument().getCharacterElement(i).getAttributes();
//here, combine, analyse, do whatever you like with your AttributeSet
//You can use StyleConstants.xxx to analyse the attributes
}
The only instance in which this changes nothing is when the document is completely empty, in which case getInputAttributes should work fine.
I've got a Jlist inside a JScrollPane and I've set a prototype value so that it doesn't have to calculate the width for big lists, but just uses this default width.
Now, the problem is that the Jlist is for some reason replacing the end of an element with dots (...) so that a horizontal scrollbar will never be shown.
How do I disable with "wrapping"? So that long elements are not being replaced with dots if they are wider than the Jlist's width?
I've reproduced the issue in a small example application. Please run it if you don't understand what I mean:
import javax.swing.*;
import java.awt.*;
public class Test
{
//window
private static final int windowWidth = 450;
private static final int windowHeight = 500;
//components
private JFrame frame;
private JList classesList;
private DefaultListModel classesListModel;
public Test()
{
load();
}
private void load()
{
//create window
frame = new JFrame("Test");
frame.setSize(windowWidth, windowHeight);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setUndecorated(true);
frame.getRootPane().setWindowDecorationStyle(JRootPane.PLAIN_DIALOG);
//classes list
classesListModel = new DefaultListModel();
classesList = new JList(classesListModel);
classesList.setPrototypeCellValue("prototype value");
classesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
classesList.setVisibleRowCount(20);
JScrollPane scrollClasses = new JScrollPane(classesList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
for (int i = 0; i < 200; i++)
{
classesListModel.addElement("this is a long string, does not fit in width");
}
//panel
JPanel drawingArea = new JPanel();
drawingArea.setBackground(Color.white);
drawingArea.add(scrollClasses);
frame.add(drawingArea);
//set visible
frame.setVisible(true);
}
}
Even if you force horizontal scrollbar, you still won't be able to scroll because the element is actually not wider than the width because of the dot (...) wrapping.
Thanks in advance.
Scrollbars appear automatically when the preferred size of the component added to the scrollpane is greater than the size of the scrollpane.
By using the setPrototypeCellValue(...) method you are affecting the way the list calculates its preferred size, which means you are responsible for providing the proper value that ensures the strings will not be truncated.
So the simple solution is not not use that method, but in addition you will need to set the preferred size of the scrollpane to be whatever you want. Then the horizontal scrollbars will appear if required.
My answer to that question is that first find the longest element in the list then use
setPrototype method on that elements
When you call classesList.setPrototypeCellValue("prototype value") you are telling the JList classesList to limit its maximum width to the length of the string "prototype value". (See javadocs)
Then later on when you populate the list with the strings "this is a long string, does not fit in width", no wonder it does not fit in the width! Because the width of the prototype you gave it is smaller than the width of the string you are filling the list with.
The JScrollPane will automatically show the scrollbars and you usually don't need to adjust their behavior. The JList will also automatically adjust its width to try and show the maximum width item in the list. The problem occurs when you tell the JList to fix its width by calling the setPrototypeCellValue().
If you comment out
classesList.setPrototypeCellValue("prototype value");
or replace it with
classesList.setPrototypeCellValue("this is a long string, does not fit in width");
then it will function as you expected it to.
A JTextArea's tab size can easily be set using setTabSize(int).
Is there a similar way to do it with a JEditorPane?
Right now, text with tabs in my pane looks like:
if (stuff){
more stuff;
}
And, I'd prefer a much smaller tab stop:
if (stuff){
more stuff;
}
As JEditorPane is designed to support different kinds of content types, it does not provide a way to specify a "tab size" directly, because the meaning of that should be defined by the content model.
However when you use a model that's a PlainDocument or one of its descendants, there is a "tabSizeAttribute" that provides what you are looking for.
Example:
JEditorPane pane = new JEditorPane(...);
...
Document doc = pane.getDocument();
if (doc instanceof PlainDocument) {
doc.putProperty(PlainDocument.tabSizeAttribute, 8);
}
...
From the Javadoc:
/**
* Name of the attribute that specifies the tab
* size for tabs contained in the content. The
* type for the value is Integer.
*/
public static final String tabSizeAttribute = "tabSize";
In case anyone's using a StyledDocument (The link on the other answer died)
You create a TabSet which is an array of TabStops. In my case I only cared about the 1st tab, and I wanted it 20px from the left, so this code worked for me:
StyleContext sc = StyleContext.getDefaultStyleContext();
TabSet tabs = new TabSet(new TabStop[] { new TabStop(20) });
AttributeSet paraSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.TabSet, tabs);
pane.setParagraphAttributes(paraSet, false);
Took me a while to figure this out.
And decided to use TabStop's in a TabSet that have calculated width based on the font size.
This has to be reset when ever the font size changes (in the paint() method of the JEditPane).
Complicated stuff! :(