serg.nechaev serg.nechaev - 3 months ago 6
Java Question

How to make an element on a painted JPanel focusable

I have a

JPanel
with an overriden
paintComponent
. I want to make certain elements that I manually draw on this panel focusable so that people using assistive technologies could use my application using keyboard.

If you could give me a few pointers that would be awesome.

Answer

You can do the following:

  1. Convert your elements into JComponents.
  2. Set the LayoutManager of your panel to null. You will then add all your components/elements into this panel and you can move them freelly around with the method Component.setBounds(...).
  3. Add a MouseListener in your panel which will transfer the focus to the selected component, for each mouse press.
  4. You can determine which component was pressed by calling the method Component.getComponentAt(Point) inside the MouseListener of your panel.

Simple example:

  1. Make a component with the standard behaviour of showing the user if it has focus or not. In my example-code below this class is FocusableComponent extends JComponent, which draws a blue rectangle around the component if it has focus (this is done inside the method FocusableComponent.paintComponent(Graphics)).
  2. Then, for each distinct "element" you draw, subclass FocusableComponent and override its paintComponent(Graphics) method to paint the element. Make sure you call "super.paintComponent(Graphics)" inside there for the blue rectangle to be drawn (if it has focus).

Code:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class FocusablePaintComps {
    private static abstract class FocusableComponent extends JComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            if (hasFocus()) {
                final Color prevColor = g.getColor();
                g.setColor(Color.BLUE);
                g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
                g.setColor(prevColor);
            }
        }
    }

    private static class FocusableComponent1 extends FocusableComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            g.fillOval(0, 0, getWidth() - 1, getHeight() - 1);
        }
    }

    private static class FocusableComponent2 extends FocusableComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            final int w = getWidth(), h = getHeight();
            g.fillRect(20, 20, w - 40, h - 40);
            g.fillArc(10, 10, w - 1, h - 1, 60, 150);
        }
    }

    private static class YourPanel extends JPanel {
        private Component previousFocusedComponent = null;

        private YourPanel() {
            super(null); //Null LayoutManager. This is important to be able to
            //move added components around freelly (with the method setBounds(...)).

            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(final MouseEvent evt) {
                    final Component src = getComponentAt(evt.getPoint());
                    if (src instanceof FocusableComponent) {
                        final FocusableComponent fc = (FocusableComponent) src;
                        fc.requestFocusInWindow(); //Transfer focus to the pressed component.
                        if (previousFocusedComponent != null)
                            previousFocusedComponent.repaint(); //Repaint the last (without focus now).
                        setComponentZOrder(fc, 0); //Update: To make fc paint over all others as  
                        //the user http://stackoverflow.com/users/131872/camickr commented.  
                        fc.repaint(); //Repaint the new (with focus now).
                        previousFocusedComponent = fc;
                    }
                    else { //If clicked on empty space, or a non-FocusableComponent:
                        requestFocusInWindow(); //Tranfer focus to somewhere else (e.g. the panel itself).
                        if (previousFocusedComponent != null) {
                            previousFocusedComponent.repaint(); //Repaint the last (without focus now).
                            previousFocusedComponent = null;
                        }
                    }
                }
            });

            setPreferredSize(new Dimension(250, 250));

            add(new FocusableComponent1(), Color.RED, new Rectangle(10, 10, 200, 20));
            add(new FocusableComponent1(), Color.GREEN, new Rectangle(40, 150, 50, 70));
            add(new FocusableComponent2(), Color.GRAY, new Rectangle(60, 125, 90, 100));
            add(new FocusableComponent2(), Color.MAGENTA, new Rectangle(150, 60, 80, 150));
        }

        private void add(final FocusableComponent fc, final Color fgColor, final Rectangle bounds) {
            fc.setForeground(fgColor);
            add(fc);
            fc.setBounds(bounds);
        }
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("Focused Paint Comps");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new YourPanel());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Screenshot:

Screenshot

Some notes:

  1. The sequence in which the focus is transfered in relation to the sequence in which the rapaint()s are called inside mousePressed(...) determines which component will have the blue rectanlge around it, and which not.
  2. Method Component.getElementAt(Point) does not "see through" transparent/non-opaque pixels.

Update:

