JEditorPane seems to have a very interesting feature: it seems to track its parent width, and determines the preferred height accordingly, also if the parent IS NOT a JViewport.
By track i mean that the preferred width of the component is set to the one of its parent (maybe apart from some insets).
The ScrollableTracksViewportWidth is false.
This is the very simple code that demonstrates this fact (just copy and fix imports):
When the JFrame is resized, the preferred width of the JEditorPane (in my environement) is always frame.width-14 (of course 14 may be graphical-system specific).
q1) Tracking the parent (non viewport) width is good. Can I rely on it? As far as i know this is an undocumented feature.More! Just replace new JEditorPane() with new JTextPane(), a richer subclass of JEditorPane, and the feature disappear.
q2) It seems to me that this "tracking" happens through the "setting" of the JEditorPane size. This means that FIRST the size (width) must be set, then the preferred size height will be ok. Is it right?
q3) Why JTextPane has not this feature?
public class SSCE01 extends JFrame {
public static void main(String[] a) {
new SSCE01().setVisible(true);
}
public SSCE01() {
final JEditorPane ep = new JEditorPane();
add(ep);
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
Dimension ps = getSize();
System.out.println("Frame size : " + ps.width + " x " + ps.height);
ps = ep.getPreferredSize();
System.out.println("JEditorPane preferredSize: " + ps.width + " x " + ps.height);
}
});
pack();
}
}
q4) More clear question. As hypothesized in q2, setting the size allows the tracking. But just for JEditorPane, NOT for JTextPane. How can I accomplish this for the JTextPane too?
this works:
public SSCE02() {
JEditorPane ep = new JEditorPane();
ep.setText("this is a very very long text. veeeeery long, so long that it will never fit into one 100 pixels width row");
ep.setSize(new Dimension(100,Integer.MAX_VALUE));
add(ep);
pack();
}
this doesn't. It has been used a JTextPane in place of JEditorPane:
public SSCE02() {
JEditorPane ep = new JTextPane();
ep.setText("this is a very very long text. veeeeery long, so long that it will never fit into one 100 pixels width row");
ep.setSize(new Dimension(100,Integer.MAX_VALUE));
add(ep);
pack();
}
UPDATE 1
Summary: the "track Size property" is observed in JEditorPane but nothing similar exists in JTextPane.
A little but significative step further:
Loading an HTML document into JEditorPane lets the feature disappear from JEditorPane too.
At this point, the feature seems implemented by the Document implementation and not by the JEditorPane (or JTextPane) itself! In the case of JEditorPane, the Document is javax.swing.text.PlainDocument. When you do:
URL url = HTMLInComponents01.class.getResource("sample.html");
jEditorPane1.setPage(url);
System.out.println(jEditorPane1.getDocument().getClass().getName());
you'll get:
javax.swing.text.html.HTMLDocument
I also notice that the good javax.swing.text.PlainDocument that gives us this great service of "calculating the height" of a component when the width is given through setSize" isn't assignable to a JTextPane, that expects a StyledDocument instance!
Now I'll verify which other text components are capable of using PlainDocument.
I recommend you add the component to the JFrame's content pane instead of using the add() method. Also set a layout on the content pane and it will all resize automatically.
JFrame f = new JFrame();
f.getContentPane().setLayout(new BorderLayout());
f.getContentPane().add(new JTextPane());
Regards
Related
Unlike JTextArea, JTextPane has no option to turn line wrapping off. I found one solution to turning off line wrapping in JTextPanes, but it seems too verbose for such a simple problem. Is there a better way to do this?
See No Wrap Text Pane. Here's the code included from the link.
JTextPane textPane = new JTextPane();
JPanel noWrapPanel = new JPanel( new BorderLayout() );
noWrapPanel.add( textPane );
JScrollPane scrollPane = new JScrollPane( noWrapPanel );
The No Wrap Text Pane also provides an alternative solution that doesn't require wrapping the JTextPane in a JPanel, instead it overrides getScrollableTracksViewportWidth(). I prefer that solution, but it didn't quite work for me - I noticed that wrapping still occurs if the viewport becomes narrower than the minimum width of the JTextPane.
I found that JEditorPane is overriding getPreferredSize() to try and 'fix' things when the viewport is too narrow by returning the minimum width instead of the preferred width. This can be resolved by overriding getPreferredSize() again to say 'no, really - we always want the actual preferred size':
public class NoWrapJTextPane extends JTextPane {
#Override
public boolean getScrollableTracksViewportWidth() {
// Only track viewport width when the viewport is wider than the preferred width
return getUI().getPreferredSize(this).width
<= getParent().getSize().width;
};
#Override
public Dimension getPreferredSize() {
// Avoid substituting the minimum width for the preferred width when the viewport is too narrow
return getUI().getPreferredSize(this);
};
}
I have a JScrollPane with a number of JLabel objects in a panel using a GridBagLayout. Each of the labels is displaying HTML text with rich elements which varies at run time.
I would like all labels to have the same width (driven by the width of the scroll pane) but vary in height depending on their content with the text wrapping (as is handled automatically by JLabel). If the labels exceed the scroll pane's height then a vertical scroll bar should appear.
Here is some sample code to demonstrate the problem:
public class ScrollLabels extends JFrame {
private final JPanel labelPanel = new JPanel(new GridBagLayout());
private final GridBagConstraints c = new GridBagConstraints();
public ScrollLabels() throws HeadlessException {
super("Scroll Labels");
}
public void createUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JScrollPane scroller = new JScrollPane(labelPanel);
add(scroller);
c.gridx = 0;
c.gridy = GridBagConstraints.RELATIVE;
c.fill = GridBagConstraints.HORIZONTAL;
addLabel("Here is <em>Rich Text</em>");
addLabel("Here is <ul><li>A</li><li>List</li></ul>");
addLabel("Here is <table><tr><th>A</th><th>Table></th></tr></table");
addLabel("Here is more <em>Rich Text</em>");
addLabel("Here is even more <b>Rich Text</b>");
addLabel("Here is a long sentence that should wrap when the panel "
+ "is too small for the text.");
pack();
}
private void addLabel(String text) {
JLabel label = new JLabel("<html>" + text + "</html>");
label.setBorder(BorderFactory.createEtchedBorder());
labelPanel.add(label, c);
}
public static void main(String[] args) {
ScrollLabels frame = new ScrollLabels();
frame.createUI();
frame.setVisible(true);
}
}
It correctly resizes the labels horizontally and shows scroll bars where appropriate. What it doesn't do is resize labels vertically to fit them within the scroll pane.
Here are the various things I have tried:
Changing the GridBagConstraint values. There are good controls for how to expand and contract components but I can't see any way to set a min or max width.
Setting the JScrollPane scroll bar policy to never show horizontal scroll bars. This just cuts off the label text rather than wrapping the text.
Manually setting the label size - i.e. setting the width from the scroll pane and the height depending on the text. I can't see an easy way to get the correct height of rich HTML text given a fixed width. In any case I'd prefer to have a layout manager that can do the job rather than manually coding preferred sizes.
The one thing I haven't tried yet is creating a custom layout manager. I suspect this might be the right answer but would like to see if any of you have an easier solution that I'm not seeing.
I would like all labels to have the same width (driven by the width of the scroll pane) but vary in height depending on their content
You need to implement the Scrollable interface on your panel and override the getScrollableTracksViewportWidth() method to return true. You will also need to provide default implementations for the other methods of the interface.
Or you can use the Scrollable Panel which provides method that allow you to set the scrolling properties.
I'm having an issue creating an empty JTabbedPane where the only portion to be seen on the GUI are the row of tabs.
Everytime I add a new tab with an "empty" component, the height of the JTabbedPane increases, but why?
The current workaround is to override getPreferredSize(), but it seems kludgy to me. Comment out the overridden method to see what I mean.
Am I missing something obvious?
Background:
We need a JTabbedPane where the tabbed pane starts off with 2 tabs, but the user can add more tabs as needed, up to 10. In addition, each tab contains the same components, but with different data. The decision was made to fake the look of a JTabbedPane, by implementing an empty JTabbedPane solely for the look, and to use a single fixed JPanel whose contents will be refreshed based on the tab clicked.
(Normally, I could just recreate the JPanel n-times, but that would nightmarish for the presenter classes who control the UI, which is beyond the scope of my question.)
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CustomTabbedPane implements Runnable
{
static final int MAX_TABS = 11; // includes the "add" tab
JPanel pnlTabs;
JTabbedPane tabbedPane;
public static void main(String[] args)
{
SwingUtilities.invokeLater(new CustomTabbedPane());
}
public void run()
{
JPanel p = buildPanel();
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(p);
frame.setSize(800,400);
frame.setVisible(true);
}
private JPanel buildPanel()
{
tabbedPane = new JTabbedPane()
{
#Override
public Dimension getPreferredSize()
{
Dimension dim = super.getPreferredSize();
dim.height = getUI().getTabBounds(this, 0).height + 1;
return dim;
}
};
tabbedPane.addTab("Tab 1", getEmptyComp());
tabbedPane.addTab("Tab 2", getEmptyComp());
tabbedPane.addTab("+", new TabCreator());
tabbedPane.addMouseListener(new MouseAdapter()
{
#Override
public void mouseClicked(MouseEvent e)
{
addTab();
}
});
JScrollPane scroll = new JScrollPane(new JTable(5,10));
JPanel p = new JPanel(new BorderLayout());
p.add(tabbedPane, BorderLayout.NORTH);
p.add(scroll, BorderLayout.CENTER);
p.setBorder(BorderFactory.createLineBorder(Color.BLUE.darker(), 1));
return p;
}
private void addTab()
{
if (tabbedPane.getSelectedComponent() instanceof TabCreator)
{
int selIndex = tabbedPane.getSelectedIndex();
if (tabbedPane.getComponentCount() < MAX_TABS)
{
if (selIndex == tabbedPane.getComponentCount()-1)
{
String title = "Tab " + (selIndex + 1);
tabbedPane.insertTab(title, null, getEmptyComp(), "", selIndex);
tabbedPane.setSelectedIndex(selIndex);
if (tabbedPane.getComponentCount() == MAX_TABS)
{
tabbedPane.setEnabledAt(MAX_TABS-1, false);
}
}
}
}
}
private Component getEmptyComp()
{
return Box.createVerticalStrut(1);
}
class TabCreator extends JLabel {}
}
Great question! But it's fairly straightforward to get a hint on what's happening.
The problem is that your content does not have a minimum width, preferred size is not set, tab placement is top/bottom and the UI is default.
Since preferred size is not set, then when the layout is revalidated the calculations of space required go into the BasicTabbedPaneUI method Dimension calculateSize(false).
That reads:
int height = 0;
int width = 0;
<other vars>
// Determine minimum size required to display largest
// child in each dimension
<actual method>
Here it calculates the minimum size to accommodate any child and stores it into height/width. In your case this yields something like 10,10 (because of the single Label tab creator I think, I didn't follow that one).
Then happens the magic:
switch(tabPlacement) {
case LEFT:
case RIGHT:
height = Math.max(height, calculateMaxTabHeight(tabPlacement));
tabExtent = preferredTabAreaWidth(tabPlacement, height - tabAreaInsets.top - tabAreaInsets.bottom);
width += tabExtent;
break;
case TOP:
case BOTTOM:
default:
width = Math.max(width, calculateMaxTabWidth(tabPlacement));
tabExtent = preferredTabAreaHeight(tabPlacement, width - tabAreaInsets.left - tabAreaInsets.right);
height += tabExtent;
}
What happens here is it sets the preferred width to be the maximum of the largest tab width and the largest child width. In your case it's around 44 for the tab text. The tabExtent is then calculated to see just how many rows of tabs are needed to support this preferred width. In your case - it's 1 extra row of tabs for each tab. That's where the extra height in preferredSize().height comes from. Essentially because for horizontal tab placement it cares about width first, then height.
How to fix:
Set a preferred size :) I know a lot of people say don't set the preferred size, but in this case this will just work. Since a preferred size is set (via actually setting it, not overriding getPreferredSize()), the code will never get to counting tabs.
Give at least one of your children a size (via setPreferredSize or overriding getPreferredSize). If one of the childrens width is that of the frame, or, say, the table at the bottom the TabbedPane will not be allocating an extra row for each tab, since a single row will fit everything.
Make your own UI for the tabbed pane. It may be easier to make your own tabbed pane though really, I've never done this.
EDIT:
After thinking about this a bit more, I realized that solution number 1 AND your own solution suffer from the flaw that, if the tabbed pane actually does require multiple rows for the tabs (hello frame resizes), bad things will happen. Don't use it.
I have a JTextPane and I have some text within that JTextPane. However, because I have been using HTML within the Pane, the text seems to have been automatically changed to Times New Roman.
I'm trying to set the font type within the JTextPane to the default font of the GUI (the font of the JTextPane when it's not HTML). However I can't just set the font to one font because it differs from operating system, therefore I want to find a way to get the default font and then change the text I have to the default font.
To demonstrate how the text is swapped to Times New Roman when converted, the following code is the format I have used. How could I change it to achieve my goal?
import javax.swing.JFrame;
import javax.swing.JTextPane;
public class GUIExample {
public static void main(String[] args) {
JFrame frame = new JFrame("My App");
frame.setSize(300,300);
JTextPane pane = new JTextPane();
pane.setContentType("text/html");
pane.setText("<html><b>This is some text!</b></html>");
frame.add(pane);
frame.setVisible(true);
}
}
Thanks!
The following will do the trick:
pane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
(Note that JTextPane extends JEditorPane.)
Update (Aug 2016):
For the setting to survive Look & Feel and system changes (e.g. Fonts changed in the Windows Control Panel) the line can be placed here:
#Override
public void updateUI() {
super.updateUI();
putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
}
(This is also called during construction.)
Simplest way is probably something like this:
string fontfamily = pane.getFont().getFamily();
That will give you the default font. Then just apply it using CSS:
pane.setText("<html><body style=\"font-family: " + fontfamily + "\"<b>This is some text!</b></html>");
The JComponent html renderer uses its own font, and not that of the JComponent. In order to get the component to render the same, you need to set the font attributes in the html string. Among other things, you will need to set the font family, size, bold/italics, etc.
As an example, you could do the following:
JTextPane pane = new JTextPane();
Font font = pane.getFont();
pane.setContentType("text/html");
pane.setText("<html><font face=\"" + font.getFamily() + "\" size=\"" + font.getSize() + "\"></font>This is some text!</html>");
It would be fairly trivial to create a function that does this for you. Pass it a JComponent and a string, and it would create the html text for you, including all the font tags.
Unlike JTextArea, JTextPane has no option to turn line wrapping off. I found one solution to turning off line wrapping in JTextPanes, but it seems too verbose for such a simple problem. Is there a better way to do this?
See No Wrap Text Pane. Here's the code included from the link.
JTextPane textPane = new JTextPane();
JPanel noWrapPanel = new JPanel( new BorderLayout() );
noWrapPanel.add( textPane );
JScrollPane scrollPane = new JScrollPane( noWrapPanel );
The No Wrap Text Pane also provides an alternative solution that doesn't require wrapping the JTextPane in a JPanel, instead it overrides getScrollableTracksViewportWidth(). I prefer that solution, but it didn't quite work for me - I noticed that wrapping still occurs if the viewport becomes narrower than the minimum width of the JTextPane.
I found that JEditorPane is overriding getPreferredSize() to try and 'fix' things when the viewport is too narrow by returning the minimum width instead of the preferred width. This can be resolved by overriding getPreferredSize() again to say 'no, really - we always want the actual preferred size':
public class NoWrapJTextPane extends JTextPane {
#Override
public boolean getScrollableTracksViewportWidth() {
// Only track viewport width when the viewport is wider than the preferred width
return getUI().getPreferredSize(this).width
<= getParent().getSize().width;
};
#Override
public Dimension getPreferredSize() {
// Avoid substituting the minimum width for the preferred width when the viewport is too narrow
return getUI().getPreferredSize(this);
};
}