Swing Components in Default ProcessingWindow

The Processing IDE (4.0b6) is an easy to use Java editor as illustrated by this demo which adds numerous Swing components to the default Processing window. The default canvas, along with pre-programmed graphics, is removed prior to adding components and event handling is performed by adding various ‘listeners’. Processing’s draw() becomes non-functional and therefore graphics is handled by painting on a JPanel. Two unexpected things happen when the sketch is exported as an application: 1.) the menu bar disappears from the sketch frame and shows up on the Mac’s main menubar 2.) some (but not all) of the components are initially invisible until they are clicked on or one of the other visible components is selected. A work around is to add xxxx.repaint(); to the functions of these components, which should prevent the problem. Overall, I found this method easier to use than more complex Java editors, albeit with some idiosyncrasies. Basically there are four steps for each component: a.) instantiate with ‘new’ b.) setBounds c.) add to frame, and then add listener for event handling as required. Repaint() may be necessary if app components aren’t initially visible. The demo exported as an app to macos-x86_64 is shown below. Not tested on M1 or other platforms.

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

javax.swing.JFrame frame;
java.awt.Canvas canvas;

int _wndW = 700;
int _wndH = 600;

int radius = 20;
Color dotColor = Color.RED;

GPanel panel;
JLabel label;
JTextArea txtArea;

