Merge branch 'develop' into improvement/JAL-4124_dont_duplacate_PAE_data_acrossviews
[jalview.git] / src / jalview / gui / JvOptionPane.java
index c0efd4a..7a5daf7 100644 (file)
@@ -32,10 +32,12 @@ import java.awt.Toolkit;
 import java.awt.Window;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseMotionAdapter;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
+import java.beans.PropertyVetoException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -50,8 +52,11 @@ import javax.swing.JDialog;
 import javax.swing.JFrame;
 import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
+import javax.swing.JRootPane;
 import javax.swing.SwingUtilities;
 import javax.swing.UIManager;
 import javax.swing.event.InternalFrameEvent;
@@ -83,6 +88,13 @@ public class JvOptionPane extends JOptionPane
 
   private Map<Object, Runnable> callbacks = new HashMap<>();
 
+  private int timeout = -1;
+
+  public void setTimeout(int i)
+  {
+    timeout = i;
+  }
+
   /*
    * JalviewJS reports user choice in the dialog as the selected option (text);
    * this list allows conversion to index (int)
@@ -717,7 +729,8 @@ public class JvOptionPane extends JOptionPane
 
   private static void outputMessage(Object message)
   {
-    System.out.println(">>> JOption Message : " + message.toString());
+    jalview.bin.Console
+            .outPrintln(">>> JOption Message : " + message.toString());
   }
 
   public static Object getMockResponse()
@@ -845,6 +858,42 @@ public class JvOptionPane extends JOptionPane
                   "Supplied buttons array not the same length as supplied options array.");
           break NOTNULL;
         }
+
+        // run through buttons for initialValue first so we can set (and start)
+        // a final timeoutThreadF to include (and interrupt) in the button
+        // actions
+        Thread timeoutThread = null;
+        for (int i = 0; i < options.length; i++)
+        {
+          Object o = options[i];
+          JButton jb = buttons[i];
+          if (o.equals(initialValue))
+          {
+            if (timeout > 0 && jb != null && jb instanceof JButton)
+            {
+              // after timeout ms click the default button
+              timeoutThread = new Thread(() -> {
+                try
+                {
+                  Thread.sleep(timeout);
+                } catch (InterruptedException e)
+                {
+                  Console.debug(
+                          "Dialog timeout interrupted.  Probably a button pressed.");
+                }
+                jb.doClick();
+              });
+            }
+            initialValueButton = jb;
+            break;
+          }
+        }
+        final Thread timeoutThreadF = timeoutThread;
+        if (timeoutThreadF != null)
+        {
+          timeoutThreadF.start();
+        }
+
         int[] buttonActions = { JOptionPane.YES_OPTION,
             JOptionPane.NO_OPTION, JOptionPane.CANCEL_OPTION };
         for (int i = 0; i < options.length; i++)
@@ -854,9 +903,6 @@ public class JvOptionPane extends JOptionPane
                   "Setting button " + i + " to '" + o.toString() + "'");
           JButton jb = buttons[i];
 
-          if (o.equals(initialValue))
-            initialValueButton = jb;
-
           int buttonAction = buttonActions[i];
           Runnable action = callbacks.get(buttonAction);
           jb.setText((String) o);
@@ -865,6 +911,10 @@ public class JvOptionPane extends JOptionPane
             @Override
             public void actionPerformed(ActionEvent e)
             {
+              if (timeoutThreadF != null)
+              {
+                timeoutThreadF.interrupt();
+              }
 
               Object obj = e.getSource();
               if (obj == null || !(obj instanceof Component))
@@ -1071,9 +1121,16 @@ public class JvOptionPane extends JOptionPane
     if (parentComponent != this
             && !(parentComponent == null && Desktop.instance == null))
     {
+      // note the parent goes back to a JRootPane so is probably
+      // Desktop.getDesktop()
       JInternalFrame jif = this.createInternalFrame(
               parentComponent != null ? parentComponent : Desktop.instance,
               title);
+      // connect to the alignFrame using a map in Desktop
+      if (parentComponent instanceof AlignFrame)
+      {
+        Desktop.addModal((AlignFrame) parentComponent, jif);
+      }
       jif.setFrameIcon(null);
       jif.addInternalFrameListener(new InternalFrameListener()
       {
@@ -1129,7 +1186,13 @@ public class JvOptionPane extends JOptionPane
 
   private void internalDialogHandleResponse()
   {
-    String responseString = (String) this.getValue();
+    Object value = this.getValue();
+    if (value == null
+            || (value instanceof Integer && (Integer) value == -1))
+    {
+      return;
+    }
+    String responseString = value.toString();
     int response = ourOptions.indexOf(responseString);
 
     if (!Platform.isJS())
@@ -1246,6 +1309,17 @@ public class JvOptionPane extends JOptionPane
           int JOPTIONPANE_MESSAGETYPE, Icon icon, Object[] options,
           Object initialValue, boolean modal, JButton[] buttons)
   {
+    showDialogOnTopAsync(dialogParent, label, actionString,
+            JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE, icon, options,
+            initialValue, modal, buttons, true);
+  }
+
+  public void showDialogOnTopAsync(JFrame dialogParent, Object label,
+          String actionString, int JOPTIONPANE_OPTION,
+          int JOPTIONPANE_MESSAGETYPE, Icon icon, Object[] options,
+          Object initialValue, boolean modal, JButton[] buttons,
+          boolean dispose)
+  {
     if (!isInteractiveMode())
     {
       handleResponse(getMockResponse());
@@ -1260,6 +1334,7 @@ public class JvOptionPane extends JOptionPane
 
     // A better hack which works is to create a new JFrame parent with
     // setAlwaysOnTop(true)
+    boolean parentOnTop = dialogParent.isAlwaysOnTop();
     dialogParent.setAlwaysOnTop(true);
     parentComponent = dialogParent;
 
@@ -1267,8 +1342,13 @@ public class JvOptionPane extends JOptionPane
             JOPTIONPANE_MESSAGETYPE, icon, options, initialValue, modal,
             buttons);
 
-    dialogParent.setAlwaysOnTop(false);
-    dialogParent.dispose();
+    dialogParent.setAlwaysOnTop(parentOnTop);
+
+    if (dispose)
+    {
+      dialogParent.setAlwaysOnTop(false);
+      dialogParent.dispose();
+    }
   }
 
   /**
@@ -1499,11 +1579,26 @@ public class JvOptionPane extends JOptionPane
     lp.add(modalInterceptor);
     f.toFront();
 
+    // disable the main menu bar if in Linux
+    JMenuBar menubar = null;
+    if (Platform.isLinux())
+    {
+      JRootPane rootpane = Desktop.getDesktop().getRootPane();
+      menubar = rootpane.getJMenuBar();
+    }
+
     // We need to explicitly dispatch events when we are blocking the event
     // dispatch thread.
     EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
     try
     {
+      if (menubar != null)
+      {
+        // don't allow clicks on main menu on linux due to a hanging bug.
+        // see JAL-4214.
+        setMenusEnabled(menubar, false);
+      }
+
       while (!f.isClosed())
       {
         if (EventQueue.isDispatchThread())
@@ -1515,11 +1610,25 @@ public class JvOptionPane extends JOptionPane
           // EventQueue.dispatchEvent() directly, because it is
           // protected, unfortunately.
           if (ev instanceof ActiveEvent)
+          {
             ((ActiveEvent) ev).dispatch();
-          else if (ev.getSource() instanceof Component)
+          }
+          else if (ev instanceof KeyEvent && ((KeyEvent) ev).isControlDown()
+                  && menubar != null)
+          {
+            // temporarily enable menus to send Ctrl+? KeyEvents
+            setMenusEnabled(menubar, true);
             ((Component) ev.getSource()).dispatchEvent(ev);
+            setMenusEnabled(menubar, false);
+          }
           else if (ev.getSource() instanceof MenuComponent)
+          {
             ((MenuComponent) ev.getSource()).dispatchEvent(ev);
+          }
+          else if (ev.getSource() instanceof Component)
+          {
+            ((Component) ev.getSource()).dispatchEvent(ev);
+          }
           // Other events are ignored as per spec in
           // EventQueue.dispatchEvent
         }
@@ -1534,14 +1643,34 @@ public class JvOptionPane extends JOptionPane
       // If we get interrupted, then leave the modal state.
     } finally
     {
+      // re-enable the main menu bar
+      if (menubar != null)
+      {
+        setMenusEnabled(menubar, true);
+      }
+
       // Clean up the modal interceptor.
       lp.remove(modalInterceptor);
 
+      // unpaint the frame
+      f.setVisible(false);
+
+      // close the frame
+      try
+      {
+        f.setClosed(true);
+      } catch (PropertyVetoException e)
+      {
+        f.doDefaultCloseAction();
+      }
+
       // 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);
+      }
     }
   }
 
@@ -1612,4 +1741,14 @@ public class JvOptionPane extends JOptionPane
 
     return jvop;
   }
+
+  private static void setMenusEnabled(JMenuBar menubar, boolean b)
+  {
+    for (int i = 0; i < menubar.getMenuCount(); i++)
+    {
+      JMenu menu = menubar.getMenu(i);
+      menu.setEnabled(b);
+    }
+  }
+
 }