Follows an update of the above code, which uses a custom LayoutManager to lay-out the components in the container, as suggested by user "Andrew Thompson" in comments.
The only difference from the above code is that instead of setting to null the LayoutManager upon construction of the YourPanel, a new instance of the custom LayoutManager is used. I have named the custom LayoutManager to RandomLayout and it places all the components of the container in random positions, taking into account the size of the components and the Insets of the container (this is demonstrated by the added Border in the YourPanel).

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class FocusablePaintComps {
    private static abstract class FocusableComponent extends JComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            if (hasFocus()) {
                final Color prevColor = g.getColor();
                g.setColor(Color.BLUE);
                g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
                g.setColor(prevColor);
            }
        }
    }

    private static class FocusableComponent1 extends FocusableComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            g.fillOval(0, 0, getWidth() - 1, getHeight() - 1);
        }
    }

    private static class FocusableComponent2 extends FocusableComponent {
        @Override protected void paintComponent(final Graphics g) {
            super.paintComponent(g);
            final int w = getWidth(), h = getHeight();
            g.fillRect(20, 20, w - 40, h - 40);
            g.fillArc(10, 10, w - 1, h - 1, 60, 150);
        }
    }

    private static class YourPanel extends JPanel {
        private Component previousFocusedComponent = null;

        private YourPanel() {
            super(new RandomLayout()); //RandomLayout: custom LayoutManager which lays
            //out the components in random positions (takes Insets into account).

            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(final MouseEvent evt) {
                    final Component src = getComponentAt(evt.getPoint());
                    if (src instanceof FocusableComponent) {
                        final FocusableComponent fc = (FocusableComponent) src;
                        fc.requestFocusInWindow(); //Transfer focus to the pressed component.
                        if (previousFocusedComponent != null)
                            previousFocusedComponent.repaint(); //Repaint the last (without focus now).
                        setComponentZOrder(fc, 0); //Update: To make fc paint over all others as  
                        //the user http://stackoverflow.com/users/131872/camickr commented.  
                        fc.repaint(); //Repaint the new (with focus now).
                        previousFocusedComponent = fc;
                    }
                    else { //If clicked on empty space, or a non-FocusableComponent:
                        requestFocusInWindow(); //Tranfer focus to somewhere else (e.g. the panel itself).
                        if (previousFocusedComponent != null) {
                            previousFocusedComponent.repaint(); //Repaint the last (without focus now).
                            previousFocusedComponent = null;
                        }
                    }
                }
            });

            setBorder(new LineBorder(Color.LIGHT_GRAY, 20));
            setPreferredSize(new Dimension(300, 250));

            add(new FocusableComponent1(), Color.RED, new Dimension(200, 20));
            add(new FocusableComponent1(), Color.GREEN, new Dimension(50, 70));
            add(new FocusableComponent2(), Color.GRAY, new Dimension(90, 100));
            add(new FocusableComponent2(), Color.MAGENTA, new Dimension(80, 150));
        }

        private void add(final FocusableComponent fc, final Color fgColor, final Dimension size) {
            add(fc);
            fc.setForeground(fgColor);
            fc.setSize(size);
        }
    }

    public static void main(final String[] args) {
        final JFrame frame = new JFrame("Focused Paint Comps");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new YourPanel());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

RandomLayout:

And the custom LayoutManager itself with JavaDoc (may be big, but reusable hopefully):

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.util.Random;

/**
 * A {@link java.awt.LayoutManager} which lays out randomly all the {@link java.awt.Component}s
 * of its parent, taking into consideration the parent's {@link java.awt.Insets}.
 * <p>
 * Use {@link #setRandomizeOnce(boolean)} method to determine if the lastly laid-out parent will
 * be only laid-out randomly once and not for each {@link #layoutContainer(java.awt.Container)}
 * subsequent call for the same parent, or the opposite.
 * </p>
 */
public class RandomLayout implements LayoutManager {
    /**
     * The {@link java.awt.Container} which was lastly laid-out.
     */
    private Container lastParent;

    /**
     * The {@link java.awt.Insets} of {@code lastParent} the last time it was laid-out.
     */
    private Insets lastInsets;

    /**
     * If {@code true} then this {@link java.awt.LayoutManager} keeps track of the
     * {@link java.awt.Container}s laid-out to make sure that {@code lastParent} is
     * only laid-out once. If the another {@link java.awt.Container} is laid-out, other
     * than {@code lastParent}, then its components are laid-out randomly and the
     * {@link java.awt.Container} becomes the {@code lastParent}.
     */
    private boolean randomizeOnce;

