2

This is my second post related to Java Swing, so pardon me if my question is too straightforward. I'm trying to get multiple JPanels to communicate with each other. I'm building a simple 2D grid that I can add walls/blocked cells to, and then run a simple Floodfill/A* Search algorithm on (given start and goal locations).

To isolate my issue, I decided that working with an example would be easier. So I created a simple application, that allows the user to write to a text box, provided that he/she has clicked on a "Start" button. Once writing to the text box is done, the user can click on the "Start" button to flip it to the "Stop" state. In the "Stop" state, the user cannot add any text to the text box (i.e. the application should not register any keystrokes at all). This is a simple problem that really brings out my question here. Here's how the UI looks like right now:

enter image description here

enter image description here

My question: I should be able to write when the button shows "Stop" (since it is in edit mode) and I shouldn't be able to write in the text area when the button shows "Start" (since it is not it edit mode). However, from the above images, you can see that I'm able to write in the text area in any case. How do I then make the editing of the text area dependent on the button state?

Here's my code which attempts at setting up that connection between the button panel and the text panel, but it somehow isn't working as expected.

I looked at StackOverflow posts here and here, but frankly, the answers didn't seem clear to me.

SimpleTextPanel:

public class SimpleTextPanel extends JPanel implements PropertyChangeListener, ChangeListener {


    private boolean canWrite;
    public SimpleTextPanel() {

        // set the border properties
        canWrite = true;
        TitledBorder title = BorderFactory.createTitledBorder("Simple Text Panel");
        title.setTitleColor(Color.BLACK);
        title.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED,
                Color.DARK_GRAY, Color.GRAY));
        this.setBorder(title);

        JTextArea editSpace = new JTextArea(10, 20);
        editSpace.setEditable(true);
        editSpace.addPropertyChangeListener(this);
        this.add(editSpace);
    }

    @Override
    public void stateChanged(ChangeEvent changeEvent) {
        JButton button = (JButton)changeEvent.getSource();
        canWrite = button.getText().equals("Start");
    }

    @Override
    public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
        JTextArea area = (JTextArea)propertyChangeEvent.getSource();
        if(!canWrite) area.setText((String)propertyChangeEvent.getOldValue());
    }
}

SimpleButtonPanel:

public class SimpleButtonPanel extends JPanel implements ActionListener {

    JButton switchButton;
    private boolean canWrite = true;

    public SimpleButtonPanel(SimpleTextPanel txt) {

        // set the border properties
        TitledBorder title = BorderFactory.createTitledBorder("Simple Button Panel");
        title.setTitleColor(Color.BLACK);
        title.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED,
                Color.DARK_GRAY, Color.GRAY));
        this.setBorder(title);

        switchButton = new JButton("Start");
        switchButton.addActionListener(this);
        switchButton.addChangeListener(txt);
        this.add(switchButton);
    }

    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        if(switchButton.getText().equals("Start")) {
            switchButton.setText("Stop");
            canWrite = false;
        } else if(switchButton.getText().equals("Stop")) {
            switchButton.setText("Start");
            canWrite = true;
        }
    }
}

SimpleExampleTest:

public class SimpleExampleTest extends JFrame {

    public SimpleExampleTest() {

        setLayout(new BorderLayout());
        setTitle("Simple Example");
        setSize(300, 300);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        SimpleTextPanel text = new SimpleTextPanel();
        SimpleButtonPanel button = new SimpleButtonPanel(text);

        add(text, BorderLayout.NORTH);
        add(button, BorderLayout.SOUTH);
        //button.addPropertyChangeListener(text); // so that text editor knows whether to allow writing or not
    }


    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                SimpleExampleTest ex = new SimpleExampleTest();
                ex.setVisible(true);
            }
        });
    }
}

Any/all help is appreciated. Thank you!

Community
  • 1
  • 1
