user2582318 user2582318 - 2 months ago 11
Java Question

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

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:

enter image description here

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.

Answer

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:

top_tabbed_pane

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:

left_tabbed_pane

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

enter image description here

NimbusLookAndFeel

enter image description here

SeaglassLookAndFeel

enter image description here

WindowsLookAndFeel

enter image description here