void menuBar() {
  JMenuBar menuBar = new JMenuBar();
  JMenu menu = new JMenu("File");
  JMenuItem open = new JMenuItem("Open...");
  menu.add(open);
  menuBar.add(menu);
  menuBar.setToolTipText("menu bar");
  frame.setJMenuBar(menuBar);
  // **** Action **** //
  open.addActionListener( new ActionListener() {
    void actionPerformed(ActionEvent event) {
      if (event.getSource() == open) {
        JFileChooser fileChooser = new JFileChooser();
        int i = fileChooser.showOpenDialog(fileChooser);
        if (i == JFileChooser.APPROVE_OPTION) {
          File file = fileChooser.getSelectedFile();
          String filepath = file.getPath();
          try {
            BufferedReader buffer = new BufferedReader(new FileReader(filepath));
            String s1 = "", s2 = "";
            while ((s1 = buffer.readLine())!= null) {
              s2 += s1 + "\n";
            }
            txtArea.setText(s2);
            buffer.close();
          }
          catch (Exception ex) {
            ex.printStackTrace();
          }
        }
      }
    }
  }
  );
  println("EDT menuBar = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void textField() {
  JTextField txtFld = new JTextField("");
  txtFld.setBounds(50, 40, 280, 30);
  frame.add(txtFld);
  txtFld.setToolTipText("text field");
  // **** Action **** //
  txtFld.addActionListener( new ActionListener() {
    void actionPerformed(ActionEvent actionEvent) {
      label.setText(txtFld.getText());
    }
  }
  );
  txtFld.repaint(); // To make it visible after export application.
  println("EDT txtFld = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void textArea() {
  txtArea = new JTextArea();
  JScrollPane scrlPane = new JScrollPane(txtArea);
  scrlPane.setBounds(30, 240, 380, 280);
  frame.add(scrlPane);
  txtArea.setEditable(true);
  txtArea.setToolTipText("text area");
  // txtArea.setFont(new Font("Menlo", Font.BOLD, 16));
  txtArea.setLineWrap(false);
  txtArea.setWrapStyleWord(true);
  txtArea.repaint();
  println("EDT txtArea = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void button() {
  JButton btn = new JButton("Press me.");
  btn.setBounds(50, 90, 100, 30);
  btn.setToolTipText("button");
  frame.add(btn);
  // **** Action **** //
  btn.addActionListener( new ActionListener() {
    void actionPerformed(ActionEvent actionEvent) {
      label.setText("Don't forget to check out the menubar.");
    }
  }
  );
  btn.repaint();
  println("EDT btn = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void sliderWithTicks() {
  JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, 100, 25);
  slider.setBounds(30, 130, 250, 50);
  frame.add(slider);
  slider.setMinorTickSpacing(5);
  slider.setMajorTickSpacing(10);
  slider.setPaintTicks(true);
  slider.setPaintLabels(true);
  slider.setToolTipText("slider with ticks");
  println("EDT sliderWithTicks = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void slider_plain() {
  JSlider slider = new JSlider(JSlider.HORIZONTAL, 5, 70, 20);
  slider.setBounds(360, 10, 120, 30);
  slider.setToolTipText("slider no ticks");
  frame.add(slider);
  // **** Action **** //
  slider.addChangeListener(new ChangeListener() {
    void stateChanged(ChangeEvent changeEvent) {
      radius = slider.getValue();
      panel.repaint();
    }
  }
  );
  println("EDT slider = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void colorBtn() {
  JButton colorBtn = new JButton("Color");
  colorBtn.setBounds(480, 10, 80, 30);
  colorBtn.setToolTipText("button");
  frame.add(colorBtn);
  // **** Action **** //
  colorBtn.addActionListener( new ActionListener() {
    void actionPerformed(ActionEvent actionEvent) {
      dotColor = JColorChooser.showDialog(null, "Choose color", Color.RED);
      panel.repaint();
    }
  }
  );
  colorBtn.repaint();
  println("EDT colorBtn = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void checkBox() {
  JCheckBox chkBox = new JCheckBox("OK");
  chkBox.setBounds(150, 92, 60, 24);
  frame.add(chkBox);
  chkBox.setToolTipText("check box");
  chkBox.addItemListener(new ItemListener() {
    public void itemStateChanged(ItemEvent evt) {
      if (evt.getStateChange()==1) {
        label.setText("check box checked");
      } else {
        label.setText("check box unchecked");
      }
    }
  }
  );
  chkBox.repaint();
  println("EDT checkBox = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void label() {
  label = new JLabel();
  label.setHorizontalAlignment(JLabel.CENTER);
  label.setBounds(30, 10, 350, 30);
  label.setText("Label for output display.");
  label.setToolTipText("label");
  frame.add(label);
  label.repaint();
  println("EDT label = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void radioGrp() {
  ButtonGroup radioGrp = new ButtonGroup();
  JRadioButton radio1 = new JRadioButton("A");
  radio1.setSelected(true);
  JRadioButton radio2 = new JRadioButton("B");
  JRadioButton radio3 = new JRadioButton("C");
  radio1.setBounds(200, 90, 45, 30);
  radio2.setBounds(250, 90, 45, 30);
  radio3.setBounds(300, 90, 60, 30);
  radioGrp.add(radio1);
  radioGrp.add(radio2);
  radioGrp.add(radio3);
  radio1.setToolTipText("radio button group");
  frame.add(radio1);
  frame.add(radio2);
  frame.add(radio3);
  radio1.addItemListener( new ItemListener() {
    void itemStateChanged(ItemEvent evnt) {
      if (evnt.getStateChange()==1) {
        label.setText("Radio1 selected.");
      }
    }
  }
  );
  radio2.addItemListener( new ItemListener() {
    void itemStateChanged(ItemEvent evnt) {
      if (evnt.getStateChange()==1) {
        label.setText("Radio2 selected.");
      }
    }
  }
  );
  radio3.addItemListener( new ItemListener() {
    void itemStateChanged(ItemEvent evnt) {
      if (evnt.getStateChange()==1) {
        label.setText("Radio3 selected.");
      }
    }
  }
  );
  radio1.repaint(); // So they will show up when exported to app.
  radio2.repaint();
  radio3.repaint();
  println("EDT radioGrp = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void progressBar() {
  JProgressBar pbar = new JProgressBar();
  pbar.setBounds(50, 180, 250, 30);
  pbar.setValue(75);
  pbar.setToolTipText("progress bar");
  frame.add(pbar);
  pbar.repaint();
  println("EDT progressBar = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void comboBox() {
  String country[]={"France", "Australia", "U.S.A", "England", "New Zealand"};
  JComboBox comboBox = new JComboBox(country);
  comboBox.setBounds(550, 60, 130, 24);
  comboBox.setToolTipText("comboBox");
  frame.add(comboBox);
  println("EDT comboBox = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void listControl() {
  DefaultListModel<String> listArray = new DefaultListModel<>();
  listArray.addElement("Item 1");
  listArray.addElement("Item 2");
  listArray.addElement("Item 3");
  listArray.addElement("Item 4");
  JList<String> list = new JList<>(listArray);
  list.setBounds(560, 120, 100, 75);
  list.setToolTipText("list");
  frame.add(list);
  // **** Listener **** //
  list.addListSelectionListener( new ListSelectionListener() {
    void valueChanged(ListSelectionEvent evnt) {
      label.setText(list.getSelectedValue() + " was selected.");
    }
  }
  );
  list.repaint();
  println("EDT list = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void spinner() {
  //syntax: (init, min, max, step)
  SpinnerModel value = new SpinnerNumberModel(5, 0, 100, 1);
  JSpinner spinner = new JSpinner(value);
  spinner.setBounds(300, 130, 50, 30);
  spinner.setToolTipText("spinner");
  ((JSpinner.DefaultEditor) spinner.getEditor()).getTextField().setEditable(false);
  frame.add(spinner);
  println("EDT spinner = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void tabbedPane() {
  JPanel panel1 = new JPanel();
  panel1.setBackground(Color.BLUE);
  JPanel panel2 = new JPanel();
  panel2.setBackground(Color.GREEN);
  JPanel panel3 = new JPanel();
  panel3.setBackground(Color.RED);
  JTabbedPane tabbedPane = new JTabbedPane();
  tabbedPane.setBounds(420, 230, 250, 150);
  tabbedPane.add("Tab 1", panel1);
  tabbedPane.add("Tab 2", panel2);
  tabbedPane.add("Tab 3", panel3);
  tabbedPane.setToolTipText("tabbed pane");
  frame.add(tabbedPane);
  // **** Action **** //
  tabbedPane.addChangeListener(new ChangeListener() {
    void stateChanged(ChangeEvent changeEvent) {
      int index = tabbedPane.getSelectedIndex();
      label.setText(tabbedPane.getTitleAt(index) + " was selected.");
    }
  }
  );
  tabbedPane.repaint();
  println("EDT tabbedPane = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void table() {
  String data[][] = {
    {"Boat", "Yellow", "Yes"},
    {"Car", "Magenta", "Yes"},
    {"House", "White", "No"}
  };
  String header[] = {"ITEM", "COLOR", "FOR SALE"};
  JTable table = new JTable(data, header);
  table.setSelectionForeground(Color.WHITE);
  table.setToolTipText("table");
  JScrollPane scrlPane = new JScrollPane(table);
  scrlPane.setBounds(430, 400, 220, 80);
  frame.add(scrlPane);
  // **** Listener **** //
  ListSelectionModel cellSelectionModel = table.getSelectionModel();
  cellSelectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
  cellSelectionModel.addListSelectionListener(new ListSelectionListener() {
    void valueChanged(ListSelectionEvent evnt) {
      String selectedData = null;
      int[] selectedRow = table.getSelectedRows();
      int[] selectedColumns = table.getSelectedColumns();
      for (int i = 0; i < selectedRow.length; i++) {
        for (int j = 0; j < selectedColumns.length; j++) {
          selectedData = (String) table.getValueAt(selectedRow[i], selectedColumns[j]);
          label.setText(selectedData + " was selected.");
        }
      }
    }
  }
  );
  println("EDT table = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

class GPanel extends JPanel {
  GPanel() {
    setBounds(380, 60, 150, 150);
    setBorder(BorderFactory.createLineBorder(Color.BLACK));
  }
  @Override
    void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor (dotColor);
    g.fillOval(getWidth()/2 - radius, getHeight()/2 - radius, radius * 2, radius * 2);
  }
}

void panel() {
  panel = new GPanel();
  panel.setToolTipText("panel");
  frame.add(panel);
  panel.repaint();
  println("EDT panel = " + javax.swing.SwingUtilities.isEventDispatchThread());
}

void buildWnd() {
  panel();
  textArea();
  checkBox();
  radioGrp();
  label();
  progressBar();
  colorBtn();
  comboBox();
  listControl();
  spinner();
  tabbedPane();
  table();
  menuBar();
  textField();
  button();
  sliderWithTicks();
  slider_plain();
}

void setup() {
  frame = (javax.swing.JFrame) ((processing.awt.PSurfaceAWT.SmoothCanvas) surface.getNative()).getFrame();
  canvas = (processing.awt.PSurfaceAWT.SmoothCanvas) ((processing.awt.PSurfaceAWT)surface).getNative();
  frame.setBounds(500, 300, _wndW, _wndH);
  frame.remove(canvas);

  javax.swing.SwingUtilities.invokeLater(new Runnable() {
    public void run() {
      buildWnd(); // Builds components on EventDispatchThread
    }
  });
}

1 Like

Just be aware that Swing isn’t designed (“safe”) to use on any thread other than the AWT event thread, eg. the Processing animation thread that is running setup().

Swing isn’t designed (“safe”) to use on any thread other than the AWT event thread

I have no knowledge of the inner workings of Processing or Swing; please explain the ramifications. I only know that it makes a pretty neat gui and is fairly easy to do.

Likely to break in unexpected ways, behave inconsistently, work fine on some machines and not others, etc. It’s basically a lack of thread-safety so the ramifications are not easily explainable!

As discussed previously, Swing has been around for a long time; it looks like programmers would have stopped using it if threading was a big problem. I remember using Swing components in a Windows environment several years back with pure Java and they looked pretty clunky. But they don’t look too bad on a Mac using Processing IDE; apparently they’ve been spiffed up somewhere along the line.

Re-read what I said! This is nothing to do with how good or not Swing is. I’m actively involved in developing various Swing projects (eg. NetBeans). Swing components need to be used on the Swing event thread. That is not the Processing animation thread.

Swing event handling code runs on a special thread known as the event dispatch thread. … Some Swing component methods are labelled “thread safe” in the API specification; these can be safely invoked from any thread. All other Swing component methods must be invoked from the event dispatch thread. Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce.

From https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html

I’ve already seen that reference and tested four or five of the components with javax.swing.SwingUtilities.isEventDispatchThread() and they’ve all returned ‘false’. If you know how to set up a special Swing event thread then please enlighten us.

There is no way to start your own event thread. You need to use java.awt.EventQueue::invokeLater to run tasks on the event thread (SwingUtilities just delegates to that). You should never manipulate Swing on any thread where isDispatchThread is false.

Turns out not to be too difficult to fix; took about three lines of code in setup():

javax.swing.SwingUtilities.invokeLater(new Runnable() {
    public void run() {
      buildWnd(); // Builds components on EventDispatchThread
    }
  });

javax.swing.SwingUtilities.isEventDispatchThread() now returns ‘true’ for all seventeen components. Demo revised to reflect changes.

If you are interested in finding out what threads are being executed then this might be helpful, as it allows you to display information about all running threads in your application.

This sketch has 2 tabs, the second one is a Java tab NOT a pde tab.

Try running the sketch and then closing the window, with Task Manager open.

Main Sketch Tab

void settings() {
  size(300, 300);
}

void setup() {
  println("\n###### setup() ########");
  ThreadInfo.printAllThreadInfo();
}

void draw() {
  background(255);
}

void exit() {
  println("\n###### exit() ########");
  ThreadInfo.printAllThreadInfo();
}

Java tab named
ThreadInfo.java

/*
 * Created on 25-Aug-2004
 * Modified 26th November 2015
 *
 * @author Peter Lager
 *
 */
public class ThreadInfo {

  public static void printAllThreadInfo() {
    ThreadGroup root;
    // find the root of all running threads
    //root = parent = Thread.currentThread().getThreadGroup();
    root = Thread.currentThread().getThreadGroup();
    while (root.getParent() != null) 
      root = root.getParent();
    printThreadGroupInfo("", root);
    System.out.println("------------------------------------------------------\n");
  }

  private static void printThreadGroupInfo(String indent, ThreadGroup group) {
    final int SAFETY = 5;
    if (group == null) return;

    System.out.println(indent + "THREAD GROUP: " + group.getName() +
      ": Max Priority: " + group.getMaxPriority() + 
      (group.isDaemon() ? "[Daemon]" : ""));
    // print information about component threads
    int numThreads = group.activeCount();
    Thread threads[] = new Thread[numThreads + SAFETY];
    numThreads = group.enumerate(threads, false);
    for (int i = 0; i < numThreads; i++)
      printThreadInfo(indent + "   ", threads[i]);
    // print infomation about component thread groups
    int numGroups = group.activeGroupCount();
    ThreadGroup groups[] = new ThreadGroup[numGroups + SAFETY];
    numGroups = group.enumerate(groups, false);
    for (int i = 0; i < numGroups; i++)
      printThreadGroupInfo(indent + "   ", groups[i]);
  }

  private static void printThreadInfo(String indent, Thread thread) {
    if (thread == null) return;

    System.out.println(indent + "THREAD: " + thread.getName() +
      ": Priority: " + thread.getPriority() +
      (thread.isDaemon() ? " [Daemon]" : "") +
      (thread.isAlive() ? " [Alive]" : " [NotAlive]")+
      ((Thread.currentThread() == thread) ? " <== Current" : ""));
  }
}
4 Likes

Great! Yes, and with lambdas (which I believe Processing 4 now supports in the language?) it can be a one-liner EventQueue.invokeLater(this::buildWnd); And I prefer using the java.awt.EventQueue directly - amounts to same thing though.

Unfortunately Processing misses the same ability, so passing tasks back into the Processing animation thread takes a little more work. You can use a java.util.concurrent.ConcurrentLinkedQueue<Runnable> and drain and run every task at the start of draw(). Mind you, not useful in your example as that would destroy the rendering, although you should be able to add components around the Processing rendering I think.

1 Like

Thank you for the source code to increase our understanding of threads. When the window is closed I can see the Current thread change from AnimationThread to AWT-EventQueue. I greatly appreciate your help with this project.

1 Like

Thank you for the information and taking the time to look at this project. I appreciate your comments and help.

2 Likes