    /**
     * Normal constructor of {@code RandomLayout} with explicit value for {@code randomizeOnce}.
     * 
     * @param randomizeOnce {@code true} if the lastly laid-out parent will be only laid-out
     * randomly once and not for each {@link #layoutContainer(java.awt.Container)} subsequent call
     * for the same parent, otherwise {@code false} and each call to
     * {@link #layoutContainer(java.awt.Container)} will lay out randomly the {@link java.awt.Container}.
     */
    public RandomLayout(final boolean randomizeOnce) {
        this.randomizeOnce = randomizeOnce;
    }

    /**
     * Default constructor of {@code RandomLayout} with {@code randomizeOnce} set to {@code true}.
     */
    public RandomLayout() {
        this(true);
    }

    /**
     * If {@code true} then this {@link java.awt.LayoutManager} keeps track of the
     * {@link java.awt.Container}s laid-out to make sure that {@code lastParent} is
     * only laid-out once. If the another {@link java.awt.Container} is laid-out, other
     * than {@code lastParent}, then its components are laid-out randomly and the
     * {@link java.awt.Container} becomes the {@code lastParent}.
     * 
     * @param randomizeOnce {@code true} if the lastly laid-out parent will be only laid-out
     * randomly once and not for each {@link #layoutContainer(java.awt.Container)} subsequent call
     * for the same parent, otherwise {@code false}.
     */
    public void setRandomizeOnce(final boolean randomizeOnce) {
        this.randomizeOnce = randomizeOnce;
    }

    /**
     * Tells if the lastly laid-out parent will be only laid-out randomly once and not for each
     * {@link #layoutContainer(java.awt.Container)} subsequent call for the same parent, or the
     * opposite.
     * 
     * @return {@code true} if the lastly laid-out parent will be only laid-out randomly once and
     * not for each {@link #layoutContainer(java.awt.Container)} subsequent call for the same
     * parent, otherwise {@code false}.
     */
    public boolean isRandomizeOnce() {
        return randomizeOnce;
    }

    /**
     * @return The {@link java.awt.Container} which was lastly laid-out.
     */
    protected Container getLastParent() {
        return lastParent;
    }

    /**
     * @return The {@link java.awt.Insets} of {@code lastParent} the last time it was laid-out.
     * @see #getLastParent()
     */
    protected Insets getLastInsets() {
        return lastInsets;
    }

    /**
     * Adds the specified component with the specified name to the layout.
     * @param name The name of the component.
     * @param comp The {@link java.awt.Component} to be added.
     */
    public void addLayoutComponent(final String name,
                                   final Component comp) {
    }

    /**
     * Removes the specified component from the layout.
     * @param comp The {@link java.awt.Component} to be removed.
     */
    public void removeLayoutComponent(final Component comp) {
    }

    /** 
     * {@inheritDoc}
     * @return The preferred size dimensions for the specified {@link java.awt.Container}.
     */
    @Override
    public Dimension preferredLayoutSize(final Container parent) {
        final Dimension prefDim = minimumLayoutSize(parent);
        prefDim.width += 2; //+2 to spare.
        prefDim.height += 2; //+2 to spare.
        return prefDim;
    }

    /**
     * {@inheritDoc}
     * @return The minimum size dimensions for the specified {@link java.awt.Container}.
     */
    @Override
    public Dimension minimumLayoutSize(final Container parent) {
        final Dimension minDim = new Dimension();

        final int childCnt = parent.getComponentCount();
        for (int i=0; i<childCnt; ++i)
            applyBigger(minDim, getPreferredSize(parent, parent.getComponent(i)));

        final Insets parInsets = parent.getInsets();
        minDim.width += (parInsets.left + parInsets.right);
        minDim.height += (parInsets.top + parInsets.bottom);

        return minDim;
    }