Nishant Kelkar
  • 412
  • 1
  • 4
  • 20
  • You could use a model of some sort to maintain stateful information between the panels or provide a listener interface of some kind that could alert the editor pane of changes to the button state... – MadProgrammer Sep 01 '14 at 06:42
  • you can give your SimpleButtonPanel a variable referring to the containing class: SimpleExampleTest. Then, within the actionPerformed method, you can use that variable to call a method in it, and from there, perform an action in the SimpleTextPanel – Stultuske Sep 01 '14 at 06:43
  • `public class SimpleTextPanel extends JPanel..` IMO this is where the problem starts. Neither panel should extend panel, but instead just be plain panel objects. The controls which must affect one another might be declared in the same class as whatever produces the panels, making this a non-question. – Andrew Thompson Sep 01 '14 at 06:52
  • @MadProgrammer: Thanks for the quick answer! This is interesting. Regarding your suggestion (1), you're saying that I could have a class (say MegaPanel) with it's member variables as SimpleTextPanel and SimpleButtonPanel. And then, pass those to my JFrame. Both SimpleTextPanel and SimpleButtonPanel register MegaPanel as their listeners so that way, all panels communicate via MegaPanel. Do I understand correctly? – Nishant Kelkar Sep 01 '14 at 06:53
  • i would suggest you go through delegation design pattern. You will get to know about how can two panels communicate. CHEERS ! – ppuskar Sep 01 '14 at 06:58
  • @AndrewThompson You're basically suggesting to pack both SimpleTextPanel and SimpleButtonPanel as simple JTextArea and JButton objects into a class (say PanelBuilder), and then have PanelBuilder build both panels, with both the objects registering PanelBuilder as their listener. Somewhat similar to MadProgrammer's suggestion (1), but not quite. Correct? – Nishant Kelkar Sep 01 '14 at 07:00
  • Not really...The model should have no concept of anybody or carry any information that it is not directly responsible for. Both the editor and button panels would recieve an instance of the (same) model, allowing the editor panel to respond to changes in it's state and the button panel to change it's state. Have a look at the example in the answer I left below.... – MadProgrammer Sep 01 '14 at 07:10

2 Answers2

4

One approach would be to devise a model that could shared by the panels.

The text panel only wants to know what the current state is and when that state changes. The button panel wants to know when the state changes and also wants to be able to change the state.

This decouples the two panels from each other, as they don't really care how the state is changed or who by, only that they can respond to the changes accordingly...

Also, the model doesn't care about either of them, it just carries the state and provides notification when it changes.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.BevelBorder;
import javax.swing.border.TitledBorder;

public class TalkToEachOther {

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

    public TalkToEachOther() {
        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("Simple Example");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                MutableSimpleModel model = new DefaultSimpleModel();

                SimpleTextPanel text = new SimpleTextPanel(model);
                SimpleButtonPanel button = new SimpleButtonPanel(model);

                frame.add(text, BorderLayout.NORTH);
                frame.add(button, BorderLayout.SOUTH);
                //button.addPropertyChangeListener(text); // so that text editor knows whether to allow writing or not
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface SimpleModel {

        public boolean isEditable();

        public void addPropertyChangeListener(PropertyChangeListener listener);
        public void removePropertyChangeListener(PropertyChangeListener listener);

    }

    public interface MutableSimpleModel extends SimpleModel {

        public void setEditable(boolean editable);

    }

    public class DefaultSimpleModel implements MutableSimpleModel {

        private Set<PropertyChangeListener> listeners;
        private boolean editable;

        public DefaultSimpleModel() {
            listeners = new HashSet<>(25);
        }

        @Override
        public void setEditable(boolean value) {
            if (value != editable) {
                editable = value;
                firePropertyChange("editable", !editable, editable);
            }
        }

        @Override
        public boolean isEditable() {
            return editable;
        }

        @Override
        public void addPropertyChangeListener(PropertyChangeListener listener) {
            listeners.add(listener);
        }

        @Override
        public void removePropertyChangeListener(PropertyChangeListener listener) {
            listeners.remove(listener);
        }

        protected void firePropertyChange(String editable, boolean oldValue, boolean newValue) {
            PropertyChangeEvent evt = new PropertyChangeEvent(this, editable, oldValue, newValue);
            for (PropertyChangeListener listener : listeners) {
                listener.propertyChange(evt);
            }
        }

    }

    public class SimpleTextPanel extends JPanel {

        private SimpleModel model;
        private JTextArea editSpace = new JTextArea(10, 20);

        public SimpleTextPanel(SimpleModel model) {

            this.model = model;
            model.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    updateEditableState();
                }
            });
            setLayout(new BorderLayout());

