Merge branch 'bug/JAL-1988_JAL-3772_improved_quit_handling' into ben-big-merge
authorBen Soares <b.soares@dundee.ac.uk>
Tue, 8 Nov 2022 17:47:25 +0000 (17:47 +0000)
committerBen Soares <b.soares@dundee.ac.uk>
Tue, 8 Nov 2022 17:47:25 +0000 (17:47 +0000)
1  2 
src/jalview/gui/JvOptionPane.java

   * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
   * The Jalview Authors are detailed in the 'AUTHORS' file.
   */
 -
  package jalview.gui;
  
 +import java.awt.AWTEvent;
 +import java.awt.ActiveEvent;
  import java.awt.Component;
 +import java.awt.Container;
  import java.awt.Dialog.ModalityType;
 +import java.awt.EventQueue;
  import java.awt.HeadlessException;
 +import java.awt.MenuComponent;
 +import java.awt.Toolkit;
  import java.awt.Window;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
 +import java.awt.event.MouseAdapter;
 +import java.awt.event.MouseMotionAdapter;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import java.util.ArrayList;
@@@ -49,13 -42,10 +49,13 @@@ import javax.swing.JButton
  import javax.swing.JDialog;
  import javax.swing.JFrame;
  import javax.swing.JInternalFrame;
 +import javax.swing.JLayeredPane;
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.SwingUtilities;
  import javax.swing.UIManager;
 +import javax.swing.event.InternalFrameEvent;
 +import javax.swing.event.InternalFrameListener;
  
  import jalview.util.Platform;
  import jalview.util.dialogrunner.DialogRunnerI;
