JTabbedPane: tab placement set to LEFT but icons are not aligned - java

I have a JTabbedPane with tab placement set to LEFT. The problem is that icons in each tab are not vertically aligned with one another.
Consider this picture:
As you can see the icon of "Editar Protocolo" (second tab) is not perfectly aligned with the icon of "Distribuir Protocolo" (first tab) and this also happen with the other tabs. I want all icons be vertically aligned to the left.
This is the code I'm using to set tab components:
...
jtabbedPane.setTabComponentAt(1, configurarJtabbedPane("Editar Protocolo", iconEditarProtocolo));
...
public JLabel configurarJtabbedPane(String title, ImageIcon icon) {
JLabel l = new JLabel(title);
l.setIcon(icon);
l.setIconTextGap(5);
l.setHorizontalTextPosition(SwingConstants.RIGHT);
return l;
}
The code is extracted from this Q&A:JTabbedPane: icon on left side of tabs.

What I want: the icons ALL in the LEFT, not based on the Text Size
[...]
The tab's content is centered by typical implementations, and it makes sense because the area needed to fit this content is unpredictable until the tab is effectively rendered. Since the area depends on the content and different tabs will likely have different title lengths, then there has to be a policy about how to render those tabs. The criteria was to center tabs content and fit the tab area to this content. When we have a default tabbed pane with tabs placed at the top, we don't care much about icon/text alignment:
The only concern could be tabs having different length, but who cares? After all, icons and text are visible and tabbed pane looks good enough. However, when you set the tabs placement to LEFT or RIGHT things are different and it looks unappealing:
Apparently this default behavior is a long standing problem, and there's a really interesting discussion here. Some SO members are involved there: #camickr, #kleopatra, #splungebob. As discussed in that post, a simple solution is not possible, and several workarounds were proposed: basically a custom UI implementation or using panels as renderers and playing with preferred width/height based on text length. Both alternatives involve quite a lot of work.
In order to avoid dealing with UI delegates and taking advantage of setTabComponentAt(...) method, I've started some time ago a tabbed pane extension that I'd like to share here. The approach is based on Swing concept of renderer: a class that has to generate a component to render another component's part, and the goal is to provide a flexible mechanism to add custom tab components.
I have included an example below using my custom tabbed pane and here is an overview of all interfaces/classes needed to provide the aforementioned mechanism.
ITabRenderer interface
The first step is to define an iterface to offer a contract to render a tab component.
AbstractTabRenderer class
An abstract class to provide base methods to help in the getTabRendererComponent(...) method implementation. This abstract class has three main properties:
prototypeText: used to define a prototype text to generate a default renderer component.
prototypeIcon: used to define a prototype icon to generate a default renderer.
horizontalTextAlignment: tab's text horizontal alignment.
Note this class is abstract because it doesn't implement getTabRendererComponent(...) method.
DefaultTabRenderer class
A concrete implementation by extending AbstractTabRenderer class. Note that if you want to include a close button as shown in tutorial demo, then a little work in this class would be enough. As a matter of fact, I already did, but I won't include that part to not extend this (already large) post.
JXTabbedPane
Finally the tabbed pane's extension which includes tab renderer support and overrides addTab(...) methods.
Example
I have run this example with positive results using these PLAFs:
WindowsLookAndFeel
WindowsClassicLookAndFeel
NimbusLookAndFeel
MetalLookAndFeel
SeaglassLookAndFeel
Additionaly if you switch tab placement from LEFT to TOP (default) or BOTTOM then all tabs still having the same width, solving the concern described at the second paragraph of this answer.
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class Demo {
private void createAndShowGUI() {
JXTabbedPane tabbedPane = new JXTabbedPane(JTabbedPane.LEFT);
AbstractTabRenderer renderer = (AbstractTabRenderer)tabbedPane.getTabRenderer();
renderer.setPrototypeText("This text is a prototype");
renderer.setHorizontalTextAlignment(SwingConstants.LEADING);
tabbedPane.addTab("Short", UIManager.getIcon("OptionPane.informationIcon"), createEmptyPanel(), "Information tool tip");
tabbedPane.addTab("Long text", UIManager.getIcon("OptionPane.warningIcon"), createEmptyPanel(), "Warning tool tip");
tabbedPane.addTab("This is a really long text", UIManager.getIcon("OptionPane.errorIcon"), createEmptyPanel(), "Error tool tip");
JFrame frame = new JFrame("Demo");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.add(tabbedPane);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JPanel createEmptyPanel() {
JPanel dummyPanel = new JPanel() {
#Override
public Dimension getPreferredSize() {
return isPreferredSizeSet() ?
super.getPreferredSize() : new Dimension(400, 300);
}
};
return dummyPanel;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Demo().createAndShowGUI();
}
});
}
class JXTabbedPane extends JTabbedPane {
private ITabRenderer tabRenderer = new DefaultTabRenderer();
public JXTabbedPane() {
super();
}
public JXTabbedPane(int tabPlacement) {
super(tabPlacement);
}
public JXTabbedPane(int tabPlacement, int tabLayoutPolicy) {
super(tabPlacement, tabLayoutPolicy);
}
public ITabRenderer getTabRenderer() {
return tabRenderer;
}
public void setTabRenderer(ITabRenderer tabRenderer) {
this.tabRenderer = tabRenderer;
}
#Override
public void addTab(String title, Component component) {
this.addTab(title, null, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component) {
this.addTab(title, icon, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component, String tip) {
super.addTab(title, icon, component, tip);
int tabIndex = getTabCount() - 1;
Component tab = tabRenderer.getTabRendererComponent(this, title, icon, tabIndex);
super.setTabComponentAt(tabIndex, tab);
}
}
interface ITabRenderer {
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex);
}
abstract class AbstractTabRenderer implements ITabRenderer {
private String prototypeText = "";
private Icon prototypeIcon = UIManager.getIcon("OptionPane.informationIcon");
private int horizontalTextAlignment = SwingConstants.CENTER;
private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public AbstractTabRenderer() {
super();
}
public void setPrototypeText(String text) {
String oldText = this.prototypeText;
this.prototypeText = text;
firePropertyChange("prototypeText", oldText, text);
}
public String getPrototypeText() {
return prototypeText;
}
public Icon getPrototypeIcon() {
return prototypeIcon;
}
public void setPrototypeIcon(Icon icon) {
Icon oldIcon = this.prototypeIcon;
this.prototypeIcon = icon;
firePropertyChange("prototypeIcon", oldIcon, icon);
}
public int getHorizontalTextAlignment() {
return horizontalTextAlignment;
}
public void setHorizontalTextAlignment(int horizontalTextAlignment) {
this.horizontalTextAlignment = horizontalTextAlignment;
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return propertyChangeSupport.getPropertyChangeListeners();
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return propertyChangeSupport.getPropertyChangeListeners(propertyName);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
PropertyChangeListener[] listeners = getPropertyChangeListeners();
for (int i = listeners.length - 1; i >= 0; i--) {
listeners[i].propertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue));
}
}
}
class DefaultTabRenderer extends AbstractTabRenderer implements PropertyChangeListener {
private Component prototypeComponent;
public DefaultTabRenderer() {
super();
prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(), getHorizontalTextAlignment());
addPropertyChangeListener(this);
}
private Component generateRendererComponent(String text, Icon icon, int horizontalTabTextAlignmen) {
JPanel rendererComponent = new JPanel(new GridBagLayout());
rendererComponent.setOpaque(false);
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 4, 2, 4);
c.fill = GridBagConstraints.HORIZONTAL;
rendererComponent.add(new JLabel(icon), c);
c.gridx = 1;
c.weightx = 1;
rendererComponent.add(new JLabel(text, horizontalTabTextAlignmen), c);
return rendererComponent;
}
#Override
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex) {
Component rendererComponent = generateRendererComponent(text, icon, getHorizontalTextAlignment());
int prototypeWidth = prototypeComponent.getPreferredSize().width;
int prototypeHeight = prototypeComponent.getPreferredSize().height;
rendererComponent.setPreferredSize(new Dimension(prototypeWidth, prototypeHeight));
return rendererComponent;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if ("prototypeText".equals(propertyName) || "prototypeIcon".equals(propertyName)) {
this.prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(), getHorizontalTextAlignment());
}
}
}
}
Screenshots
MetalLookAndFeel
NimbusLookAndFeel
SeaglassLookAndFeel
WindowsLookAndFeel

