1

I'm making a touch screen keyboard. I want the alphabetic keys to change text when I press shift (from lower case to upper case). Here's a snippet of my current implementation:

public void updatedButtons()
{
    switch( m_state )
    {
        case QWERTY:
            for( KeyboardButton button : m_keyboardButtons )
            {
                button.setText( button.getQwertyText().toLowerCase() );
            }
            break;
        case QWERTY_SHIFT:
            for( KeyboardButton button : m_keyboardButtons )
            {
                button.setText( button.getQwertyText().toUpperCase() );
            }
            break;
    }
}

Where KeyboardButton is a simple extension of JButton with the qwertyText String field.

The issue here is that the buttons update in a chaotic manner. I understand why this is happening; when I call setText(), it's calling repaint() for the individual component, and this is happening for each button. My question is, am I able to "batch repaint" these buttons, without undermining Swing's design (I'd prefer not override AbstractButton's setText() method). Thanks.

UPDATE

Turns out it is an issue with FormLayout. Below is simple code that illustrates the problem (note that you will need a JGoodies Form Jar, and might need to modify the breakpoint value).

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

public class JButtonTest
{
    public static List<JButton> buttons = new ArrayList<JButton>();
    public static boolean isCaps = false;
    public static JFrame frame;


    public static void main(String[] args)
    {
        frame = new JFrame();
        frame.setSize( 1000, 100 );

        JPanel panel = new JPanel();
        // Form with 11 83x83 pixel squares, with 5x83 pixel spaces.
        panel.setLayout( new FormLayout("83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, "
                + "83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 83px, 5px, 86px",
                                        "83px"));

        int i = 1;
        for (char c = 'a'; c < 'l'; ++c)
        {
            JButton button = new ComplexButton( Character.toString(c) );

            button.addActionListener( new ActionListener()
            {
                @Override
                public void actionPerformed( ActionEvent e )
                {
                    updateButtons();
                }
            });

            panel.add( button, new CellConstraints().xywh( (i*2)-1, 1, 1, 1, CellConstraints.FILL, CellConstraints.FILL) );
            buttons.add( button );
            ++i;
        }

        frame.setContentPane( panel );
        frame.setVisible( true );
    }

    // Enable the commented-out lines in this method to allow concurrent updating.
    public static void updateButtons()
    {
        for (JButton button : buttons)
        {
            if (!isCaps)
                button.setText( button.getText().toUpperCase() );
            else
                button.setText( button.getText().toLowerCase() );
            //RepaintManager.currentManager( button ).markCompletelyClean( button );
        }
        //frame.repaint();
        isCaps = !isCaps;
    }
    protected static class ComplexButton extends JButton
    {
        public ComplexButton( String string )
        {
            super(string);
        }

        @Override
        public void paint( Graphics g )
        {
            int breakpoint = 3000000;
            super.paint( g );
            // Simulate some complex operations.
            for (int i = 0; i < breakpoint; ++i)
            {
                g.setColor( new Color( i%255, (2*i)%255, (3*i)%255 ));
            }
        }
    }
}

Note that, if you change from FormLayout to FlowLayout, it works perfectly fine (though sluggish in nature). It also works fine if you remove the comments on the commented-out code (thanks MadProgrammer).

Also note that, if you put printlns at the beginning and end of the updateButtons() method, the method will end long before the buttons stop updating, and the buttons will not update in unison. This means the coalesced nature of repaint is somehow not preserved with FormLayout.

Regardless, even if it was preserved, sluggish controls are just as bad as chaotically-updating controls. Guess I'll have to attempt to optimize our paint code. Thanks for the support.