    /**
     * {@inheritDoc}. If the another {@link java.awt.Container} is laid-out, other
     * than {@code lastParent}, then its components are laid-out randomly and the
     * {@link java.awt.Container} becomes the {@code lastParent}.
     */
    @Override
    public void layoutContainer(final Container parent) {
        if (parent == null)
            throw new IllegalArgumentException("Cannot lay out null.");
        if (isRandomizeOnce() && lastParent == parent) { //At least take care of insets (if they have changed).
            final Insets parentInsets = parent.getInsets();
            if (!lastInsets.equals(parentInsets)) {
                final int offx = parentInsets.left - lastInsets.left,
                          offy = parentInsets.top - lastInsets.top;

                final int childCnt = parent.getComponentCount();
                for (int i=0; i<childCnt; ++i) {
                    final Component child = parent.getComponent(i);
                    final Point childLoca = child.getLocation();
                    childLoca.x += offx;
                    childLoca.y += offy;
                    child.setLocation(childLoca);
                }

                lastInsets = parentInsets;
            }
        }
        else
            layoutContainerRandomly(parent);
    }

    /**
     * Explicitly lays out randomly the specified container.
     * <p>
     * This is equivalent of calling:
     * <pre>
     * boolean isRand1 = randomLayout.isRandomizeOnce();
     * randomLayout.setRandomizeOnce(false);
     * randomLayout.layoutContainer(parent);
     * randomLayout.setRandomizeOnce(isRand1);
     * </pre>
     * {@code parent} becomes {@code lastParent}.
     * </p>
     * @param parent The container to be laid out.
     */
    public void layoutContainerRandomly(final Container parent) { //Place each child at a random location for the "new" parent (lastParent != parent).
        if (parent == null)
            throw new IllegalArgumentException("Cannot lay out null.");

        reset();

        final Dimension parentSize = parent.getSize();
        final Insets parentInsets = parent.getInsets();
        final Dimension childSize = new Dimension();
        final Point childLoca = new Point();
        final Random rand = new Random();

        final int childCnt = parent.getComponentCount();
        for (int i=0; i<childCnt; ++i) {
            final Component child = parent.getComponent(i);

            child.getSize(childSize);

            childLoca.x = parentInsets.left + 1;
            childLoca.y = parentInsets.top + 1;

            final int xBound = parentSize.width - parentInsets.left - parentInsets.right - childSize.width,
                      yBound = parentSize.height - parentInsets.top - parentInsets.bottom - childSize.height;

            if (xBound > 0)
                childLoca.x += rand.nextInt(xBound);
            if (yBound > 0)
                childLoca.y += rand.nextInt(yBound);

            child.setLocation(childLoca);
        }

        lastParent = parent;
        lastInsets = parentInsets;
    }

    /**
     * Invalidates the tracking of the lastly laid-out {@link java.awt.Container} and its last
     * {@link java.awt.Insets}.
     * @see #getLastParent()
     * @see #getLastInsets()
     */
    protected void reset() {
        lastParent = null;
        lastInsets = null;
    }

    private static void applyBigger(final Dimension inputOutput,
                                    final Dimension input) {
        if (inputOutput != null && input != null) {
            inputOutput.width = (int) Math.max(inputOutput.width, input.width);
            inputOutput.height = (int) Math.max(inputOutput.height, input.height);
        }
    }

    private static void applyIfBetter(final Dimension inputOutput,
                                      final Dimension input) {
        if (inputOutput != null && input != null
            && (input.width > inputOutput.width
                || input.height > inputOutput.height)) {
            inputOutput.width = input.width;
            inputOutput.height = input.height;
        }
    }

    /**
     * Tries to determine the best size for {@code child}.
     * @param parnt The parent {@link java.awt.Container} being laid-out.
     * @param child The child {@link java.awt.Component} of {@code parnt} being laid-out.
     * @return A preferred size for the {@code child} to be laid-out.
     */
    protected static Dimension getPreferredSize(final Container parnt,
                                                final Component child) {
        final Dimension minDim = new Dimension();
        if (child != null) {
            applyIfBetter(minDim, child.getMinimumSize());
            applyIfBetter(minDim, child.getSize());
            applyIfBetter(minDim, child.getPreferredSize());
        }
        return minDim;
    }
}

Updated screenshot:

And here is the new screenshot (no big visual differences):

Updated screenshot

Note for the update:

Be aware that this is my first custom LayoutManager, but I have read the documentation, and also GridLayout and SpringLayout as examples (because, in my oppinion, LayoutManager's documentation is not enough) and of course I tested it. As it is wright now, I cannot find any problem with it. Any suggestions or proposals for improvements will be appreciated of course.