It's not clear from your fragment where the alignment is going awry. TabComponentsDemo is a complete example that illustrates how to create Tabs With Custom Components. In ButtonTabComponent, note how the component is given a FlowLayout having FlowLayout.LEFT alignment. You might compare this with your current approach.

There is a simpler solution using HTML formatting:
final String PRE_HTML = "<html><p style=\"text-align: left; width: 230px\">";
final String POST_HTML = "</p></html>";
tabbedpane.setTitleAt(0, PRE_HTML + "your title" + POST_HTML);
tabbedpane.setTitleAt(2, PRE_HTML + "your title 2" + POST_HTML);

what I did was to add a component (a JPanel in this case) for the tab. I needed to add a checkbox to the tabs so instead of a checkbox you could add the Icon manually.
here's my code:
private JPanel makeTabPanel(JCheckBox checkBox) {
String title = checkBox.getText();
checkBox.setText("");
JPanel pane = new JPanel();
pane.setLayout(new BoxLayout(pane, BoxLayout.LINE_AXIS));
pane.add(checkBox);
pane.add(new JLabel(title, SwingUtilities.LEFT));
pane.setMinimumSize(new Dimension(150, 25));
pane.setPreferredSize(new Dimension(180, 25));
pane.setMaximumSize(new Dimension(220, 25));
return pane;
}
//then I call it with
tabbed.setTabComponentAt(0, makeTabPanel(checkIncluderesume));
I know it's not good practice to add size to the panels, but this is the easiest method I could find. The results are:
sample

tabbedPane.setTabComponentAt(idx, new JLabel(title, JLabel.LEFT));
This does not work because the JLabel's size fits to the string it displays. See image below. I drew line borders around the JLabels to make their size more obvious:
Swing window with tabs titles center aligned
I got around this by setting the preferredSize of all JLabels to be the same as the longest JLabel.
You will have to do your own code to find the longest title, but once you get it, you can do this:
JLabel prototypeLabel = new JLabel("The longest tab title");
for(int i = 0; i < tabbedPane.getTabCount(); ++i) {
String title = tabbedPane.getTitleAt(i);
JLabel label = new JLabel(title, JLabel.LEFT);
label.setPreferredSize(prototypeLabel.getPreferredSize());
tabbedPane.setTabComponentAt(i, label);
}
Swing window with tabs titles left aligned

