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. . .
Related
There is not a lot to explain. Just see the MCVE/image below:
public class FontExample extends JFrame {
private static final Font FONT = new Font("Calibri", Font.PLAIN, 14);
public FontExample() {
super("");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
JLabel withoutHtml = new JLabel("hello stackoverflow");
withoutHtml.setFont(FONT);
withoutHtml.setBorder(BorderFactory.createLineBorder(Color.red));
add(withoutHtml);
JLabel withHtml = new JLabel("<html><body style='vertical-align:top;'>hello stackoverflow");
withHtml.setBorder(BorderFactory.createLineBorder(Color.green));
withHtml.setFont(FONT);
add(withHtml);
setLocationByPlatform(true);
pack();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
//Make sure Calibri font is installed
if (!"Calibri".equals(FONT.getFamily())) {
System.err.println("Font calibri is not installed.");
System.exit(1);
}
new FontExample().setVisible(true);
});
}
}
The green one is with the <html> tag. Is there a way to fix it? And by fix, I mean to make it like the left one, without this stupid space?
It does not seem to happen with any other font (I tested 2-3 more). I am on Java 8 with Windows 7 and Windows 10.
I tried to add padding at bottom:
JLabel withHtml = new JLabel("<html><body style='padding-bottom:5px'>hello stackoverflow");
and as expected what I get is this:
which a) will screw the alignment of other components in the same container (bad for UI purposes) and b) I will have to hard code a lot of values since 5 since to be the proper for font size 14. But for other font size, it needs another value.
#Andrew Thomson in comments said to use the HTML format for all JLabels. But then, if they are next to another text-based component like a JTextField, I get this:
which obviously, is bad too.
UPDATE
Also, I tried to download Calibri font (among with variations like "Calibri Light", etc) somewhere from the web and install it as described in this question. I do not know if that "Overrides" the existing one, but I had the same result.
A line of text consists of 3 parts:
The ascent
The descent
The leading
To see more clearly, I used Calibri with size 50. The label without HTML is:
In HTML mode, things are different. The HTML renderer puts the leading first (for some reason):
This gives the unpleasant result you have observed.
Now you will ask "But why do I see that effect only with Calibri?" In fact the effect exists with all fonts, but it's usually much smaller, so you don't notice it.
Here is a program that outputs the metrics for some common Windows fonts:
import java.awt.*;
import javax.swing.JLabel;
public class FontInfo
{
static void info(String family, int size)
{
Font font = new Font(family, Font.PLAIN, size);
if(!font.getFamily().equals(family))
throw new RuntimeException("Font not available: "+family);
FontMetrics fm = new JLabel().getFontMetrics(font);
System.out.printf("%-16s %2d %2d %2d\n", family, fm.getAscent(), fm.getDescent(), fm.getLeading());
}
public static void main(String[] args)
{
String[] fonts = {"Arial", "Calibri", "Courier New", "Segoe UI", "Tahoma", "Times New Roman", "Verdana"};
System.out.printf("%-16s %s\n", "", " A D L");
for(String f : fonts)
info(f, 50);
}
}
For size 50, the results are:
A D L
Arial 46 11 2
Calibri 38 13 11
Courier New 42 15 0
Segoe UI 54 13 0
Tahoma 50 11 0
Times New Roman 45 11 2
Verdana 51 11 0
As you can see, the leading for Calibri is huge compared to the other fonts.
For size 14, the results are:
A D L
Arial 13 3 1
Calibri 11 4 3
Courier New 12 5 0
Segoe UI 16 4 0
Tahoma 14 3 0
Times New Roman 13 3 1
Verdana 15 3 0
The leading for Calibri is still 3 pixels. Other fonts have 0 or 1, which means the effect for them is invisible or very small.
It doesn't seem possible to change the behavior of the HTML renderer. However, if the goal is to align the baselines of adjacent components, then it is possible. The FlowLayout you have used has an alignOnBaseline property. If you enable it, it does align the components correctly:
UPDATE 1
Here's a JFixedLabel class that gives the same result, whether it contains HTML or plain text. It translates the Graphics by the leading value when in HTML mode:
import java.awt.Graphics;
import javax.swing.JLabel;
import javax.swing.plaf.basic.BasicHTML;
public class JFixedLabel extends JLabel
{
public JFixedLabel(String text)
{
super(text);
}
#Override
protected void paintComponent(Graphics g)
{
int dy;
if(getClientProperty(BasicHTML.propertyKey)!=null)
dy = getFontMetrics(getFont()).getLeading();
else
dy = 0;
g.translate(0, -dy);
super.paintComponent(g);
g.translate(0, dy);
}
}
Result:
UPDATE 2
The previous solution had an issue with icons, so here's a new one that handles both text and icons. Here we don't extend JLabel, instead we define a new UI class:
import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.basic.BasicHTML;
import javax.swing.plaf.metal.MetalLabelUI;
public class FixedLabelUI extends MetalLabelUI
{
#Override
protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon,
Rectangle viewR, Rectangle iconR, Rectangle textR)
{
String res = super.layoutCL(label, fontMetrics, text, icon, viewR, iconR, textR);
if(label.getClientProperty(BasicHTML.propertyKey)!=null)
textR.y -= fontMetrics.getLeading();
return res;
}
}
To assign the UI to a label, do like this:
JLabel label = new JLabel();
label.setUI(new FixedLabelUI());
Olivier's answer suggests to use flowLayout.setAlignOnBaseline(true); but it will not work in another Layoutmanagers, e.g GridLayout. However, it helped me a lot to find the exact solution I was looking for. Even if it is a messy/hacky one.
Here it is:
If you System.out.println(label.getFontMetrics(label.getFont())), you will see that the actual class of the FontMetrics is FontDesignMetrics. Luckily for us, the getters for the values ascent, descent and leading rely on the fields without some crazy calculations. Luckily for us vol.2, These font metrics are the same (equals) for the same font. That means, we have a single FontDesignMetrics instance of for each Font style-size combination (and obviously its family).
With other words:
private static final Font FONT = new Font("Calibri", Font.PLAIN, 50);
JLabel withoutHtml = new JLabel("hello stackoverflow");
withoutHtml.setFont(FONT);
add(withoutHtml);
JLabel withHtml = new JLabel("<html>hello stackoverflow");
withHtml.setFont(FONT);
FontMetrics withHtmlFontMetrics = withHtml.getFontMetrics(withHtml.getFont());
FontMetrics withoutHtmlFontMetrics = withoutHtml.getFontMetrics(withoutHtml.getFont());
boolean equals = withHtmlFontMetrics.equals(withoutHtmlFontMetrics);
System.out.println(equals);
It prints true even if the getFontMetrics was called in different labels. If you withHtml.setFont(FONT.deriveFont(Font.BOLD)); you will see that it prints false. Because the font is different, we have different font metrics instance.
The fix
(Disclaimer: Desperate times call for desperate measures)
As I already mentioned, it's some sort of hacky and it relies on reflection. With reflection we can manipulate these 3 values. Something like:
FontMetrics fontMetrics = label.getFontMetrics(label.getFont());
Field descentField = fontMetrics.getClass().getDeclaredField("descent");
descentField.setAccessible(true);
descentField.set(fontMetrics, 0);
But you are going to either hard code values for each font size/style, or you can do what I did.
What I did is to copy these values from other font's FontMetrics. It looks that in case of Calibri font, Tahoma is the one.
First, create the method that change the values in the fields, taken from Tahoma font metrics:
private static void copyTahomaFontMetricsTo(JComponent component) {
try {
FontMetrics calibriMetrics = component.getFontMetrics(component.getFont());
// Create a dummy JLabel with tahoma font, to obtain tahoma font metrics
JLabel dummyTahomaLabel = new JLabel();
dummyTahomaLabel.setFont(new Font("Tahoma", component.getFont().getStyle(), component.getFont().getSize()));
FontMetrics tahomaMetrics = dummyTahomaLabel.getFontMetrics(dummyTahomaLabel.getFont());
Field descentField = calibriMetrics.getClass().getDeclaredField("descent");
descentField.setAccessible(true);
descentField.set(calibriMetrics, tahomaMetrics.getDescent());
Field ascentField = calibriMetrics.getClass().getDeclaredField("ascent");
ascentField.setAccessible(true);
ascentField.set(calibriMetrics, tahomaMetrics.getAscent());
Field leadingField = calibriMetrics.getClass().getDeclaredField("leading");
leadingField.setAccessible(true);
leadingField.set(calibriMetrics, tahomaMetrics.getLeading());
} catch (Exception e) {
e.printStackTrace();
}
}
Now, call it by: copyTahomaFontMetricsTo(withHtml); without caring if its the withHtml label or the withoutHtml, since they both have the same font.
The result (font size in frame title):
Even with other text-based components next to it:
As you see, it is works! Plus the layout alignment is not screwed.
It looks perfect, but it's not.
Again, as mentioned earlier, for each font (combination of family, size and style), there is one instance of FontMetrics. Changing one of these label's font to Font.BOLD will stop us from getting perfect alignment. Probably a one (or two) pixels miss. Plus we will have to copyTahomaFontMetricsTo for the Bold as well:
copyTahomaFontMetricsTo(withoutBoldFont);
copyTahomaFontMetricsTo(withBoldFont);
and the result (again font size on frame's title):
Look closer:
There is one pixel difference. But I guess I will take it since this is way (way) better than Swing's/Windows default Calibri-HTML behavior:
The complete example:
public class FontExample extends JFrame {
private static final Font FONT = new Font("Calibri", Font.PLAIN, 20);
public FontExample() {
super("Font: " + FONT.getSize());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
JLabel withoutHtml = new JLabel("hello stackoverflow");
withoutHtml.setBorder(BorderFactory.createLineBorder(Color.GREEN));
withoutHtml.setFont(FONT.deriveFont(Font.BOLD));
add(withoutHtml);
JLabel withHtml = new JLabel("<html>hello stackoverflow");
withHtml.setBorder(BorderFactory.createLineBorder(Color.RED));
withHtml.setFont(FONT);
copyTahomaFontMetricsTo(withoutHtml);
copyTahomaFontMetricsTo(withHtml);
add(withHtml);
setLocationByPlatform(true);
pack();
}
private static void copyTahomaFontMetricsTo(JLabel label) {
try {
FontMetrics calibriMetrics = label.getFontMetrics(label.getFont());
// Create a dummy JLabel with tahoma font, to obtain tahoma font metrics
JLabel dummyTahomaLabel = new JLabel();
dummyTahomaLabel.setFont(new Font("Tahoma", label.getFont().getStyle(), label.getFont().getSize()));
FontMetrics tahomaMetrics = dummyTahomaLabel.getFontMetrics(dummyTahomaLabel.getFont());
Field descentField = calibriMetrics.getClass().getDeclaredField("descent");
descentField.setAccessible(true);
descentField.set(calibriMetrics, tahomaMetrics.getDescent());
Field ascentField = calibriMetrics.getClass().getDeclaredField("ascent");
ascentField.setAccessible(true);
ascentField.set(calibriMetrics, tahomaMetrics.getAscent());
Field leadingField = calibriMetrics.getClass().getDeclaredField("leading");
leadingField.setAccessible(true);
leadingField.set(calibriMetrics, tahomaMetrics.getLeading());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new FontExample().setVisible(true);
});
}
}
Two ways you can probably handle this, add
html {
margin:0;
}
or add padding to both bits of text. :)
Of course you can try
<html style="margin:0;">
<body style='vertical-align:text-bottom;' worked for me, but if I'm misunderstanding your question, you can find other values at https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align
I am new to GWT and have made 3 textarea objects and have added them to a vertical panel, which is also added to my rootpanel. However, I cannot seem to input any text in these textareas. Any suggestions?
VerticalPanel panel = new VerticalPanel();
TextArea tb = new TextArea();
TextArea tb1 = new TextArea();
TextArea tb2 = new TextArea();
panel.add(tb);
panel.add(tb1);
panel.add(tb2);
RootPanel.get().add(panel);
I would try enabling them:
tb.setEnabled(true)
tb1.setEnabled(true)
tb2.setEnabled(true)
But I don't think that should be necessary.
There might be something small you are missing, I would compare all of your code to this. It seems to be a good working example that you could compare your code to and see if you missed a small step.
It seems you may need to add the TextArea objects to horizontal panels and then add those horizontal panels to the vertical panel.
The problem you describe maybe caused by adding another widget on top of your TextArea widgets. In this case TextArea widget may remain visible, but it will be unusable.
I don't see it in the code snippet that you provided, but maybe it's not all of your code.
Try this. It is the example straight from the GWT Javadoc.
Maybe you need to use setCharacterWidth(int size) and setVisibleLines(int size) before adding it.
public class TextBoxExample implements EntryPoint {
public void onModuleLoad() {
//Make an 80 x 50 TextArea
TextArea ta = new TextArea();
ta.setCharacterWidth(80);
ta.setVisibleLines(50);
// Add them to the root panel.
VerticalPanel panel = new VerticalPanel();
panel.add(ta);
RootPanel.get().add(panel);
}
}
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.
My swing app contains a JEditorPane which lets users edit text in a WYSIWYG fashion, including toolbar buttons for bold, italic, and setting fonts, etc.
The content type on the JEditorPane is text/html
The problem: Users want to be able to type tab characters in the editor pane, close the edit dialog (saving HTML text to persistent storage) and see their tabs rendered. However, HTML treats all whitespace runs as a single space.
public class NoteTest {
public static void main(String[] args) {
final JEditorPane editPane1 = new JEditorPane("text/html", "Try typing some tabs");
editPane1.setPreferredSize(new Dimension(400, 300));
JOptionPane.showMessageDialog(null, new JScrollPane(editPane1));
JOptionPane.showMessageDialog(null, new JScrollPane(new JEditorPane("text/html", editPane1.getText()))); // where did the tabs go?
}
}
After typing tabs in the first JEditorPane, we get the HTML text from it, and pass it to a second JEditorPane, which renders the tabs as spaces.
How can I render these tabs? Should I be saving the user-entered content as RTF instead of HTML? Should I parse the HTML and build an HTML table with rows and cells (ugh). Or is there some way to tell swing to render the tab characters as tabs?
For better help sooner, post an SSCCE. This SSCCE does not show the behavior you describe.
Note the 'tab' between the n & g of typin g.
import java.awt.Dimension;
import javax.swing.*;
public class NoteTest {
public static void main(String[] args) {
final JEditorPane editPane1 = new JEditorPane("text/html", "Try typing some tabs");
editPane1.setPreferredSize(new Dimension(400, 300));
JOptionPane.showMessageDialog(null, new JScrollPane(editPane1));
JOptionPane.showMessageDialog(null, new JScrollPane(new JEditorPane("text/html", editPane1.getText()))); // where did the tabs go?
}
}
There were some tabs at the end that disappeared, but that makes sense, since tabs are not supported in HTML correctly unless included in a pre element. I'm guessing that the Swing HTML parsing ignores them as redundant.
What if you use < PRE > < /PRE > tags for the text and for the tab char?
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! :(