            // set the border properties
            TitledBorder title = BorderFactory.createTitledBorder("Simple Text Panel");
            title.setTitleColor(Color.BLACK);
            title.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED,
                            Color.DARK_GRAY, Color.GRAY));
            this.setBorder(title);

            editSpace = new JTextArea(10, 20);
            this.add(new JScrollPane(editSpace));

            updateEditableState();
        }

        protected void updateEditableState() {
            editSpace.setEditable(model.isEditable());
        }
    }

    public class SimpleButtonPanel extends JPanel implements ActionListener {

        private MutableSimpleModel model;
        private boolean canWrite = true;

        private JButton switchButton;

        public SimpleButtonPanel(MutableSimpleModel model) {

            this.model = model;
            model.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    updateEditableState();
                }
            });

            // set the border properties
            TitledBorder title = BorderFactory.createTitledBorder("Simple Button Panel");
            title.setTitleColor(Color.BLACK);
            title.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED,
                            Color.DARK_GRAY, Color.GRAY));
            this.setBorder(title);

            switchButton = new JButton("Start");
            switchButton.addActionListener(this);
            this.add(switchButton);

            updateEditableState();
        }


        @Override
        public void actionPerformed(ActionEvent actionEvent) {
            model.setEditable(!model.isEditable());
        }

        protected void updateEditableState() {
            if (model.isEditable()) {
                switchButton.setText("Stop");
            } else {
                switchButton.setText("Start");
            }
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • +1 Ran after copying into my IDE right away! I see what you meant by decoupling: Each class only knowing what it needs to. – Nishant Kelkar Sep 01 '14 at 07:21
  • Ok, finally went through it all. So, the show stopper here is the MutableSimpleModel#setEditable method. Everytime SimpleButtonPanel's actionPerformed() is triggered, so is a round of PropertyChangeListener#propertyChange methods for all of the model's listeners. As SimpleTextPanel only needs notifications from the model, it doesn't need to hook up any other PropertyChangeListeners ("editSpace.addPropertyChangeListener(this);" from my code in SimpleTextPanel was totally unnecessary). Neat solution! – Nishant Kelkar Sep 01 '14 at 07:58
2

I suggest that you create a single JPanel subclass which in turn contains other JPanel objects. In this way, you can keep references to the GUI elements that need to interact (i.e. the button and the text field). Now the ActionListener of the button can access the JTextField, assuming that the ActionListener is an anonymous inner class and that the JTextField is a member variable.

In a more complex situation, this design might not be ideal. Some of the concepts will be the same, though. In particular, you need a parent JPanel which facilitates communication between the children JPanels. The children JPanels expose an interface to allow this communication. For example you could have a TextFieldPanel which has enable() and disable() methods.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • Nice and clear answer. I'll try this out right away. – Nishant Kelkar Sep 01 '14 at 07:08
  • I would argue the need for a *"parent panel"* to facilitate communications, it's not really it's responsibility. Some kind of model which maintained just the state information required for UI, plus event notification capabilities would decouple the solution and reduce exposing the various aspects of the UI to areas which just don't need to know about them...but that's just my opinion ;) – MadProgrammer Sep 01 '14 at 07:13
  • 1
    @MadProgrammer That is certainly a very good solution. And perhaps superior to my suggestions in many ways. Like many areas in programming, there is more than one way to do it, and finding the "best" solution is often a matter of brainstorming potential solutions and trying out each one. – Code-Apprentice Sep 01 '14 at 07:22
  • Having had (other developers) do things to references that they shouldn't have (changed the UI), I never expose anything more then I have to ... but as I say, that's just me ;) – MadProgrammer Sep 01 '14 at 08:00
  • @MadProgrammer s/should/shouldn't ?? – Code-Apprentice Sep 01 '14 at 08:03
  • @MadProgrammer Yes, that sounds like a reasonable general rule of thumb. – Code-Apprentice Sep 01 '14 at 08:03