Related

Paint two overlaid JLabels

I want to paint a JLabel over another.
I override paint() so that a second JLabel is painted after calling super.paint().
However this second JLabel is not painted at all (because it is not showing, I think).
How can I achieve that effect?
public class OverlaidJLabel extends JLabel {
private String upperText;
public OverlaidJLabel(){
}
public void setUpperText(String upperText){
this.upperText = upperText;
}
#Override
public void paint(Graphics g){
super.paint(g);
JLabel upperLabel = new JLabel(upperText);
upperLabel.paint(g);
}
}
Some big problems here:
You don't want to override paint in this situation but rather paintComponent
You never want to create components within a painting method. These methods should be for painting and painting only as you'll be creating components many times (since painting methods can be called many times -- and completely out of your control) when they only should be created once, and you don't want to ever slow painting down since this hampers the perceived responsiveness of your GUI.
It makes no sense to create a component that is never added to the GUI, such as your upperLabel. Calling its paint method will achieve absolutely nothing (as you're finding out).
In general, you will want to favor composition over inheritance.
Much better:
If you want to have text on top of text, you could create two JLabels (separately), and place them on top of each other using an appropriate layout manager or using a JLayeredPane.
Or you can do your painting and your painting overlay in a single component, likely the paintComponent override of a JPanel.
For example, one possible overly can use a JLayeredPane that holds two JLabels. A simple example below uses JLabels that hold only text, although you could also implement with ImageIcons, and allow changing each JLabel's font, foreground color, etc...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
#SuppressWarnings("serial")
public class MyLabel extends JLayeredPane {
// labels to hold texts. label1 is the lower label
private JLabel label1 = new JLabel();
private JLabel label2 = new JLabel();
public MyLabel(String text1, String text2) {
// just so I can see the text separately:
label2.setForeground(Color.RED);
// set text
label1.setText(text1);
label2.setText(text2);
// JPanels to hold the labels. GridBagLayout will center the labels
JPanel baseComponent1 = new JPanel(new GridBagLayout());
JPanel baseComponent2 = new JPanel(new GridBagLayout());
// add labels to the JPanels
baseComponent1.add(label1);
baseComponent2.add(label2);
// have to be able to see through the JPanels
baseComponent1.setOpaque(false);
baseComponent2.setOpaque(false);
// add to the JLayeredPane label2 above label1
add(baseComponent1, JLayeredPane.DEFAULT_LAYER);
add(baseComponent2, JLayeredPane.PALETTE_LAYER);
// If the overall component resizes, resize the
// container base JPanels
addComponentListener(new ComponentAdapter() {
#Override
public void componentResized(ComponentEvent e) {
baseComponent1.setSize(MyLabel.this.getSize());
baseComponent2.setSize(MyLabel.this.getSize());
repaint();
}
});
}
// preferred size depends on the largest dimensions
// of the two JLabels
#Override
public Dimension getPreferredSize() {
Dimension size1 = label1.getPreferredSize();
Dimension size2 = label2.getPreferredSize();
int w = Math.max(size1.width, size2.width);
int h = Math.max(size1.height, size2.height);
return new Dimension(w, h);
}
}
and it can be tested like so:
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
#SuppressWarnings("serial")
public class MyLabelTest extends JPanel {
public MyLabelTest() {
add(new MyLabel("Test .............................. String 1", "Number 2"));
}
private static void createAndShowGui() {
MyLabelTest mainPanel = new MyLabelTest();
JFrame frame = new JFrame("MyLabelTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
My next iteration would be to add mutator methods to MyLabel to allow changing font, text, and foreground:
public void setFont1(Font font) {
label1.setFont(font);
}
public void setFont2(Font font) {
label2.setFont(font);
}
public void setText1(String text) {
label1.setText(text);
}
public void setText2(String text) {
label2.setText(text);
}
public void setForeground1(Color fg) {
label1.setForeground(fg);
}
public void setForeground2(Color fg) {
label2.setForeground(fg);
}

FlowLayout without linebreaks and/or in vertical direction

This is a follow-up to this question. Any viable answer will also answer that one.
What layout may be used with as little modification as possible to replicate the aligning nature of a FlowLayout, but never linebreak and also be available in a from-top-to-bottom flavour?
The obvious candidate, BoxLayout, does not work nicely with JPanels. Consider the following two examples:
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
class App
{
public static void main(String[] args)
{
JFrame window = new JFrame();
Box box = new Box(BoxLayout.Y_AXIS);
for(int i = 0; i < 5; ++i)
{
JLabel label = new JLabel("XX");
box.add(label);
}
box.add(Box.createVerticalGlue());
window.add(box);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
}
This will properly display a vertical line of labels, beginning at the top and stretching as far towards the bottom as the labels take space. Good.
Modifying this, however, just a tiny bit:
public static void main(String[] args)
{
JFrame window = new JFrame();
Box box = new Box(BoxLayout.Y_AXIS);
for(int i = 0; i < 5; ++i)
{
JLabel label = new JLabel("XX");
JPanel panel = new JPanel();
panel.add(label);
box.add(panel);
}
box.add(Box.createVerticalGlue());
window.add(box);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
This will stretch all components of the Box to the same height, placing the labels far away from each other. Bad.
Overriding the JPanel's getPreferredSize and getMaximumSize methods (with getMinimumSize) has no effect and would be a bad way to fix it, because it relied on the components rather than the container and its layout.
Addendum:
Here is an already pretty successful attempt using GroupLayout. Unfortunately it did not seem to occur to the designer that among DEFAULT_SIZE and PREFERRED_SIZE a choice MINIMUM_SIZE would have been a good idea.
Furthermore if it is possible to invert the sequence of GroupLayout.SequentialGroup, the API is no help to figure out how. I for one certainly have no clue how to even extend that class.
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Container;
import javax.swing.GroupLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
class LineLayout extends GroupLayout
{
public LineLayout(Container owner, int axis)
{
super(owner);
this.direction = axis;
this.direction |= owner.getComponentOrientation() != ComponentOrientation.LEFT_TO_RIGHT
? LineLayout.RIGHT_TO_LEFT : LineLayout.LEFT_TO_RIGHT;
this.setupGroups();
}
public LineLayout(Container owner, int axis, int orientation)
{
super(owner);
this.direction = axis;
this.direction |= orientation;
this.setupGroups();
}
#Override // to replicate FlowLayout functionality : this method is called from owner.add
public void addLayoutComponent(Component component, Object constraints)
{
if(constraints == null)
{
// REALLY surprised that this works, considering that overriding the JPanel's
// getMaximumSize method with getPreferredSize had no effect
this.horizontal.addComponent(component, GroupLayout.DEFAULT_SIZE,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE);
this.vertical.addComponent (component, GroupLayout.DEFAULT_SIZE,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE);
}
// TODO: else
}
protected void setupGroups()
{
super.setAutoCreateGaps(false); // does nothing
if((this.direction & LineLayout.AXIS) == LineLayout.Y_AXIS)
{
this.horizontal = super.createParallelGroup();
this.vertical = (this.direction & LineLayout.ORIENTATION) == LineLayout.RIGHT_TO_LEFT
? this.createSequentialInvertedGroup() : super.createSequentialGroup();
}
else
{
this.horizontal = (this.direction & LineLayout.ORIENTATION) == LineLayout.RIGHT_TO_LEFT
? this.createSequentialInvertedGroup() : super.createSequentialGroup();
this.vertical = super.createParallelGroup();
}
super.setHorizontalGroup(this.horizontal);
super.setVerticalGroup (this.vertical);
}
// How!?
// protected LineLayout.SequentialInvertedGroup createSequentialInvertedGroup() { return new LineLayout.SequentialInvertedGroup(); }
protected GroupLayout.SequentialGroup createSequentialInvertedGroup() { return super.createSequentialGroup(); } // placeholder
protected int direction;
protected GroupLayout.Group horizontal;
protected GroupLayout.Group vertical;
// not sure how reliable the constant field values of BoxLayout are, whether it's smart to assume them unchanging over the ages
public static final int AXIS = 0b1;
public static final int X_AXIS = 0b0; // = BoxLayout.X_AXIS;
public static final int Y_AXIS = 0b1; // = BoxLayout.Y_AXIS;
public static final int ORIENTATION = 0b10;
public static final int LEFT_TO_RIGHT = 0b00; // also top to bottom
public static final int RIGHT_TO_LEFT = 0b10; // also bottom to top
// No idea how; only has "add" methods; cannot actually do anything with the added components!?
//protected static class SequentialInvertedGroup extends GroupLayout.SequentialGroup
//{}
}
class Applikation
{
public static void main(String[] args)
{
JFrame window = new JFrame();
JPanel box = new JPanel();
box.setLayout(new LineLayout(box, LineLayout.Y_AXIS));
for(int i = 0; i < 5; ++i)
{
JLabel label = new JLabel("XX");
JPanel panel = new JPanel();
panel.add(label);
box.add(panel);
}
window.add(box);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
}
If you try this out, you will note that there are still notable border spaces between the "XX" labels, taking up about 2/3 of an extra label per gap. While already much better than in the BoxLayout example, I do not think there is a good way to improve this spacing further.
private static int MAX_HEIGHT = 40;
private static final Dimension DIMENSION = new Dimension(Integer.MAX_VALUE, MAX_HEIGHT);
public static void main(String[] args)
{
JFrame window = new JFrame();
Box box = new Box(BoxLayout.Y_AXIS){
private static final long serialVersionUID = 1L;
#Override
public Component add(Component comp) {
comp.setMaximumSize(DIMENSION);
return super.add(comp);
}
};
for(int i = 0; i < 5; ++i)
{
JLabel label = new JLabel("XX");
JPanel panel = new JPanel();
panel.add(label);
box.add(panel);
}
window.add(box);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.pack();
window.setVisible(true);
}
You are using a Box for adding your components into. And the Documentation says:
a Box can use only a BoxLayout.
Now lets look into the Documentation for BoxLayout. It says:
BoxLayout pays attention to a component's requested minimum, preferred, and maximum sizes.
Now we have found the reason for the different outputs of your two examples. In your first example you are adding JLabels directly to your Box. Since they have a default maximumSize depending on their content they are not scaled by the Box.
In your second example you are adding JPanels to the Box that have your JLabels in it. A JPanel does not have a default maximumSize and so it is scaled by the Box.
So if you want to get the same output with JPanels as without you need your JPanels to have a maximumSize depending on their content means the JLabels.
So you could set a maximumSize manually. Something like that:
panel.setMaximumSize(new Dimension(100,20));
Or you use a different LayoutManager with your JPanels. One that calculates its size depending on its components. One that pays attention to a component's requested minimum, preferred, and maximum sizes.
Does this sound familiar to you? Right its from the Documentation of BoxLayout. So try to use a BoxLayout on your JPanels and you will get exactly the same result as your first example.
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

Aligning and Inlining components (or icons) in JTextPane

I'm working on an application in Java which, among other things, is supposed to display the details of a Magic: The Gathering card in a text field (since I'm using Swing, this is currently a JTextPane).
Those details contain text and small icons, with some of those icons inlined with the text (so the text flows around them), and some icons, by my design, right-aligned with some left-aligned text in the same line.
I took an image from another application which uses a very similar design to the one I'm working on (although not in Java):
This is how it basically should look like.
Now, for the love of everything, I can't get that to work in a JTextPane.
I started with trying to do this with CSS, but found out that the JEditorPane and subclasses don't support the "float" attribute, so I tried it with using the Pane's StyledDocument instead.
I didn't get the first part to work (the icon and right-align text at the top always ignored their alignment and were placed directly after the left-aligned text in the line) at first, until I found this question.
It was suggested to use the following line of code:
pane.setEditorKit(new HTMLEditorKit());
which somehow indeed fixed my first issue.
But now I'm stuck at the second part, getting those icons in the second part inline with the text. Here's what I currently got:
What I've found is that for some reason, when you switch the JTextPane to html mode with an editor kit with the line of code above, inserting components just goes completely crazy.
Both the icons on the top (which I have merged into a single image actually), and the ones in the text below (not merged) are both inside of JLabels, but it doesn't matter if I add them as images or inside of JLabels. The images or labels are definitely not bigger than what you see there, I have no idea at all where the extra whitespace is coming from.
I found this question, and the answer suggest that this is some kind of bug or just weird behavior with the html mode of the JEditorPane.
If I remove the above code line again, I end up with my original problem:
Depending on where exactly the icons are in the text, I get all kinds of different weird results. I put together some more example pictures below:
So, how could I possibly fix this? The JTextPane is working fine for me, except for this part, but I could possibly use some other solution, as long as the end result still looks the same. Remember that I might want to add some other components (like a Button) in there, so I'd like to stick to something native to Swing if possible at all.
The user will not be able to edit the TextPane's contents, but I'd like to add an option later to copy all of the content in one click (so I'd rather stay with a text area).
Below, I have put together a (not really that minimal) working example for you to play around with:
(EDIT: Updated code at the bottom now! The old code is still there under the following link.)
http://pastebin.com/RwAdPCzb
The icons that I'm using are below. You'd need to rename them and change the path in the code.
Some things to notice:
In the beginning I styled the text using the `Style` class, as described in the "How to use Editor Panes and Text Panes" tutorial on the Oracle website (TextSamplerDemo.java, for your reference). With this, I wasn't even able to do the right-align part at the top. Strangely, when I used the `SimpleAttributeSet` class for styling instead, even with the very same settings, it works.
I tried different alignment options for both the text and the labels which contain the icons. Regardless of what options I used, there was no visible difference at all.
UPDATE 1:
After Sharcoux' answer, I have edited my code to have 2 JLabels above the actual JTextPane which hold the two lines that were supposed to have different alignings (a left- and a right-aligned part).
The JTextPane doesn't use a HTMLEditorKit anymore now, and I use insertIcon() to insert the icons into the text.
This way, the icons are inserted (almost) correctly!
Image here:
However, there are two small things that I'm still not satisfied with:
First:
I need to put everything into a JScrollPane because the text in the TextPane is much longer in my actual application. Since I now have three components instead of just the TextPane, I needed to put everything into a JPanel and that into the ScrollPane.
However, if you do it like this, the JTextPane doesn't know that its with should not exceed the JScrollPane's anymore. It stopps wrapping text and just grows as big as the entire text.
I have opened a new question for this, since I feel that this is a basic issue of Swing and deserves its own question. If you want to help, here is the link:
JTextComponent inside JPanel inside JScrollPane
Second:
This is probably not possible, but I guess I'll ask anyway. The icons have the same baseline as the text when you add them this way. Is they any way to move them just a bit lower? 2-3 pixels, maybe? They would align much better with the text that way. Two pictures below.
This is how it looks now:
And this is how I would like it to look:
Maybe I can subclass and override some part of the JTextPane to move all icons that are rendered on it down a set pixel amount, or something like that?
For reference, here is also my new, updated code. I replaced the old one above with a pastebin.com link, if you still want to look at it.
UPDATE 2:
My first problem has already been eliminated! I updated the code below to reflect that, too.
My second question still stands!
Here's the new code:
import java.awt.EventQueue;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.JTextPane;
import javax.swing.JViewport;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import javax.swing.ScrollPaneConstants;
public class MinimalExample extends JFrame {
private JPanel contentPane;
private JScrollPane scrollPane;
private JTextPane textPane;
// Setup some data for an example card:
String name = "Absorb Vis";
String set = "CON";
String manaCost = "{6}{B}";
String printedType = "Sorcery";
String artist = "Brandon Kitkouski";
String rulesText = "Target player loses 4 life and you gain 4 life.\n"
+ "Basic landcycling {1}{B} ({1}{B}, Discard this card: "
+ "Search your library for a basic land card, reveal it, and put "
+ "it into your hand. Then shuffle your library.)";
HashMap<String, BufferedImage> manaSymbolImages;
private ScrollablePanel textPanel;
//private JPanel textPanel;
private JPanel headlinesPanel;
private JPanel firstHeadlinePanel;
private JPanel secondHeadlinePanel;
private JLabel titleLabel;
private JLabel manaCostLabel;
private JLabel typeLabel;
private JLabel setLabel;
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MinimalExample frame = new MinimalExample();
frame.setVisible(true);
}
catch (Exception e) {
e.printStackTrace();
}
}
});
}
public MinimalExample() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 230, 400);
contentPane = new JPanel();
contentPane.setBackground(Color.WHITE);
contentPane.setBorder(null);
setContentPane(contentPane);
/* HTMLEditorKit eKit = new HTMLEditorKit();
* textPane.setEditorKit(eKit); HTMLDocument htmlDoc = (HTMLDocument)
* textPane.getDocument(); htmlDoc.setPreservesUnknownTags(false); */
contentPane.setLayout(new GridLayout(0, 1, 0, 0));
textPanel = new ScrollablePanel();
//textPanel = new JPanel();
textPanel.setBackground(Color.WHITE);
textPanel.setLayout(new BorderLayout(0, 0));
headlinesPanel = new JPanel();
headlinesPanel.setBorder(new EmptyBorder(2, 5, 3, 5));
headlinesPanel.setBackground(Color.WHITE);
textPanel.add(headlinesPanel, BorderLayout.NORTH);
headlinesPanel.setLayout(new GridLayout(0, 1, 0, 0));
firstHeadlinePanel = new JPanel();
firstHeadlinePanel.setBorder(new EmptyBorder(0, 0, 3, 0));
firstHeadlinePanel.setOpaque(false);
headlinesPanel.add(firstHeadlinePanel);
firstHeadlinePanel.setLayout(new BorderLayout(0, 0));
titleLabel = new JLabel("");
titleLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
firstHeadlinePanel.add(titleLabel, BorderLayout.WEST);
manaCostLabel = new JLabel("");
firstHeadlinePanel.add(manaCostLabel, BorderLayout.EAST);
secondHeadlinePanel = new JPanel();
secondHeadlinePanel.setBorder(null);
secondHeadlinePanel.setOpaque(false);
headlinesPanel.add(secondHeadlinePanel);
secondHeadlinePanel.setLayout(new BorderLayout(0, 0));
typeLabel = new JLabel("");
typeLabel.setFont(new Font("Tahoma", Font.PLAIN, 12));
secondHeadlinePanel.add(typeLabel, BorderLayout.WEST);
setLabel = new JLabel("");
setLabel.setFont(new Font("Tahoma", Font.BOLD, 12));
secondHeadlinePanel.add(setLabel, BorderLayout.EAST);
scrollPane = new JScrollPane();
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBackground(Color.WHITE);
contentPane.add(scrollPane);
textPane = new JTextPane();
textPane.setBorder(new EmptyBorder(0, 3, 0, 3));
textPane.setAlignmentY(0.3f);
textPane.setEditable(false);
textPanel.add(textPane, BorderLayout.CENTER);
scrollPane.setViewportView(textPanel);
loadManaCostIcons();
setPaneText();
}
// This part inserts the text into the document of the text pane.
public void setPaneText() {
titleLabel.setText(name);
manaCostLabel.setIcon(combineSymbols(manaCost));
typeLabel.setText(printedType);
setLabel.setText(set);
StyledDocument textPaneDoc = textPane.getStyledDocument();
SimpleAttributeSet defaultAtts = new SimpleAttributeSet();
StyleConstants.setFontFamily(defaultAtts, "SansSerif");
StyleConstants.setFontSize(defaultAtts, 12);
SimpleAttributeSet rulesAtts = new SimpleAttributeSet(defaultAtts);
SimpleAttributeSet artistAtts = new SimpleAttributeSet(defaultAtts);
StyleConstants.setFontSize(artistAtts, 10);
addTextWithSymbols(rulesText, rulesAtts);
try {
textPaneDoc.insertString(textPaneDoc.getLength(), artist, artistAtts);
}
catch (BadLocationException e) {
e.printStackTrace();
}
textPane.revalidate();
textPane.repaint();
}
/* This adds the rest of the text to the pane. The codes for the symbols get
* replaced by the actual symbols and the text gets inserted piece by piece. */
public void addTextWithSymbols(String text, SimpleAttributeSet style) {
StyledDocument textPaneDoc = textPane.getStyledDocument();
Pattern symbolPattern = Pattern.compile("\\{(.*?)\\}");
try {
Matcher symbolMatcher = symbolPattern.matcher(text);
int previousMatch = 0;
while (symbolMatcher.find()) {
int start = symbolMatcher.start();
int end = symbolMatcher.end();
String subStringText = text.substring(previousMatch, start);
String currentMatch = text.substring(start, end);
if (subStringText.isEmpty() == false) {
textPaneDoc.insertString(textPaneDoc.getLength(), subStringText, style);
}
ImageIcon currentIcon = new ImageIcon(manaSymbolImages.get(currentMatch));
SimpleAttributeSet iconAtts = new SimpleAttributeSet();
JLabel iconLabel = new JLabel(currentIcon);
StyleConstants.setComponent(iconAtts, iconLabel);
textPane.insertIcon(currentIcon);
previousMatch = end;
}
String subStringText = text.substring(previousMatch);
if (subStringText.isEmpty() == false) {
textPaneDoc.insertString(textPaneDoc.getLength(), subStringText + "\n", style);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
/* Everything below is more or less irrelevant. However, you might need to
* adjust the image image file paths. */
public void loadManaCostIcons() {
manaSymbolImages = new HashMap<String, BufferedImage>();
try {
// Most likely, those paths won't work for you!
File bFile = new File("resource/B.png");
File c1File = new File("resource/1.png");
File c6File = new File("resource/6.png");
manaSymbolImages.put("{B}", ImageIO.read(bFile));
manaSymbolImages.put("{1}", ImageIO.read(c1File));
manaSymbolImages.put("{6}", ImageIO.read(c6File));
}
catch (IOException e) {
e.printStackTrace();
}
}
public ImageIcon combineSymbols(String symbols) {
String[] manaSymbols = symbols.split("(?<=})");
int combinedWidth = 0;
int maxHeight = 0;
for (int i = 0; i < manaSymbols.length; i++) {
BufferedImage currentSymbolImage = manaSymbolImages.get(manaSymbols[i]);
combinedWidth += currentSymbolImage.getWidth();
if (maxHeight < currentSymbolImage.getWidth()) {
maxHeight = currentSymbolImage.getWidth();
}
}
BufferedImage combinedManaCostImage = new BufferedImage(combinedWidth, maxHeight,
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = combinedManaCostImage.createGraphics();
int currentPosition = 0;
for (int i = 0; i < manaSymbols.length; i++) {
BufferedImage tempCurrentImage = manaSymbolImages.get(manaSymbols[i].trim());
graphics.drawImage(tempCurrentImage, null, currentPosition, 0);
currentPosition += tempCurrentImage.getWidth();
}
graphics.dispose();
return (new ImageIcon(combinedManaCostImage));
}
/* Original source of this is here:
* https://stackoverflow.com/questions/15783014/jtextarea-on-jpanel-inside-jscrollpane-does-not-resize-properly
* And one update to it is here:
* */
private static class ScrollablePanel extends JPanel implements Scrollable {
#Override
public Dimension getPreferredScrollableViewportSize() {
return super.getPreferredSize();
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 16;
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 16;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
#Override
public boolean getScrollableTracksViewportHeight() {
boolean track = true;
Container parent = getParent();
if (parent instanceof JViewport) {
JViewport viewport = (JViewport) parent;
if (viewport.getHeight() < getPreferredSize().height) {
track = false;
}
}
return track;
}
}
}
I think that the issue is the way you insert your images, and most probably, from your combineSymbol method.
Here is the way to insert stuff in the JTextPane :
public class Test {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame mainFrame = new JFrame("test");
mainFrame.setSize(300, 100);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container pane = mainFrame.getContentPane();
pane.setLayout(new BorderLayout());
JTP jtp = new JTP();
pane.add(jtp);
mainFrame.setVisible(true);
}
});
}
static class JTP extends JTextPane {
JTP() {
HTMLEditorKit eKit = new HTMLEditorKit();
setEditorKit(eKit);
HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML
//inserting plain text (just change null for an attributeSet for styled text)
try {
htmlDoc.insertString(0, "test", null);
} catch (BadLocationException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
//inserting images
insertIcon(new ImageIcon("image.png"));
insertIcon(new ImageIcon("image.png"));
//inserting components (With component, you should specify the yAlignment by yourself)
JLabel label = new JLabel(new ImageIcon("image.png"));
label.setAlignmentY(JLabel.TOP);
insertComponent(label);
}
}
}
But to make things easier, I strongly advice you to use a title line outside the JTextPane. Text editors aren't really made for text having different alignment on the same line. Here is what I would suggest :
public class Test {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame mainFrame = new JFrame("test");
mainFrame.setSize(300, 100);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container pane = mainFrame.getContentPane();
pane.setLayout(new BorderLayout());
pane.setBackground(Color.WHITE);
pane.add(new JTP());
pane.add(new Title(), BorderLayout.NORTH);
mainFrame.setVisible(true);
}
});
}
static class JTP extends JTextPane {
JTP() {
setEditable(false);
setOpaque(false);
HTMLEditorKit eKit = new HTMLEditorKit();
setEditorKit(eKit);
HTMLDocument htmlDoc = (HTMLDocument) getDocument();//the HTMLEditorKit automatically created an HTMLDocument
htmlDoc.setPreservesUnknownTags(false);//I advice you to put this line if you plan to insert some foreign HTML
//inserting plain text (just change null for an attributeSet for styled text)
try {
htmlDoc.insertString(0, "capacity : ", null);
} catch (BadLocationException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
//inserting images
insertIcon(new ImageIcon("image.png"));
insertIcon(new ImageIcon("image.png"));
//inserting components (With component, you should specify the yAlignment by yourself)
JLabel label = new JLabel(new ImageIcon("image.png"));
label.setAlignmentY(JLabel.TOP);
insertComponent(label);
}
}
static class Title extends JPanel {
Title() {
setLayout(new BorderLayout());
setOpaque(false);
add(new JLabel("<html><b>Card title</b></html>"), BorderLayout.CENTER);
add(new JLabel(new ImageIcon("image.png")), BorderLayout.EAST);
}
}
}
You can try to define your own TabStops to align icons.
If you know size of icon and width of JTextPane just add your content as
"Person Name -tab- icon" and set custom TabSet for the paragraph. The TabSet has just one TabStop. The TabStop position = jTextPaneWidth - iconWidth

