Search code examples
javawindowsswingsystem-tray

How to prevent the TrayIcon popup to occupy the whole dispatcher thread


I have a java application that uses a JFrame as well as a TrayIcon and I added a PopupMenu to the TrayIcon.

When I click on the TrayIcon the popup menu shows up, but the main frame freezes as long as the PopupMenu is visible.

My first thought was that the event dispatch thread is occupied by someone. So I wrote a small example application that uses a swing worker and a progress bar.

public class TrayIconTest {

  public static void main(String[] args) throws AWTException {
    JFrame frame = new JFrame("TrayIconTest");
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    Container contentPane = frame.getContentPane();
    JProgressBar jProgressBar = new JProgressBar();
    BoundedRangeModel boundedRangeModel = jProgressBar.getModel();
    contentPane.add(jProgressBar);

    boundedRangeModel.setMinimum(0);
    boundedRangeModel.setMaximum(100);

    PopupMenu popup = new PopupMenu();
    TrayIcon trayIcon =
            new TrayIcon(new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB), "TEST");


    // Create a popup menu components
    MenuItem aboutItem = new MenuItem("About");
    popup.add(aboutItem);
    trayIcon.setPopupMenu(popup);
    SystemTray tray = SystemTray.getSystemTray();
    tray.add(trayIcon);

    IndeterminateSwingWorker indeterminateSwingWorker = new IndeterminateSwingWorker(boundedRangeModel);
    indeterminateSwingWorker.execute();


    frame.setSize(640, 80);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);

  }
}

class IndeterminateSwingWorker extends SwingWorker<Void, Integer> {

  private BoundedRangeModel boundedRangeModel;

  public IndeterminateSwingWorker(BoundedRangeModel boundedRangeModel) {
    this.boundedRangeModel = boundedRangeModel;
  }

  @Override
  protected Void doInBackground() throws Exception {
    int i = 0;
    long start = System.currentTimeMillis();
    long runtime = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
    while (true) {
      i++;
      i %= boundedRangeModel.getMaximum();

      publish(i);
      Thread.sleep(20);

      long now = System.currentTimeMillis();
      if ((now - start) > runtime) {
        break;
      }

      System.out.println("i = " + i);
    }
    return null;
  }

  @Override
  protected void process(List<Integer> chunks) {
    for (Integer chunk : chunks) {
      boundedRangeModel.setValue(chunk);
    }
  }
}

How to reproduce

When you start the example application you will see a frame with a progress bar. A SwingWorker simulates a progress action.

The SwingWorker publishes the progress value and also prints it to System.out.

When I click on the tray icon ('the black one') the progress bar freezes and the popup menu shows up. I can see that the SwingWorker is still running in the background, because it outputs the progress value to the command line.

Investigation

So I took a look at the application using jvisualvm and it shows me that the event dispatcher thread is very busy as long as the popup is visible.

jvisualvm analysis

I could figure out that look at the popup is made visible by the method sun.awt.windows.WTrayIconPeer.showPopupMenu(int, int).

This method submits a runnable using EventQueue.invokeLater to the event dispatch thread. In this runnable the method sun.awt.windows.WPopupMenuPeer._show is invoked. This method is native and it seems that it implements a busy wait loop so that the event dispatch thread is completely occupied by this method. Thus other ui related tasks are not executed as long as the popup menu is visible.

Does anyone know a workaround that keeps the event dispatch thread responsive while a TrayIcon popup is shown?


Solution

  • I'm now using a JPopupMenu as workaround, but you can't add the JPopupMenu directly to the TrayIcon.

    The trick is to write a MouseListener

    public class JPopupMenuMouseAdapter extends MouseAdapter {
    
      private JPopupMenu popupMenu;
    
      public JPopupMenuMouseAdapter(JPopupMenu popupMenu) {
        this.popupMenu = popupMenu;
      }
    
      @Override
      public void mouseReleased(MouseEvent e) {
        maybeShowPopup(e);
      }
    
      @Override
      public void mousePressed(MouseEvent e) {
        maybeShowPopup(e);
      }
    
      private void maybeShowPopup(MouseEvent e) {
        if (e.isPopupTrigger()) {
          Dimension size = popupMenu.getPreferredSize();
          popupMenu.setLocation(e.getX() - size.width, e.getY() - size.height);
          popupMenu.setInvoker(popupMenu);
          popupMenu.setVisible(true);
        }
      }
    

    and connect it to the TrayIcon

    TrayIcon trayIcon = ...;
    JPopupMenu popupMenu = ...;
    
    JPopupMenuMouseAdapter jPopupMenuMouseAdapter = new JPopupMenuMouseAdapter(popupMenu);
    trayIcon.addMouseListener(jPopupMenuMouseAdapter);