JFormDesigner / FlatLaf

FlatLaf - Swing Look and Feel (with Darcula/IntelliJ themes support)
https://www.formdev.com/flatlaf/
Apache License 2.0
3.42k stars 272 forks source link

ArrayIndexOutOfBoundsException in FlatTabbedPaneUI #875

Closed IanKrL closed 3 months ago

IanKrL commented 3 months ago

When a TabbedPane returns a selected index of -1, it causes a ArrayIndexOutOfBoundsException.

Linux: AlmaLinux 8.10 Gnome: Version 3.32.2

package stuffs;

import java.net.UnknownHostException;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SingleSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;

import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLightLaf;

public class SimpleLaf {
    private static final String TEXT = "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG";

    public static void main(String[] args) throws UnknownHostException {
        SwingUtilities.invokeLater(() -> {
            JFrame.setDefaultLookAndFeelDecorated(true);
            FlatLightLaf.setup();
            final JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setTitle("Some Title");
            frame.setSize(350, 100);
            final JPanel panel = new JPanel();

            JTabbedPane pane = new JTabbedPane();
            pane.putClientProperty(FlatClientProperties.TABBED_PANE_TAB_TYPE,
                FlatClientProperties.TABBED_PANE_TAB_TYPE_CARD);
            SingleSelectionModel model = new SingleSelectionModel() {

                @Override
                public void setSelectedIndex(int index) {}

                @Override
                public void removeChangeListener(ChangeListener listener) {}

                @Override
                public boolean isSelected() {
                    return false;
                }

                @Override
                public int getSelectedIndex() {
                    return -1;
                }

                @Override
                public void clearSelection() {}

                @Override
                public void addChangeListener(ChangeListener listener) {}
            };
            pane.setModel(model);
            pane.add(new JLabel(TEXT));
            panel.add(pane);
            frame.add(panel);
            frame.setVisible(true);
        });

    }
}

Just launch this and the window displays, but also throws this exception several times:

Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 1
    at java.desktop/javax.swing.plaf.basic.BasicTabbedPaneUI.getTabBounds(BasicTabbedPaneUI.java:1738)
    at com.formdev.flatlaf.ui.FlatTabbedPaneUI.getTabBounds(FlatTabbedPaneUI.java:1881)
    at java.desktop/javax.swing.plaf.basic.BasicTabbedPaneUI.getTabBounds(BasicTabbedPaneUI.java:1673)
    at com.formdev.flatlaf.ui.FlatTabbedPaneUI.paintContentBorder(FlatTabbedPaneUI.java:1690)
    at com.formdev.flatlaf.ui.FlatTabbedPaneUI.paint(FlatTabbedPaneUI.java:1170)
    at java.desktop/javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
    at com.formdev.flatlaf.ui.FlatTabbedPaneUI.update(FlatTabbedPaneUI.java:1154)
    at java.desktop/javax.swing.JComponent.paintComponent(JComponent.java:842)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1119)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:952)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:952)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:952)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128)
    at java.desktop/javax.swing.JLayeredPane.paint(JLayeredPane.java:586)
    at java.desktop/javax.swing.JComponent.paintChildren(JComponent.java:952)
    at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5318)
    at java.desktop/javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:246)
    at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1337)
    at java.desktop/javax.swing.JComponent.paint(JComponent.java:1105)
    at java.desktop/java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:39)
    at java.desktop/sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:75)
    at java.desktop/sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:112)
    at java.desktop/java.awt.Container.paint(Container.java:2005)
    at java.desktop/java.awt.Window.paint(Window.java:3959)
    at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:890)
    at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:862)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:862)
    at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:835)
    at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:784)
    at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1898)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

In FlatTabbedPaneUI.paint:

    int tabPlacement = tabPane.getTabPlacement();
    int selectedIndex = tabPane.getSelectedIndex();

    paintContentBorder( g, tabPlacement, selectedIndex );

Where getSelectedIndex() returns -1 this leads to the index exception, but from the SingleSelectionModel Javadoc:

  • @return the model's selection, or -1 if there is no selection

So this is a valid value for this method to return, but it is not properly handled by FlatTabbedPaneUI or BasicTabbedPaneUI.

remcopoelstra commented 3 months ago

I am curious about your use case, are you trying to create a TabbedPane that supports 'clearing' of the selection? (no tabs selected and empty content).

According to the documentation there will always be a selected tab: https://docs.oracle.com/javase/8/docs/api/javax/swing/JTabbedPane.html

If the tab count is greater than 0, then there will always be a selected index, which by default will be initialized to the first tab.

IanKrL commented 3 months ago

You may be right, I didn't catch that line. I'll double check my real-life case and get back to you.

DevCharly commented 3 months ago

fixed in latest 3.5.1-SNAPSHOT: https://github.com/JFormDesigner/FlatLaf#snapshots

IanKrL commented 3 months ago

I looked more closely at my real-life case and what we were doing was actually more like this:

package stuffs;

import java.net.UnknownHostException;

import javax.swing.DefaultSingleSelectionModel;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SingleSelectionModel;
import javax.swing.SwingUtilities;

import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.FlatLightLaf;

public class SimpleLaf {
    private static final String TEXT = "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG";

    public static void main(String[] args) throws UnknownHostException {
        SwingUtilities.invokeLater(() -> {
            JFrame.setDefaultLookAndFeelDecorated(true);
            FlatLightLaf.setup();
            final JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setTitle("Some Title");
            frame.setSize(350, 100);
            final JPanel panel = new JPanel();
            JTabbedPane pane = new JTabbedPane();
            pane.putClientProperty(FlatClientProperties.TABBED_PANE_TAB_TYPE,
                FlatClientProperties.TABBED_PANE_TAB_TYPE_CARD);
            SingleSelectionModel model = new DefaultSingleSelectionModel();
            pane.setModel(model);
            pane.add(new JLabel(TEXT));
            panel.add(pane);
            frame.add(panel);
            // ***** This call here *****
            pane.setSelectedIndex(-1);
            frame.setVisible(true);
        });

    }
}

That is, manually setting the selected index to -1. This value is set during a tab-collapse animation to indicate a changing state. Ideally I imagine I'd rewrite to not set the unexpected -1 while a tab is present, but it's pretty thorny to untangle the logic and probably not worth it at this point.

I did come up with a workaround to make flatlaf 3.3 happy (sorry I forgot to report the version in my original description) until I can upgrade to 3.5.1+. I overrode getSelectedIndex() in my subclass of JTabbedPane and just don't allow it to return a negative number when there are tabs. That allows my weird animation code to function, but looks kosher to the outside world.

Thanks for fixing, DevCharly.