TT.
TT.

Reputation: 16146

JComboBox with tooltip if display area too small, while maintaining look and feel

I am creating a combobox that shows a tooltip when the display area is too small to show all text of the selected item.

The problem I have with the solution I have so far:

The little MCVE I included at the bottom shows what I came up with. It shows four comboboxes. The first and third are regular untweaked JComboBox instances, the second and fourth are tweaked comboboxes to show tooltips if the display area is too small for the text. The first and second are enabled, the third and fourth disabled; this to better show the difference in look and feel. A snapshot:

enter image description here

How can I maintain the look and feel of a regular combobox and have a tooltip when the display area becomes too small? Am I on the right path and if so, what do I need to do further? If I am not, what should I do instead?


import java.awt.Dimension;
import java.awt.EventQueue;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class ComboWithTooltip {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName( ));
        } 
        catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater (
            new Runnable() {
                @Override
                public void run() {
                    buildFrame().setVisible(true);
                }
            }
        );
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items = new String[]{ "Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt", "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss" };

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100,25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer (
                new DefaultListCellRenderer() {
                    @Override
                    public void setBounds(int x, int y, int width, int height) {
                        super.setBounds( x, y, width, height );

                        if( width != 0 ) {
                            if( getPreferredSize( ).width > getSize( ).width )
                                JComboBoxTweaked.this.setToolTipText( getText( ) );
                            else
                                JComboBoxTweaked.this.setToolTipText( null );
                        }
                    }
                }
            );
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100,25); // intentionally too small
        }
    };

    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }
}

Upvotes: 2

Views: 498

Answers (2)

aterai
aterai

Reputation: 9833

Another way is to use ListCellRenderer which can be obtained by JComboBox#getRenderer() method instead of new DefaultListCellRenderer() {...}.

ListCellRenderer<? super String> renderer = getRenderer();
setRenderer(new ListCellRenderer<String>() {
  @Override public Component getListCellRendererComponent(
      JList<? extends String> list, String value, int index,
      boolean isSelected, boolean cellHasFocus) {
    Component c = renderer.getListCellRendererComponent(
      list, value, index, isSelected, cellHasFocus);

In the case of WindowsLookAndFeel, it will be WindowsComboBoxRenderer:

// @see com/sun/java/swing/plaf/windows/WindowsComboBoxUI.java
/**
 * Subclassed to set opacity {@code false} on the renderer
 * and to show border for focused cells.
 */
@SuppressWarnings("serial") // Superclass is not serializable across versions
private static class WindowsComboBoxRenderer
      extends BasicComboBoxRenderer.UIResource {

ComboWithTooltip2.java

import java.awt.*;
import java.util.Objects;
import javax.swing.*;

public class ComboWithTooltip2 {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName( ));
        } 
        catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater (
            new Runnable() {
                @Override
                public void run() {
                    buildFrame().setVisible(true);
                }
            }
        );
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/*enabled*/));
        contentPane.add(Box.createVerticalStrut(25));

        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo2(true));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo2(false));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(Box.createVerticalGlue());
        contentPane.setPreferredSize(new Dimension(100, 320));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items = new String[]{ "Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt", "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss" };

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100,25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer (
                new DefaultListCellRenderer() {
                    @Override
                    public void setBounds(int x, int y, int width, int height) {
                        super.setBounds( x, y, width, height );

                        if( width != 0 ) {
                            if( getPreferredSize( ).width > getSize( ).width )
                                JComboBoxTweaked.this.setToolTipText( getText( ) );
                            else
                                JComboBoxTweaked.this.setToolTipText( null );
                        }
                    }
                }
            );
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100,25); // intentionally too small
        }
    };

    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }

  private static JComboBox<String> createDefaultCombo2(boolean enabled) {
    JComboBox<String> comboBox = new JComboBox<String>(items) {
      @Override public void updateUI() {
        setRenderer(null);
        super.updateUI();
        ListCellRenderer<? super String> renderer = getRenderer();
        setRenderer(new ListCellRenderer<String>() {
          @Override public Component getListCellRendererComponent(JList<? extends String> list, String value, int index, boolean isSelected, boolean cellHasFocus) {
            Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

            Insets i1 = ((JComponent) c).getInsets();
            Insets i2 = getInsets();
            int availableWidth = getWidth() - i1.top - i1.bottom - i2.top - i2.bottom;
            if (index < 0) {
              int buttonSize = getHeight() - i2.top - i2.bottom;
              availableWidth -= buttonSize;
              JTextField tf = (JTextField) getEditor().getEditorComponent();
              Insets i3 = tf.getMargin();
              availableWidth -= i3.left + i3.right;
            }

            String str = Objects.toString(value, "");
            FontMetrics fm = c.getFontMetrics(getFont());
            setToolTipText(fm.stringWidth(str) > availableWidth ? str : null);
            return c;
          }
        });
      }
    };
    comboBox.setEnabled(enabled);
    return comboBox;
  }
}