Ironcache
  • 1,719
  • 21
  • 33
  • I would say that you are doing something wrong, but since you've neglected to provide a [runnable example](https://stackoverflow.com/help/mcve) which demonstrates your problem, anything we would suggest would be wild guess at best... – MadProgrammer Dec 04 '14 at 20:54
  • I'm not asking you to fix my code. I'm saying that, regardless of the implementation details, this is what Swing is doing (calling repaint() on every setText() call), and I want to prevent it from doing it. My question is if there's a clean way to prevent swing from acting upon these repaint calls. If you insist on having a demo in order to see that, I'll throw something together shortly. – Ironcache Dec 04 '14 at 21:03
  • 1
    @Ironcache actually, repaint() is a "delayed/coalesced" method, so it means that the actual painting will only happened after your loop is finished and all the calls to repaint triggered by your loop will be merged into a single repaint. So the problem is hardly caused by your explanation. Please provide a [mcve](https://stackoverflow.com/help/mcve) – Guillaume Polet Dec 04 '14 at 21:08
  • @GuillaumePolet Yeah, you're right. I knew about the coalesced effect, but I guess I kind of wanted to put shudders over my eyes and blame Java for my problems. I made a quick example here at home to try to illustrate my issue, but it's working for me here. When I get back to the office I'll try running my example there and see if it's a problem with our environment (which is extremely old), or if the problem is elsewhere. – Ironcache Dec 04 '14 at 22:26
  • I'm actually going back on my last statement and calling this one a draw. While it wasn't an issue with Swing's default components (or Swing in general), the coalesced nature of repaint was not preserved in the layout manager I'm using (FormLayout, which seems to be a standard). Neither of us were really wrong. MCVE attached. – Ironcache Dec 05 '14 at 16:37
  • Your ComplexButton is breaking the paint chain – MadProgrammer Dec 05 '14 at 20:48
  • Can you elaborate? What about ComplexButton is breaking the paint chain. More over, why does the paint chain only break with one layout manager (FormLayout) and not the other (FlowLayout). I should also state that, while the code in ComplexButton's paint() method is obviously artificial in nature, the behavior can be reproduced with realistic logic (IE: drawing a solid-colored buffered image over the button). Thanks for the support. – Ironcache Dec 06 '14 at 01:08

1 Answers1

3

Painting in Swing is controlled by the RepaintManager which decides when things should get updated. The RepaintManager is optimised to reduce the number paint events it schedules in order to maintain performance.

This means that when it receives a bunch of requests for repaints, it will consolidate them down into as few paint events as it can. This means that when you call setText from within a loop on a bunch of buttons, it's likely that the RepaintManager has reduced this down to as close to one as it can (depending on what you are updating there might be more, but will most likely be less than the number of iterations of the loop)...

For example, it seems to work for me...

Keys

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestKeyboard {

    public static void main(String[] args) {
        new TestKeyboard();
    }

    public TestKeyboard() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private List<JButton> buttons;

        public TestPane() {
            setFocusable(true);
            setLayout(new GridLayout(0, 4));
            buttons = new ArrayList<>(26);
            for (int index = 0; index < 26; index++) {
                JButton btn = createButton(index);
                buttons.add(btn);
                add(btn);
            }

            addKeyListener(new KeyAdapter() {

                @Override
                public void keyPressed(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
                        for (JButton btn : buttons) {
                            String text = btn.getText().toUpperCase();
                            btn.setText(text);
                        }
                        revalidate();
                        repaint();
                    }
                }

                @Override
                public void keyReleased(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
                        for (JButton btn : buttons) {
                            String text = btn.getText().toLowerCase();
                            btn.setText(text);
                        }
                        revalidate();
                        repaint();
                    }
                }               
            });
        }

        protected JButton createButton(int index) {
            JButton btn = new JButton(Character.toString((char) ('a' + index)));
            btn.setMargin(new Insets(4, 4, 4, 4));
            btn.setFocusable(false);
            btn.setFocusPainted(false);
            return btn;
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thank you, your answer helped me arrive to the solution for my problem. I didn't know about the repaint manager. Marking this best answer, and my own answer will follow. – Ironcache Dec 05 '14 at 16:20
  • Updated question instead with an example of my resulting code. – Ironcache Dec 05 '14 at 16:33