How to change the width of tab labels in java but keep the "selection" options

I have a JTabbedPane object in my program and I override the getForegroundAt and getBackgroundAt methods in order to have different background colours when the tab is selected or not. I want to change the width and height of the tabs. I managed to do that using code similar as the following:
jtp.addTab("<html><body><table width='200'>Main</table></body></html>", mainPanel);
The problem is that if I use this html code to change the width of the tabs, the methods which I override are not longer called because the options are set with the html code. Is there a way to work around this problem? Is there html code that I can use in order to change the background colour of the tab depending on whether it is selected or not? Thanks.
Here's one way to change the width of the tabs by overriding calculateTabWidth(...) in the JTabbedPane's UI:
EDIT: MadProgrammer's comment is correct. I've changed the sample from BasicTabbedPaneUI to MetalTabbedPaneUI, since that's the default UI used for this sample. If you're specifiying a specific L&F for your app, then change the UI accordingly.
import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.metal.*;
public class CustomTabWidthDemo implements Runnable
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new CustomTabWidthDemo());
}
public void run()
{
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.setUI(new MetalTabbedPaneUI()
{
#Override
protected int calculateTabWidth(int tabPlacement, int tabIndex,
FontMetrics metrics)
{
int width = super.calculateTabWidth(tabPlacement, tabIndex, metrics);
int extra = tabIndex * 50;
return width + extra;
}
});
tabbedPane.addTab("JTable", new JScrollPane(new JTable(5,5)));
tabbedPane.addTab("JTree", new JScrollPane(new JTree()));
tabbedPane.addTab("JSplitPane", new JSplitPane());
JPanel p = new JPanel();
p.add(tabbedPane);
JFrame frame = new JFrame();
frame.setContentPane(p);
frame.pack();
frame.setVisible(true);
}
}