@@@ -65,7 -55,7 +65,7 @@@ public class JvOptionPane extends JOpti
  {
    private static final long serialVersionUID = -3019167117756785229L;
  
 -  private static Object mockResponse = JOptionPane.CANCEL_OPTION;
 +  private static Object mockResponse = JvOptionPane.CANCEL_OPTION;
  
    private static boolean interactiveMode = true;
  
@@@ -74,8 -64,8 +74,8 @@@
    private Map<Object, Callable<Void>> callbacks = new HashMap<>();
  
    /*
 -   * JalviewJS reports user choice in the dialog as the selected
 -   * option (text); this list allows conversion to index (int)
 +   * JalviewJS reports user choice in the dialog as the selected option (text);
 +   * this list allows conversion to index (int)
     */
    List<Object> ourOptions;
  
      }
      switch (optionType)
      {
 -    case JOptionPane.YES_NO_CANCEL_OPTION:
 +    case JvOptionPane.YES_NO_CANCEL_OPTION:
        // FeatureRenderer amendFeatures ?? TODO ??
        // Chimera close
        // PromptUserConfig
        // $FALL-THROUGH$
      default:
 -    case JOptionPane.YES_NO_OPTION:
 +    case JvOptionPane.YES_NO_OPTION:
        // PromptUserConfig usage stats
        // for now treated as "OK CANCEL"
        // $FALL-THROUGH$
 -    case JOptionPane.OK_CANCEL_OPTION:
 +    case JvOptionPane.OK_CANCEL_OPTION:
        // will fall back to simple HTML
        return JOptionPane.showConfirmDialog(parentComponent, message, title,
                optionType);
      }
      switch (optionType)
      {
 -    case JOptionPane.YES_NO_CANCEL_OPTION:
 +    case JvOptionPane.YES_NO_CANCEL_OPTION:
        // ColourMenuHelper.addMenuItmers.offerRemoval TODO
 -    case JOptionPane.YES_NO_OPTION:
 +    case JvOptionPane.YES_NO_OPTION:
        // UserDefinedColoursSave -- relevant? TODO
        // $FALL-THROUGH$
      default:
 -    case JOptionPane.OK_CANCEL_OPTION:
 +    case JvOptionPane.OK_CANCEL_OPTION:
  
        // EditNameDialog --- uses panel for messsage TODO
  
      }
      switch (optionType)
      {
 -    case JOptionPane.YES_NO_CANCEL_OPTION:
 -    case JOptionPane.YES_NO_OPTION:
 +    case JvOptionPane.YES_NO_CANCEL_OPTION:
 +    case JvOptionPane.YES_NO_OPTION:
        // UserQuestionanaireCheck
        // VamsasApplication
        // $FALL-THROUGH$
      default:
 -    case JOptionPane.OK_CANCEL_OPTION:
 +    case JvOptionPane.OK_CANCEL_OPTION:
        // will fall back to simple HTML
        return JOptionPane.showConfirmDialog(parentComponent, message, title,
                optionType, messageType);
      }
      switch (optionType)
      {
 -    case JOptionPane.YES_NO_CANCEL_OPTION:
 -    case JOptionPane.YES_NO_OPTION:
 +    case JvOptionPane.YES_NO_CANCEL_OPTION:
 +    case JvOptionPane.YES_NO_OPTION:
        //$FALL-THROUGH$
      default:
 -    case JOptionPane.OK_CANCEL_OPTION:
 +    case JvOptionPane.OK_CANCEL_OPTION:
        // Preferences editLink/newLink
        return JOptionPane.showConfirmDialog(parentComponent, message, title,
                optionType, messageType, icon);
  
    public static void resetMock()
    {
 -    setMockResponse(JOptionPane.CANCEL_OPTION);
 +    setMockResponse(JvOptionPane.CANCEL_OPTION);
      setInteractiveMode(true);
    }
  
      {
        switch (messageType)
        {
 -      case JOptionPane.WARNING_MESSAGE:
 +      case JvOptionPane.WARNING_MESSAGE:
          prefix = "WARNING! ";
          break;
 -      case JOptionPane.ERROR_MESSAGE:
 +      case JvOptionPane.ERROR_MESSAGE:
          prefix = "ERROR! ";
          break;
        default:
      if (!isInteractiveMode())
      {
        handleResponse(getMockResponse());
+       return;
      }
      // two uses:
      //
                useButtons ? initialValueButton : initialValue);
  
        /*
 -       * In Java, the response is returned to this thread and handled here;
 -       * (for Javascript, see propertyChange)
 +       * In Java, the response is returned to this thread and handled here; (for
 +       * Javascript, see propertyChange)
         */
        if (!Platform.isJS())
        /**
      else
      {
        /*
 -       * This is java similar to the swingjs handling, with the callbacks
 -       * attached to the button press of the dialog.  This means we can use
 -       * a non-modal JDialog for the confirmation without blocking the GUI.
 +       * This is java similar to the swingjs handling, with the callbacks attached to
 +       * the button press of the dialog. This means we can use a non-modal JDialog for
 +       * the confirmation without blocking the GUI.
         */
 +      JOptionPane joptionpane = new JOptionPane();
 +      // Make button options
 +      int[] buttonActions = { JvOptionPane.YES_OPTION,
 +          JvOptionPane.NO_OPTION, JvOptionPane.CANCEL_OPTION };
 +
 +      // we need the strings to make the buttons with actionEventListener
 +      if (options == null)
 +      {
 +        ArrayList<String> options_default = new ArrayList<>();
 +        options_default
 +                .add(UIManager.getString("OptionPane.yesButtonText"));
 +        if (optionType == JvOptionPane.YES_NO_OPTION
 +                || optionType == JvOptionPane.YES_NO_CANCEL_OPTION)
 +        {
 +          options_default
 +                  .add(UIManager.getString("OptionPane.noButtonText"));
 +        }
 +        if (optionType == JvOptionPane.YES_NO_CANCEL_OPTION)
 +        {
 +          options_default
 +                  .add(UIManager.getString("OptionPane.cancelButtonText"));
 +        }
 +        options = options_default.toArray();
 +      }
  
 -      JDialog dialog = createDialog(parentComponent, message, title,
 -              optionType, messageType, icon, options, initialValue, modal,
 -              buttons);
 -      jalview.bin.Console.debug("About to setVisible(true)");
 +      ArrayList<JButton> options_btns = new ArrayList<>();
 +      Object initialValue_btn = null;
 +      if (!Platform.isJS()) // JalviewJS already uses callback, don't need to
 +                            // add them here
 +      {
 +        for (int i = 0; i < options.length && i < 3; i++)
 +        {
 +          Object o = options[i];
 +          int buttonAction = buttonActions[i];
 +          Callable<Void> action = callbacks.get(buttonAction);
 +          JButton jb = new JButton();
 +          jb.setText((String) o);
 +          jb.addActionListener(new ActionListener()
 +          {
 +            @Override
 +            public void actionPerformed(ActionEvent e)
 +            {
 +              joptionpane.setValue(buttonAction);
 +              if (action != null)
 +                Executors.newSingleThreadExecutor().submit(action);
 +              // joptionpane.transferFocusBackward();
 +              joptionpane.transferFocusBackward();
 +              joptionpane.setVisible(false);
 +              // put focus and raise parent window if possible, unless cancel
 +              // button pressed
 +              boolean raiseParent = (parentComponent != null);
 +              if (buttonAction == JvOptionPane.CANCEL_OPTION)
 +                raiseParent = false;
 +              if (optionType == JvOptionPane.YES_NO_OPTION
 +                      && buttonAction == JvOptionPane.NO_OPTION)
 +                raiseParent = false;
 +              if (raiseParent)
 +              {
 +                parentComponent.requestFocus();
 +                if (parentComponent instanceof JInternalFrame)
 +                {
 +                  JInternalFrame jif = (JInternalFrame) parentComponent;
 +                  jif.show();
 +                  jif.moveToFront();
 +                  jif.grabFocus();
 +                }
 +                else if (parentComponent instanceof Window)
 +                {
 +                  Window w = (Window) parentComponent;
 +                  w.toFront();
 +                  w.requestFocus();
 +                }
 +              }
 +              joptionpane.setVisible(false);
 +            }
 +          });
 +          options_btns.add(jb);
 +          if (o.equals(initialValue))
 +            initialValue_btn = jb;
 +        }
 +      }
 +      joptionpane.setMessage(message);
 +      joptionpane.setMessageType(messageType);
 +      joptionpane.setOptionType(optionType);
 +      joptionpane.setIcon(icon);
 +      joptionpane.setOptions(
 +              Platform.isJS() ? options : options_btns.toArray());
 +      joptionpane.setInitialValue(
 +              Platform.isJS() ? initialValue : initialValue_btn);
 +
 +      JDialog dialog = joptionpane.createDialog(parentComponent, title);
 +      dialog.setIconImage(WindowIcons.logoIcon.getImage());
 +      dialog.setModalityType(modal ? ModalityType.APPLICATION_MODAL
 +              : ModalityType.MODELESS);
 +      dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        dialog.setVisible(true);
 -      jalview.bin.Console.debug("Just setVisible(true)");
      }
    }
  
        handleResponse(getMockResponse());
      }
  
 +    // need to set these separately so we can set the title bar icon later
 +    this.setOptionType(yesNoCancelOption);
 +    this.setMessageType(questionMessage);
 +    this.setIcon(icon);
 +    this.setInitialValue(initresponse);
 +    this.setOptions(options);
 +    this.setMessage(mainPanel);
 +
      ourOptions = Arrays.asList(options);
      int response;
      if (parentComponent != this)
      {
 -      response = JOptionPane.showInternalOptionDialog(parentComponent,
 -              mainPanel, title, yesNoCancelOption, questionMessage, icon,
 -              options, initresponse);
 +      JInternalFrame jif = this.createInternalFrame(parentComponent, title);
 +      jif.setFrameIcon(WindowIcons.logoIcon);
 +      jif.addInternalFrameListener(new InternalFrameListener()
 +      {
 +        @Override
 +        public void internalFrameActivated(InternalFrameEvent arg0)
 +        {
 +        }
 +
 +        @Override
 +        public void internalFrameClosed(InternalFrameEvent arg0)
 +        {
 +          JvOptionPane.this.internalDialogHandleResponse();
 +        }
 +
 +        @Override
 +        public void internalFrameClosing(InternalFrameEvent arg0)
 +        {
 +        }
 +
 +        @Override
 +        public void internalFrameDeactivated(InternalFrameEvent arg0)
 +        {
 +        }
 +
 +        @Override
 +        public void internalFrameDeiconified(InternalFrameEvent arg0)
 +        {
 +        }
 +
 +        @Override
 +        public void internalFrameIconified(InternalFrameEvent arg0)
 +        {
 +        }
 +
 +        @Override
 +        public void internalFrameOpened(InternalFrameEvent arg0)
 +        {
 +        }
 +      });
 +      jif.setVisible(true);
 +      startModal(jif);
 +      return;
      }
      else
      {
 -      response = JOptionPane.showOptionDialog(parentComponent, mainPanel,
 -              title, yesNoCancelOption, questionMessage, icon, options,
 -              initresponse);
 +      JDialog dialog = this.createDialog(parentComponent, title);
 +      dialog.setIconImage(WindowIcons.logoIcon.getImage());
 +      dialog.setVisible(true); // blocking
 +      this.internalDialogHandleResponse();
 +      return;
      }
 +  }
 +
 +  private void internalDialogHandleResponse()
 +  {
 +    String responseString = (String) this.getValue();
 +    int response = ourOptions.indexOf(responseString);
 +
      if (!Platform.isJS())
      /**
       * Java only
    }
  
    /*
 -  @Override
 -  public JvOptionPane setResponseHandler(Object response, Runnable action)
 -  {
 -    callbacks.put(response, new Callable<Void>()
 -    {
 -      @Override
 -      public Void call()
 -      {
 -        action.run();
 -        return null;
 -      }
 -    });
 -    return this;
 -  }
 -  */
 +   * @Override public JvOptionPane setResponseHandler(Object response, Runnable
 +   * action) { callbacks.put(response, new Callable<Void>() {
 +   * 
 +   * @Override public Void call() { action.run(); return null; } }); return this;
 +   * }
 +   */
    @Override
    public JvOptionPane setResponseHandler(Object response,
            Callable<Void> action)
    public static int showDialogOnTop(String label, String actionString,
            int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE)
    {
+     if (!isInteractiveMode())
+     {
+       return (int) getMockResponse();
+     }
      // Ensure Jalview window is brought to front (primarily for Quit
      // confirmation window to be visible)
  
      // A better hack which works is to create a new JFrame parent with
      // setAlwaysOnTop(true)
      JFrame dialogParent = new JFrame();
 +    dialogParent.setIconImage(WindowIcons.logoIcon.getImage());
      dialogParent.setAlwaysOnTop(true);
  
      int answer = JOptionPane.showConfirmDialog(dialogParent, label,
            int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE, Icon icon,
            Object[] options, Object initialValue, boolean modal)
    {
 -    showDialogOnTopAsync(new JFrame(), label, actionString,
 -            JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE, icon, options,
 -            initialValue, modal);
 +    JFrame frame = new JFrame();
 +    frame.setIconImage(WindowIcons.logoIcon.getImage());
 +    showDialogOnTopAsync(frame, label, actionString, JOPTIONPANE_OPTION,
 +            JOPTIONPANE_MESSAGETYPE, icon, options, initialValue, modal);
    }
  
    public void showDialogOnTopAsync(JFrame dialogParent, Object label,
            int JOPTIONPANE_MESSAGETYPE, Icon icon, Object[] options,
            Object initialValue, boolean modal, JButton[] buttons)
    {
+     if (!isInteractiveMode())
+     {
+       handleResponse(getMockResponse());
+       return;
+     }
      // Ensure Jalview window is brought to front (primarily for Quit
      // confirmation window to be visible)
  
    public void handleResponse(Object response)
    {
      /*
 -    * this test is for NaN in Chrome
 -    */
 +     * this test is for NaN in Chrome
 +     */
      if (response != null && !response.equals(response))
      {
        return;
            Object[] options, Object initialValue, boolean modal,
            JButton[] buttons)
    {
+     if (!isInteractiveMode())
+     {
+       handleResponse(getMockResponse());
+       return null;
+     }
      JButton[] optionsButtons = null;
      Object initialValueButton = null;
      JOptionPane joptionpane = new JOptionPane();
              Platform.isJS() ? initialValue : initialValueButton);
  
      JDialog dialog = joptionpane.createDialog(parentComponent, title);
 +    dialog.setIconImage(WindowIcons.logoIcon.getImage());
      dialog.setModalityType(
              modal ? ModalityType.APPLICATION_MODAL : ModalityType.MODELESS);
      dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
  
      return false;
    }
 +
 +  /**
 +   * This helper method makes the JInternalFrame wait until it is notified by an
 +   * InternalFrameClosing event. This method also adds the given JOptionPane to
 +   * the JInternalFrame and sizes it according to the JInternalFrame's preferred
 +   * size.
 +   *
 +   * @param f
 +   *          The JInternalFrame to make modal.
 +   */
 +  private static void startModal(JInternalFrame f)
 +  {
 +    // We need to add an additional glasspane-like component directly
 +    // below the frame, which intercepts all mouse events that are not
 +    // directed at the frame itself.
 +    JPanel modalInterceptor = new JPanel();
 +    modalInterceptor.setOpaque(false);
 +    JLayeredPane lp = JLayeredPane.getLayeredPaneAbove(f);
 +    lp.setLayer(modalInterceptor, JLayeredPane.MODAL_LAYER.intValue());
 +    modalInterceptor.setBounds(0, 0, lp.getWidth(), lp.getHeight());
 +    modalInterceptor.addMouseListener(new MouseAdapter()
 +    {
 +    });
 +    modalInterceptor.addMouseMotionListener(new MouseMotionAdapter()
 +    {
 +    });
 +    lp.add(modalInterceptor);
 +    f.toFront();
 +
 +    // We need to explicitly dispatch events when we are blocking the event
 +    // dispatch thread.
 +    EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
 +    try
 +    {
 +      while (!f.isClosed())
 +      {
 +        if (EventQueue.isDispatchThread())
 +        {
 +          // The getNextEventMethod() issues wait() when no
 +          // event is available, so we don't need do explicitly wait().
 +          AWTEvent ev = queue.getNextEvent();
 +          // This mimics EventQueue.dispatchEvent(). We can't use
 +          // EventQueue.dispatchEvent() directly, because it is
 +          // protected, unfortunately.
 +          if (ev instanceof ActiveEvent)
 +            ((ActiveEvent) ev).dispatch();
 +          else if (ev.getSource() instanceof Component)
 +            ((Component) ev.getSource()).dispatchEvent(ev);
 +          else if (ev.getSource() instanceof MenuComponent)
 +            ((MenuComponent) ev.getSource()).dispatchEvent(ev);
 +          // Other events are ignored as per spec in
 +          // EventQueue.dispatchEvent
 +        }
 +        else
 +        {
 +          // Give other threads a chance to become active.
 +          Thread.yield();
 +        }
 +      }
 +    } catch (InterruptedException ex)
 +    {
 +      // If we get interrupted, then leave the modal state.
 +    } finally
 +    {
 +      // Clean up the modal interceptor.
 +      lp.remove(modalInterceptor);
 +
 +      // Remove the internal frame from its parent, so it is no longer
 +      // lurking around and clogging memory.
 +      Container parent = f.getParent();
 +      if (parent != null)
 +        parent.remove(f);
 +    }
 +  }
  }