Upvotes: 1

Sergiy Medvynskyy
Sergiy Medvynskyy

Reputation: 11327

Solution here: provide delegation to the standard L&F renderer. Here is the code:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.util.Objects;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class ComboWithTooltip {
    public static void main(String[] args) {
        try {
            // On my system: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            System.err.println("Can't set look and feel");
            return;
        }
        EventQueue.invokeLater(
                new Runnable() {
                    @Override
                    public void run() {
                        buildFrame().setVisible(true);
                    }
                });
    }

    static JFrame buildFrame() {
        JPanel contentPane = new JPanel();
        BoxLayout layout = new BoxLayout(contentPane, BoxLayout.Y_AXIS);
        contentPane.setLayout(layout);
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(true/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(true/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createDefaultCombo(false/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));
        contentPane.add(createTweakedCombo(false/* enabled */));
        contentPane.add(Box.createVerticalStrut(25));

        JFrame frame = new JFrame();
        frame.setContentPane(contentPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        return frame;
    }

    private final static String[] items =
            new String[] {"Long text 1234444444444444444444444", "Some more of that long texttttttttttttttttttttt",
                    "The longer the text, the harder to read it becomessssssssssssssssssssssssssssssssssssssss"};

    @SuppressWarnings("serial")
    private static JComboBox<String> createDefaultCombo(boolean enabled) {
        JComboBox<String> comboBoxDefault = new JComboBox<String>(items) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(100, 25); // intentionally too small
            }
        };
        comboBoxDefault.setEnabled(enabled);
        return comboBoxDefault;
    }

    @SuppressWarnings("serial")
    private static class JComboBoxTweaked extends JComboBox<String> {
        public JComboBoxTweaked(String[] items) {
            super(items);

            setRenderer(new DelegateComboBoxRenderer<>(getRenderer(), this));
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 25); // intentionally too small
        }
    };

    private static class DelegateComboBoxRenderer<E> extends JPanel implements ListCellRenderer<E> {

        private final ListCellRenderer<E> delegate;

        private final JComboBox<?> combo;
        private String lastText;

        /**
         * @param delegate
         */
        public DelegateComboBoxRenderer(ListCellRenderer<E> delegate, JComboBox<?> combo) {
            setLayout(new GridLayout(1, 1));
            setOpaque(true);
            this.delegate = delegate;
            this.combo = combo;

        }

        @Override
        public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected,
                boolean cellHasFocus) {
            removeAll();
            Component c = delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            add(c);
            if (c instanceof JLabel) {
                lastText = ((JLabel) c).getText();
            } else {
                lastText = Objects.toString(value, null);
            }
            setOpaque(c.isOpaque()); // some renderers works with opacity state, we need to consider it
            return this;
        }

        // some UI classes reset background/foreground, so we need to forward them
        @Override
        public void setBackground(Color bg) {
            super.setBackground(bg);
            for (Component c : getComponents()) {
                c.setBackground(bg);
            }
        }

        @Override
        public void setForeground(Color fg) {
            super.setForeground(fg);
            for (Component c : getComponents()) {
                c.setForeground(fg);
            }
        }

        // not sure whether it's required
        @Override
        public void setFont(Font font) {
            super.setFont(font);
            for (Component c : getComponents()) {
                c.setFont(font);
            }
        }

        @Override
        public void setBounds(int x, int y, int width, int height) {
            super.setBounds(x, y, width, height);

            if (width != 0) {
                if (getPreferredSize().width > getSize().width) {
                    combo.setToolTipText(lastText);
                } else {
                    combo.setToolTipText(null);
                }
            }
        }

    }
    private static JComboBox<String> createTweakedCombo(boolean enabled) {
        JComboBox<String> comboBoxTweaked = new JComboBoxTweaked(items);
        comboBoxTweaked.setEnabled(enabled);
        return comboBoxTweaked;
    }
}

Upvotes: 2

Related Questions