How do I make JScrollPane scroll to follow input focus?

I have a Swing app with a large panel which is wrapped in a JScrollPane. Users normally move between the panel's subcomponents by tabbing, so when they tab to something out view, I want the scroll pane to autoscroll so the component with input focus is always visible.
I've tried using KeyboardFocusManager to listen for input focus changes, and then calling scrollRectToVisible.
Here's an SSCCE displaying my current strategy (just copy/paste and run!):
import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener("focusOwner",
new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
focused.scrollRectToVisible(focused.getBounds());
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
If you run this example, you'll notice it doesn't work very well. It does get the focus change notifications, but the call to scrollRectToVisible doesn't appear to have any effect. In my app (which is too complex to show here), scrollRectToVisible works about half the time when I tab into something outside of the viewport.
Is there an established way to solve this problem? If it makes any difference, the Swing app is built on Netbeans RCP (and most of our customers run Windows).
My comment to the other answer:
scrollRectToVisible on the component itself is the whole point of that
method ;-) It's passed up the hierarchy until a parent doing the
scroll is found
... except when the component itself handles it - as JTextField does: it's implemented to scroll horizontally to make the caret visible. The way out is to call the method on the field's parent.
Edit
just for clarity, the replaced line is
content.scrollRectToVisible(focused.getBounds());
you have to take Rectangle from JPanel and JViewPort too, then compare, for example
notice (against down-voting) for final and nice output required some work for positions in the JViewPort
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
//http://stackoverflow.com/questions/8245328/how-do-i-make-jscrollpane-scroll-to-follow-input-focus
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
final JScrollPane scroll = new JScrollPane(content);
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JViewport viewport = (JViewport) content.getParent();
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
Rectangle rect = focused.getBounds();
Rectangle r2 = viewport.getVisibleRect();
content.scrollRectToVisible(new Rectangle(rect.x, rect.y, (int) r2.getWidth(), (int) r2.getHeight()));
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
Here my short summary.
Add this to your Tools class:
public static void addOnEnter(Component c, Consumer<FocusEvent> onEnter) {
FocusListener fl = new FocusListener() {
#Override
public void focusGained(FocusEvent e) {
onEnter.accept(e);
}
#Override
public void focusLost(FocusEvent e) { }
};
c.addFocusListener(fl);
}
public static void scrollToFocus(FocusEvent e) {
((JComponent) e.getComponent().getParent()).scrollRectToVisible(
e.getComponent().getBounds());
}
and use it like this:
Tools.addOnEnter(component, Tools::scrollToFocus);
component can be JTextField, JButton, ...
One major issue in your code is:
focused.scrollRectToVisible(focused.getBounds());
You are calling scrollRectToVisible on the component itself! Presumably a typo.
Make your JScrollPane a final variable and call
scrollPane.getViewport().scrollRectToVisible(focused.getBounds());
Here jtextbox is the component you want to focus and jscrollpane is your scrollpane:
jScrollpane.getVerticalScrollBar().setValue(jtextbox.getLocation().x);

Categories

Resources