Merge branch 'bug/JAL-1988_JAL-3772_improved_quit_handling' into merge/JAL-1988_JAL...
[jalview.git] / src / jalview / gui / JvOptionPane.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import java.awt.AWTEvent;
24 import java.awt.ActiveEvent;
25 import java.awt.Component;
26 import java.awt.Container;
27 import java.awt.Dialog.ModalityType;
28 import java.awt.EventQueue;
29 import java.awt.HeadlessException;
30 import java.awt.MenuComponent;
31 import java.awt.Toolkit;
32 import java.awt.Window;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.MouseAdapter;
36 import java.awt.event.MouseMotionAdapter;
37 import java.beans.PropertyChangeEvent;
38 import java.beans.PropertyChangeListener;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.concurrent.Callable;
45 import java.util.concurrent.Executors;
46
47 import javax.swing.Icon;
48 import javax.swing.JButton;
49 import javax.swing.JDialog;
50 import javax.swing.JFrame;
51 import javax.swing.JInternalFrame;
52 import javax.swing.JLayeredPane;
53 import javax.swing.JOptionPane;
54 import javax.swing.JPanel;
55 import javax.swing.SwingUtilities;
56 import javax.swing.UIManager;
57 import javax.swing.event.InternalFrameEvent;
58 import javax.swing.event.InternalFrameListener;
59
60 import jalview.util.Platform;
61 import jalview.util.dialogrunner.DialogRunnerI;
62
63 public class JvOptionPane extends JOptionPane
64         implements DialogRunnerI, PropertyChangeListener
65 {
66   private static final long serialVersionUID = -3019167117756785229L;
67
68   private static Object mockResponse = JvOptionPane.CANCEL_OPTION;
69
70   private static boolean interactiveMode = true;
71
72   private Component parentComponent;
73
74   private Map<Object, Callable<Void>> callbacks = new HashMap<>();
75
76   /*
77    * JalviewJS reports user choice in the dialog as the selected option (text);
78    * this list allows conversion to index (int)
79    */
80   List<Object> ourOptions;
81
82   public JvOptionPane(final Component parent)
83   {
84     this.parentComponent = Platform.isJS() ? this : parent;
85   }
86
87   public static int showConfirmDialog(Component parentComponent,
88           Object message) throws HeadlessException
89   {
90     // only called by test
91     return isInteractiveMode()
92             ? JOptionPane.showConfirmDialog(parentComponent, message)
93             : (int) getMockResponse();
94   }
95
96   /**
97    * Message, title, optionType
98    * 
99    * @param parentComponent
100    * @param message
101    * @param title
102    * @param optionType
103    * @return
104    * @throws HeadlessException
105    */
106   public static int showConfirmDialog(Component parentComponent,
107           Object message, String title, int optionType)
108           throws HeadlessException
109   {
110     if (!isInteractiveMode())
111     {
112       return (int) getMockResponse();
113     }
114     switch (optionType)
115     {
116     case JvOptionPane.YES_NO_CANCEL_OPTION:
117       // FeatureRenderer amendFeatures ?? TODO ??
118       // Chimera close
119       // PromptUserConfig
120       // $FALL-THROUGH$
121     default:
122     case JvOptionPane.YES_NO_OPTION:
123       // PromptUserConfig usage stats
124       // for now treated as "OK CANCEL"
125       // $FALL-THROUGH$
126     case JvOptionPane.OK_CANCEL_OPTION:
127       // will fall back to simple HTML
128       return JOptionPane.showConfirmDialog(parentComponent, message, title,
129               optionType);
130     }
131   }
132
133   /**
134    * Adds a message type. Fallback is to just add it in the beginning.
135    * 
136    * @param parentComponent
137    * @param message
138    * @param title
139    * @param optionType
140    * @param messageType
141    * @return
142    * @throws HeadlessException
143    */
144   public static int showConfirmDialog(Component parentComponent,
145           Object message, String title, int optionType, int messageType)
146           throws HeadlessException
147   {
148     // JalviewServicesChanged
149     // PromptUserConfig raiseDialog
150     return isInteractiveMode()
151             ? JOptionPane.showConfirmDialog(parentComponent, message, title,
152                     optionType, messageType)
153             : (int) getMockResponse();
154   }
155
156   /**
157    * Adds an icon
158    * 
159    * @param parentComponent
160    * @param message
161    * @param title
162    * @param optionType
163    * @param messageType
164    * @param icon
165    * @return
166    * @throws HeadlessException
167    */
168   public static int showConfirmDialog(Component parentComponent,
169           Object message, String title, int optionType, int messageType,
170           Icon icon) throws HeadlessException
171   {
172     // JvOptionPaneTest only
173     return isInteractiveMode()
174             ? JOptionPane.showConfirmDialog(parentComponent, message, title,
175                     optionType, messageType, icon)
176             : (int) getMockResponse();
177   }
178
179   /**
180    * Internal version "OK"
181    * 
182    * @param parentComponent
183    * @param message
184    * @return
185    */
186   public static int showInternalConfirmDialog(Component parentComponent,
187           Object message)
188   {
189     // JvOptionPaneTest only;
190     return isInteractiveMode()
191             ? JOptionPane.showInternalConfirmDialog(parentComponent,
192                     message)
193             : (int) getMockResponse();
194   }
195
196   /**
197    * Internal version -- changed to standard version for now
198    * 
199    * @param parentComponent
200    * @param message
201    * @param title
202    * @param optionType
203    * @return
204    */
205   public static int showInternalConfirmDialog(Component parentComponent,
206           String message, String title, int optionType)
207   {
208     if (!isInteractiveMode())
209     {
210       return (int) getMockResponse();
211     }
212     switch (optionType)
213     {
214     case JvOptionPane.YES_NO_CANCEL_OPTION:
215       // ColourMenuHelper.addMenuItmers.offerRemoval TODO
216     case JvOptionPane.YES_NO_OPTION:
217       // UserDefinedColoursSave -- relevant? TODO
218       // $FALL-THROUGH$
219     default:
220     case JvOptionPane.OK_CANCEL_OPTION:
221
222       // EditNameDialog --- uses panel for messsage TODO
223
224       // Desktop.inputURLMenuItem
225       // WsPreferenses
226       return JOptionPane.showConfirmDialog(parentComponent, message, title,
227               optionType);
228     }
229   }
230
231   /**
232    * 
233    * @param parentComponent
234    * @param message
235    * @param title
236    * @param optionType
237    * @param messageType
238    * @return
239    */
240   public static int showInternalConfirmDialog(Component parentComponent,
241           Object message, String title, int optionType, int messageType)
242   {
243     if (!isInteractiveMode())
244     {
245       return (int) getMockResponse();
246     }
247     switch (optionType)
248     {
249     case JvOptionPane.YES_NO_CANCEL_OPTION:
250     case JvOptionPane.YES_NO_OPTION:
251       // UserQuestionanaireCheck
252       // VamsasApplication
253       // $FALL-THROUGH$
254     default:
255     case JvOptionPane.OK_CANCEL_OPTION:
256       // will fall back to simple HTML
257       return JOptionPane.showConfirmDialog(parentComponent, message, title,
258               optionType, messageType);
259     }
260   }
261
262   /**
263    * adds icon; no longer internal
264    * 
265    * @param parentComponent
266    * @param message
267    * @param title
268    * @param optionType
269    * @param messageType
270    * @param icon
271    * @return
272    */
273   public static int showInternalConfirmDialog(Component parentComponent,
274           Object message, String title, int optionType, int messageType,
275           Icon icon)
276   {
277     if (!isInteractiveMode())
278     {
279       return (int) getMockResponse();
280     }
281     switch (optionType)
282     {
283     case JvOptionPane.YES_NO_CANCEL_OPTION:
284     case JvOptionPane.YES_NO_OPTION:
285       //$FALL-THROUGH$
286     default:
287     case JvOptionPane.OK_CANCEL_OPTION:
288       // Preferences editLink/newLink
289       return JOptionPane.showConfirmDialog(parentComponent, message, title,
290               optionType, messageType, icon);
291     }
292
293   }
294
295   /**
296    * custom options full-featured
297    * 
298    * @param parentComponent
299    * @param message
300    * @param title
301    * @param optionType
302    * @param messageType
303    * @param icon
304    * @param options
305    * @param initialValue
306    * @return
307    * @throws HeadlessException
308    */
309   public static int showOptionDialog(Component parentComponent,
310           String message, String title, int optionType, int messageType,
311           Icon icon, Object[] options, Object initialValue)
312           throws HeadlessException
313   {
314     if (!isInteractiveMode())
315     {
316       return (int) getMockResponse();
317     }
318     // two uses:
319     //
320     // TODO
321     //
322     // 1) AlignViewport for openLinkedAlignment
323     //
324     // Show a dialog with the option to open and link (cDNA <-> protein) as a
325     // new
326     // alignment, either as a standalone alignment or in a split frame. Returns
327     // true if the new alignment was opened, false if not, because the user
328     // declined the offer.
329     //
330     // 2) UserDefinedColors warning about saving over a name already defined
331     //
332     return JOptionPane.showOptionDialog(parentComponent, message, title,
333             optionType, messageType, icon, options, initialValue);
334   }
335
336   /**
337    * Just an OK message
338    * 
339    * @param message
340    * @throws HeadlessException
341    */
342   public static void showMessageDialog(Component parentComponent,
343           String message) throws HeadlessException
344   {
345     if (!isInteractiveMode())
346     {
347       outputMessage(message);
348       return;
349     }
350
351     // test class only
352
353     JOptionPane.showMessageDialog(parentComponent, message);
354   }
355
356   /**
357    * OK with message, title, and type
358    * 
359    * @param parentComponent
360    * @param message
361    * @param title
362    * @param messageType
363    * @throws HeadlessException
364    */
365   public static void showMessageDialog(Component parentComponent,
366           String message, String title, int messageType)
367           throws HeadlessException
368   {
369     // 30 implementations -- all just fine.
370
371     if (!isInteractiveMode())
372     {
373       outputMessage(message);
374       return;
375     }
376
377     JOptionPane.showMessageDialog(parentComponent,
378             getPrefix(messageType) + message, title, messageType);
379   }
380
381   /**
382    * adds title and icon
383    * 
384    * @param parentComponent
385    * @param message
386    * @param title
387    * @param messageType
388    * @param icon
389    * @throws HeadlessException
390    */
391   public static void showMessageDialog(Component parentComponent,
392           String message, String title, int messageType, Icon icon)
393           throws HeadlessException
394   {
395
396     // test only
397
398     if (!isInteractiveMode())
399     {
400       outputMessage(message);
401       return;
402     }
403
404     JOptionPane.showMessageDialog(parentComponent, message, title,
405             messageType, icon);
406   }
407
408   /**
409    * was internal
410    * 
411    */
412   public static void showInternalMessageDialog(Component parentComponent,
413           Object message)
414   {
415
416     // WsPreferences only
417
418     if (!isInteractiveMode())
419     {
420       outputMessage(message);
421       return;
422     }
423
424     JOptionPane.showMessageDialog(parentComponent, message);
425   }
426
427   /**
428    * Adds title and messageType
429    * 
430    * @param parentComponent
431    * @param message
432    * @param title
433    * @param messageType
434    */
435   public static void showInternalMessageDialog(Component parentComponent,
436           String message, String title, int messageType)
437   {
438
439     // 41 references
440
441     if (!isInteractiveMode())
442     {
443       outputMessage(message);
444       return;
445     }
446
447     JOptionPane.showMessageDialog(parentComponent,
448             getPrefix(messageType) + message, title, messageType);
449   }
450
451   /**
452    * 
453    * @param parentComponent
454    * @param message
455    * @param title
456    * @param messageType
457    * @param icon
458    */
459   public static void showInternalMessageDialog(Component parentComponent,
460           Object message, String title, int messageType, Icon icon)
461   {
462
463     // test only
464
465     if (!isInteractiveMode())
466     {
467       outputMessage(message);
468       return;
469     }
470
471     JOptionPane.showMessageDialog(parentComponent, message, title,
472             messageType, icon);
473   }
474
475   /**
476    * 
477    * @param message
478    * @return
479    * @throws HeadlessException
480    */
481   public static String showInputDialog(Object message)
482           throws HeadlessException
483   {
484     // test only
485
486     if (!isInteractiveMode())
487     {
488       return getMockResponse().toString();
489     }
490
491     return JOptionPane.showInputDialog(message);
492   }
493
494   /**
495    * adds inital selection value
496    * 
497    * @param message
498    * @param initialSelectionValue
499    * @return
500    */
501   public static String showInputDialog(String message,
502           String initialSelectionValue)
503   {
504     if (!isInteractiveMode())
505     {
506       return getMockResponse().toString();
507     }
508
509     // AnnotationPanel character option
510
511     return JOptionPane.showInputDialog(message, initialSelectionValue);
512   }
513
514   /**
515    * adds inital selection value
516    * 
517    * @param message
518    * @param initialSelectionValue
519    * @return
520    */
521   public static String showInputDialog(Object message,
522           Object initialSelectionValue)
523   {
524     if (!isInteractiveMode())
525     {
526       return getMockResponse().toString();
527     }
528
529     // AnnotationPanel character option
530
531     return JOptionPane.showInputDialog(message, initialSelectionValue);
532   }
533
534   /**
535    * centered on parent
536    * 
537    * @param parentComponent
538    * @param message
539    * @return
540    * @throws HeadlessException
541    */
542   public static String showInputDialog(Component parentComponent,
543           String message) throws HeadlessException
544   {
545     // test only
546
547     return isInteractiveMode()
548             ? JOptionPane.showInputDialog(parentComponent, message)
549             : getMockResponse().toString();
550   }
551
552   /**
553    * input with initial selection
554    * 
555    * @param parentComponent
556    * @param message
557    * @param initialSelectionValue
558    * @return
559    */
560   public static String showInputDialog(Component parentComponent,
561           String message, String initialSelectionValue)
562   {
563
564     // AnnotationPanel
565
566     return isInteractiveMode()
567             ? JOptionPane.showInputDialog(parentComponent, message,
568                     initialSelectionValue)
569             : getMockResponse().toString();
570   }
571
572   /**
573    * input with initial selection
574    * 
575    * @param parentComponent
576    * @param message
577    * @param initialSelectionValue
578    * @return
579    */
580   public static String showInputDialog(Component parentComponent,
581           Object message, Object initialSelectionValue)
582   {
583
584     // AnnotationPanel
585
586     return isInteractiveMode()
587             ? JOptionPane.showInputDialog(parentComponent, message,
588                     initialSelectionValue)
589             : getMockResponse().toString();
590   }
591
592   /**
593    * 
594    * @param parentComponent
595    * @param message
596    * @param title
597    * @param messageType
598    * @return
599    * @throws HeadlessException
600    */
601   public static String showInputDialog(Component parentComponent,
602           String message, String title, int messageType)
603           throws HeadlessException
604   {
605
606     // test only
607
608     return isInteractiveMode()
609             ? JOptionPane.showInputDialog(parentComponent, message, title,
610                     messageType)
611             : getMockResponse().toString();
612   }
613
614   /**
615    * Customized input option
616    * 
617    * @param parentComponent
618    * @param message
619    * @param title
620    * @param messageType
621    * @param icon
622    * @param selectionValues
623    * @param initialSelectionValue
624    * @return
625    * @throws HeadlessException
626    */
627   public static Object showInputDialog(Component parentComponent,
628           Object message, String title, int messageType, Icon icon,
629           Object[] selectionValues, Object initialSelectionValue)
630           throws HeadlessException
631   {
632
633     // test only
634
635     return isInteractiveMode()
636             ? JOptionPane.showInputDialog(parentComponent, message, title,
637                     messageType, icon, selectionValues,
638                     initialSelectionValue)
639             : getMockResponse().toString();
640   }
641
642   /**
643    * internal version
644    * 
645    * @param parentComponent
646    * @param message
647    * @return
648    */
649   public static String showInternalInputDialog(Component parentComponent,
650           String message)
651   {
652     // test only
653
654     return isInteractiveMode()
655             ? JOptionPane.showInternalInputDialog(parentComponent, message)
656             : getMockResponse().toString();
657   }
658
659   /**
660    * internal with title and messageType
661    * 
662    * @param parentComponent
663    * @param message
664    * @param title
665    * @param messageType
666    * @return
667    */
668   public static String showInternalInputDialog(Component parentComponent,
669           String message, String title, int messageType)
670   {
671
672     // AlignFrame tabbedPane_mousePressed
673
674     return isInteractiveMode()
675             ? JOptionPane.showInternalInputDialog(parentComponent,
676                     getPrefix(messageType) + message, title, messageType)
677             : getMockResponse().toString();
678   }
679
680   /**
681    * customized internal
682    * 
683    * @param parentComponent
684    * @param message
685    * @param title
686    * @param messageType
687    * @param icon
688    * @param selectionValues
689    * @param initialSelectionValue
690    * @return
691    */
692   public static Object showInternalInputDialog(Component parentComponent,
693           String message, String title, int messageType, Icon icon,
694           Object[] selectionValues, Object initialSelectionValue)
695   {
696     // test only
697
698     return isInteractiveMode()
699             ? JOptionPane.showInternalInputDialog(parentComponent, message,
700                     title, messageType, icon, selectionValues,
701                     initialSelectionValue)
702             : getMockResponse().toString();
703   }
704
705   ///////////// end of options ///////////////
706
707   private static void outputMessage(Object message)
708   {
709     System.out.println(">>> JOption Message : " + message.toString());
710   }
711
712   public static Object getMockResponse()
713   {
714     return mockResponse;
715   }
716
717   public static void setMockResponse(Object mockOption)
718   {
719     JvOptionPane.mockResponse = mockOption;
720   }
721
722   public static void resetMock()
723   {
724     setMockResponse(JvOptionPane.CANCEL_OPTION);
725     setInteractiveMode(true);
726   }
727
728   public static boolean isInteractiveMode()
729   {
730     return interactiveMode;
731   }
732
733   public static void setInteractiveMode(boolean interactive)
734   {
735     JvOptionPane.interactiveMode = interactive;
736   }
737
738   private static String getPrefix(int messageType)
739   {
740     String prefix = "";
741
742     // JavaScript only
743     if (Platform.isJS())
744     {
745       switch (messageType)
746       {
747       case JvOptionPane.WARNING_MESSAGE:
748         prefix = "WARNING! ";
749         break;
750       case JvOptionPane.ERROR_MESSAGE:
751         prefix = "ERROR! ";
752         break;
753       default:
754         prefix = "Note: ";
755       }
756     }
757     return prefix;
758   }
759
760   /**
761    * create a new option dialog that can be used to register responses - along
762    * lines of showOptionDialog
763    * 
764    * @param desktop
765    * @param question
766    * @param string
767    * @param defaultOption
768    * @param plainMessage
769    * @param object
770    * @param options
771    * @param string2
772    * @return
773    */
774   public static JvOptionPane newOptionDialog()
775   {
776     return new JvOptionPane(null);
777   }
778
779   public static JvOptionPane newOptionDialog(Component parentComponent)
780   {
781     return new JvOptionPane(parentComponent);
782   }
783
784   public void showDialog(String message, String title, int optionType,
785           int messageType, Icon icon, Object[] options, Object initialValue)
786   {
787     showDialog(message, title, optionType, messageType, icon, options,
788             initialValue, true);
789   }
790
791   public void showDialog(Object message, String title, int optionType,
792           int messageType, Icon icon, Object[] options, Object initialValue,
793           boolean modal)
794   {
795     showDialog(message, title, optionType, messageType, icon, options,
796             initialValue, modal, null);
797   }
798
799   public void showDialog(Object message, String title, int optionType,
800           int messageType, Icon icon, Object[] options, Object initialValue,
801           boolean modal, JButton[] buttons)
802   {
803     if (!isInteractiveMode())
804     {
805       handleResponse(getMockResponse());
806       return;
807     }
808     // two uses:
809     //
810     // TODO
811     //
812     // 1) AlignViewport for openLinkedAlignment
813     //
814     // Show a dialog with the option to open and link (cDNA <-> protein) as a
815     // new
816     // alignment, either as a standalone alignment or in a split frame. Returns
817     // true if the new alignment was opened, false if not, because the user
818     // declined the offer.
819     //
820     // 2) UserDefinedColors warning about saving over a name already defined
821     //
822
823     ourOptions = Arrays.asList(options);
824
825     if (modal)
826     {
827       boolean useButtons = false;
828       Object initialValueButton = null;
829       NOTNULL: if (buttons != null)
830       {
831         if (buttons.length != options.length)
832         {
833           jalview.bin.Console.error(
834                   "Supplied buttons array not the same length as supplied options array.");
835           break NOTNULL;
836         }
837         int[] buttonActions = { JOptionPane.YES_OPTION,
838             JOptionPane.NO_OPTION, JOptionPane.CANCEL_OPTION };
839         for (int i = 0; i < options.length; i++)
840         {
841           Object o = options[i];
842           jalview.bin.Console.debug(
843                   "Setting button " + i + " to '" + o.toString() + "'");
844           JButton jb = buttons[i];
845
846           if (o.equals(initialValue))
847             initialValueButton = jb;
848
849           int buttonAction = buttonActions[i];
850           Callable<Void> action = callbacks.get(buttonAction);
851           jb.setText((String) o);
852           jb.addActionListener(new ActionListener()
853           {
854             @Override
855             public void actionPerformed(ActionEvent e)
856             {
857
858               Object obj = e.getSource();
859               if (obj == null || !(obj instanceof Component))
860               {
861                 jalview.bin.Console.debug(
862                         "Could not find Component source of event object "
863                                 + obj);
864                 return;
865               }
866               Object joptionpaneObject = SwingUtilities.getAncestorOfClass(
867                       JOptionPane.class, (Component) obj);
868               if (joptionpaneObject == null
869                       || !(joptionpaneObject instanceof JOptionPane))
870               {
871                 jalview.bin.Console.debug(
872                         "Could not find JOptionPane ancestor of event object "
873                                 + obj);
874                 return;
875               }
876               JOptionPane joptionpane = (JOptionPane) joptionpaneObject;
877               joptionpane.setValue(buttonAction);
878               if (action != null)
879                 Executors.newSingleThreadExecutor().submit(action);
880               joptionpane.transferFocusBackward();
881               joptionpane.setVisible(false);
882               // put focus and raise parent window if possible, unless cancel or
883               // no button pressed
884               boolean raiseParent = (parentComponent != null);
885               if (buttonAction == JOptionPane.CANCEL_OPTION)
886                 raiseParent = false;
887               if (optionType == JOptionPane.YES_NO_OPTION
888                       && buttonAction == JOptionPane.NO_OPTION)
889                 raiseParent = false;
890               if (raiseParent)
891               {
892                 parentComponent.requestFocus();
893                 if (parentComponent instanceof JInternalFrame)
894                 {
895                   JInternalFrame jif = (JInternalFrame) parentComponent;
896                   jif.show();
897                   jif.moveToFront();
898                   jif.grabFocus();
899                 }
900                 else if (parentComponent instanceof Window)
901                 {
902                   Window w = (Window) parentComponent;
903                   w.toFront();
904                   w.requestFocus();
905                 }
906               }
907               joptionpane.setVisible(false);
908             }
909           });
910
911         }
912         useButtons = true;
913       }
914       // use a JOptionPane as usual
915       int response = JOptionPane.showOptionDialog(parentComponent, message,
916               title, optionType, messageType, icon,
917               useButtons ? buttons : options,
918               useButtons ? initialValueButton : initialValue);
919
920       /*
921        * In Java, the response is returned to this thread and handled here; (for
922        * Javascript, see propertyChange)
923        */
924       if (!Platform.isJS())
925       /**
926        * Java only
927        * 
928        * @j2sIgnore
929        */
930       {
931         handleResponse(response);
932       }
933     }
934     else
935     {
936       /*
937        * This is java similar to the swingjs handling, with the callbacks attached to
938        * the button press of the dialog. This means we can use a non-modal JDialog for
939        * the confirmation without blocking the GUI.
940        */
941       JOptionPane joptionpane = new JOptionPane();
942       // Make button options
943       int[] buttonActions = { JvOptionPane.YES_OPTION,
944           JvOptionPane.NO_OPTION, JvOptionPane.CANCEL_OPTION };
945
946       // we need the strings to make the buttons with actionEventListener
947       if (options == null)
948       {
949         ArrayList<String> options_default = new ArrayList<>();
950         options_default
951                 .add(UIManager.getString("OptionPane.yesButtonText"));
952         if (optionType == JvOptionPane.YES_NO_OPTION
953                 || optionType == JvOptionPane.YES_NO_CANCEL_OPTION)
954         {
955           options_default
956                   .add(UIManager.getString("OptionPane.noButtonText"));
957         }
958         if (optionType == JvOptionPane.YES_NO_CANCEL_OPTION)
959         {
960           options_default
961                   .add(UIManager.getString("OptionPane.cancelButtonText"));
962         }
963         options = options_default.toArray();
964       }
965
966       ArrayList<JButton> options_btns = new ArrayList<>();
967       Object initialValue_btn = null;
968       if (!Platform.isJS()) // JalviewJS already uses callback, don't need to
969                             // add them here
970       {
971         for (int i = 0; i < options.length && i < 3; i++)
972         {
973           Object o = options[i];
974           int buttonAction = buttonActions[i];
975           Callable<Void> action = callbacks.get(buttonAction);
976           JButton jb = new JButton();
977           jb.setText((String) o);
978           jb.addActionListener(new ActionListener()
979           {
980             @Override
981             public void actionPerformed(ActionEvent e)
982             {
983               joptionpane.setValue(buttonAction);
984               if (action != null)
985                 Executors.newSingleThreadExecutor().submit(action);
986               // joptionpane.transferFocusBackward();
987               joptionpane.transferFocusBackward();
988               joptionpane.setVisible(false);
989               // put focus and raise parent window if possible, unless cancel
990               // button pressed
991               boolean raiseParent = (parentComponent != null);
992               if (buttonAction == JvOptionPane.CANCEL_OPTION)
993                 raiseParent = false;
994               if (optionType == JvOptionPane.YES_NO_OPTION
995                       && buttonAction == JvOptionPane.NO_OPTION)
996                 raiseParent = false;
997               if (raiseParent)
998               {
999                 parentComponent.requestFocus();
1000                 if (parentComponent instanceof JInternalFrame)
1001                 {
1002                   JInternalFrame jif = (JInternalFrame) parentComponent;
1003                   jif.show();
1004                   jif.moveToFront();
1005                   jif.grabFocus();
1006                 }
1007                 else if (parentComponent instanceof Window)
1008                 {
1009                   Window w = (Window) parentComponent;
1010                   w.toFront();
1011                   w.requestFocus();
1012                 }
1013               }
1014               joptionpane.setVisible(false);
1015             }
1016           });
1017           options_btns.add(jb);
1018           if (o.equals(initialValue))
1019             initialValue_btn = jb;
1020         }
1021       }
1022       joptionpane.setMessage(message);
1023       joptionpane.setMessageType(messageType);
1024       joptionpane.setOptionType(optionType);
1025       joptionpane.setIcon(icon);
1026       joptionpane.setOptions(
1027               Platform.isJS() ? options : options_btns.toArray());
1028       joptionpane.setInitialValue(
1029               Platform.isJS() ? initialValue : initialValue_btn);
1030
1031       JDialog dialog = joptionpane.createDialog(parentComponent, title);
1032       dialog.setIconImage(WindowIcons.logoIcon.getImage());
1033       dialog.setModalityType(modal ? ModalityType.APPLICATION_MODAL
1034               : ModalityType.MODELESS);
1035       dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
1036       dialog.setVisible(true);
1037     }
1038   }
1039
1040   public void showInternalDialog(JPanel mainPanel, String title,
1041           int yesNoCancelOption, int questionMessage, Icon icon,
1042           Object[] options, String initresponse)
1043   {
1044     if (!isInteractiveMode())
1045     {
1046       handleResponse(getMockResponse());
1047     }
1048
1049     // need to set these separately so we can set the title bar icon later
1050     this.setOptionType(yesNoCancelOption);
1051     this.setMessageType(questionMessage);
1052     this.setIcon(icon);
1053     this.setInitialValue(initresponse);
1054     this.setOptions(options);
1055     this.setMessage(mainPanel);
1056
1057     ourOptions = Arrays.asList(options);
1058     int response;
1059     if (parentComponent != this)
1060     {
1061       JInternalFrame jif = this.createInternalFrame(parentComponent, title);
1062       jif.setFrameIcon(WindowIcons.logoIcon);
1063       jif.addInternalFrameListener(new InternalFrameListener()
1064       {
1065         @Override
1066         public void internalFrameActivated(InternalFrameEvent arg0)
1067         {
1068         }
1069
1070         @Override
1071         public void internalFrameClosed(InternalFrameEvent arg0)
1072         {
1073           JvOptionPane.this.internalDialogHandleResponse();
1074         }
1075
1076         @Override
1077         public void internalFrameClosing(InternalFrameEvent arg0)
1078         {
1079         }
1080
1081         @Override
1082         public void internalFrameDeactivated(InternalFrameEvent arg0)
1083         {
1084         }
1085
1086         @Override
1087         public void internalFrameDeiconified(InternalFrameEvent arg0)
1088         {
1089         }
1090
1091         @Override
1092         public void internalFrameIconified(InternalFrameEvent arg0)
1093         {
1094         }
1095
1096         @Override
1097         public void internalFrameOpened(InternalFrameEvent arg0)
1098         {
1099         }
1100       });
1101       jif.setVisible(true);
1102       startModal(jif);
1103       return;
1104     }
1105     else
1106     {
1107       JDialog dialog = this.createDialog(parentComponent, title);
1108       dialog.setIconImage(WindowIcons.logoIcon.getImage());
1109       dialog.setVisible(true); // blocking
1110       this.internalDialogHandleResponse();
1111       return;
1112     }
1113   }
1114
1115   private void internalDialogHandleResponse()
1116   {
1117     String responseString = (String) this.getValue();
1118     int response = ourOptions.indexOf(responseString);
1119
1120     if (!Platform.isJS())
1121     /**
1122      * Java only
1123      * 
1124      * @j2sIgnore
1125      */
1126     {
1127       handleResponse(response);
1128     }
1129   }
1130
1131   /*
1132    * @Override public JvOptionPane setResponseHandler(Object response, Runnable
1133    * action) { callbacks.put(response, new Callable<Void>() {
1134    * 
1135    * @Override public Void call() { action.run(); return null; } }); return this;
1136    * }
1137    */
1138   @Override
1139   public JvOptionPane setResponseHandler(Object response,
1140           Callable<Void> action)
1141   {
1142     callbacks.put(response, action);
1143     return this;
1144   }
1145
1146   /**
1147    * showDialogOnTop will create a dialog that (attempts to) come to top of OS
1148    * desktop windows
1149    */
1150   public static int showDialogOnTop(String label, String actionString,
1151           int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE)
1152   {
1153     if (!isInteractiveMode())
1154     {
1155       return (int) getMockResponse();
1156     }
1157     // Ensure Jalview window is brought to front (primarily for Quit
1158     // confirmation window to be visible)
1159
1160     // This method of raising the Jalview window is broken in java
1161     // jalviewDesktop.setVisible(true);
1162     // jalviewDesktop.toFront();
1163
1164     // A better hack which works is to create a new JFrame parent with
1165     // setAlwaysOnTop(true)
1166     JFrame dialogParent = new JFrame();
1167     dialogParent.setIconImage(WindowIcons.logoIcon.getImage());
1168     dialogParent.setAlwaysOnTop(true);
1169
1170     int answer = JOptionPane.showConfirmDialog(dialogParent, label,
1171             actionString, JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE);
1172
1173     dialogParent.setAlwaysOnTop(false);
1174     dialogParent.dispose();
1175
1176     return answer;
1177   }
1178
1179   public void showDialogOnTopAsync(String label, String actionString,
1180           int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE, Icon icon,
1181           Object[] options, Object initialValue, boolean modal)
1182   {
1183     JFrame frame = new JFrame();
1184     frame.setIconImage(WindowIcons.logoIcon.getImage());
1185     showDialogOnTopAsync(frame, label, actionString, JOPTIONPANE_OPTION,
1186             JOPTIONPANE_MESSAGETYPE, icon, options, initialValue, modal);
1187   }
1188
1189   public void showDialogOnTopAsync(JFrame dialogParent, Object label,
1190           String actionString, int JOPTIONPANE_OPTION,
1191           int JOPTIONPANE_MESSAGETYPE, Icon icon, Object[] options,
1192           Object initialValue, boolean modal)
1193   {
1194     showDialogOnTopAsync(dialogParent, label, actionString,
1195             JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE, icon, options,
1196             initialValue, modal, null);
1197   }
1198
1199   public void showDialogOnTopAsync(JFrame dialogParent, Object label,
1200           String actionString, int JOPTIONPANE_OPTION,
1201           int JOPTIONPANE_MESSAGETYPE, Icon icon, Object[] options,
1202           Object initialValue, boolean modal, JButton[] buttons)
1203   {
1204     if (!isInteractiveMode())
1205     {
1206       handleResponse(getMockResponse());
1207       return;
1208     }
1209     // Ensure Jalview window is brought to front (primarily for Quit
1210     // confirmation window to be visible)
1211
1212     // This method of raising the Jalview window is broken in java
1213     // jalviewDesktop.setVisible(true);
1214     // jalviewDesktop.toFront();
1215
1216     // A better hack which works is to create a new JFrame parent with
1217     // setAlwaysOnTop(true)
1218     dialogParent.setAlwaysOnTop(true);
1219     parentComponent = dialogParent;
1220
1221     showDialog(label, actionString, JOPTIONPANE_OPTION,
1222             JOPTIONPANE_MESSAGETYPE, icon, options, initialValue, modal,
1223             buttons);
1224
1225     dialogParent.setAlwaysOnTop(false);
1226     dialogParent.dispose();
1227   }
1228
1229   /**
1230    * JalviewJS signals option selection by a property change event for the
1231    * option e.g. "OK". This methods responds to that by running the response
1232    * action that corresponds to that option.
1233    * 
1234    * @param evt
1235    */
1236   @Override
1237   public void propertyChange(PropertyChangeEvent evt)
1238   {
1239     Object newValue = evt.getNewValue();
1240     int ourOption = ourOptions.indexOf(newValue);
1241     if (ourOption >= 0)
1242     {
1243       handleResponse(ourOption);
1244     }
1245     else
1246     {
1247       // try our luck..
1248       handleResponse(newValue);
1249     }
1250   }
1251
1252   @Override
1253   public void handleResponse(Object response)
1254   {
1255     /*
1256      * this test is for NaN in Chrome
1257      */
1258     if (response != null && !response.equals(response))
1259     {
1260       return;
1261     }
1262     Callable<Void> action = callbacks.get(response);
1263     if (action != null)
1264     {
1265       try
1266       {
1267         action.call();
1268       } catch (Exception e)
1269       {
1270         e.printStackTrace();
1271       }
1272       if (parentComponent != null)
1273         parentComponent.requestFocus();
1274     }
1275   }
1276
1277   /**
1278    * Create a non-modal confirm dialog
1279    */
1280   public JDialog createDialog(Component parentComponent, Object message,
1281           String title, int optionType, int messageType, Icon icon,
1282           Object[] options, Object initialValue, boolean modal)
1283   {
1284     return createDialog(parentComponent, message, title, optionType,
1285             messageType, icon, options, initialValue, modal, null);
1286   }
1287
1288   public JDialog createDialog(Component parentComponent, Object message,
1289           String title, int optionType, int messageType, Icon icon,
1290           Object[] options, Object initialValue, boolean modal,
1291           JButton[] buttons)
1292   {
1293     if (!isInteractiveMode())
1294     {
1295       handleResponse(getMockResponse());
1296       return null;
1297     }
1298     JButton[] optionsButtons = null;
1299     Object initialValueButton = null;
1300     JOptionPane joptionpane = new JOptionPane();
1301     // Make button options
1302     int[] buttonActions = { JOptionPane.YES_OPTION, JOptionPane.NO_OPTION,
1303         JOptionPane.CANCEL_OPTION };
1304
1305     // we need the strings to make the buttons with actionEventListener
1306     if (options == null)
1307     {
1308       ArrayList<String> options_default = new ArrayList<>();
1309       options_default.add(UIManager.getString("OptionPane.yesButtonText"));
1310       if (optionType == JOptionPane.YES_NO_OPTION
1311               || optionType == JOptionPane.YES_NO_CANCEL_OPTION)
1312       {
1313         options_default.add(UIManager.getString("OptionPane.noButtonText"));
1314       }
1315       if (optionType == JOptionPane.YES_NO_CANCEL_OPTION)
1316       {
1317         options_default
1318                 .add(UIManager.getString("OptionPane.cancelButtonText"));
1319       }
1320       options = options_default.toArray();
1321     }
1322     if (!Platform.isJS()) // JalviewJS already uses callback, don't need to
1323                           // add them here
1324     {
1325       if (((optionType == JOptionPane.YES_OPTION
1326               || optionType == JOptionPane.NO_OPTION
1327               || optionType == JOptionPane.CANCEL_OPTION
1328               || optionType == JOptionPane.OK_OPTION
1329               || optionType == JOptionPane.DEFAULT_OPTION)
1330               && options.length < 1)
1331               || ((optionType == JOptionPane.YES_NO_OPTION
1332                       || optionType == JOptionPane.OK_CANCEL_OPTION)
1333                       && options.length < 2)
1334               || (optionType == JOptionPane.YES_NO_CANCEL_OPTION
1335                       && options.length < 3))
1336       {
1337         jalview.bin.Console
1338                 .debug("JvOptionPane: not enough options for dialog type");
1339       }
1340       optionsButtons = new JButton[options.length];
1341       for (int i = 0; i < options.length && i < 3; i++)
1342       {
1343         Object o = options[i];
1344         int buttonAction = buttonActions[i];
1345         Callable<Void> action = callbacks.get(buttonAction);
1346         JButton jb;
1347         if (buttons != null && buttons.length > i && buttons[i] != null)
1348         {
1349           jb = buttons[i];
1350         }
1351         else
1352         {
1353           jb = new JButton();
1354         }
1355         jb.setText((String) o);
1356         jb.addActionListener(new ActionListener()
1357         {
1358           @Override
1359           public void actionPerformed(ActionEvent e)
1360           {
1361             joptionpane.setValue(buttonAction);
1362             if (action != null)
1363               Executors.newSingleThreadExecutor().submit(action);
1364             // joptionpane.transferFocusBackward();
1365             joptionpane.transferFocusBackward();
1366             joptionpane.setVisible(false);
1367             // put focus and raise parent window if possible, unless cancel
1368             // button pressed
1369             boolean raiseParent = (parentComponent != null);
1370             if (buttonAction == JOptionPane.CANCEL_OPTION)
1371               raiseParent = false;
1372             if (optionType == JOptionPane.YES_NO_OPTION
1373                     && buttonAction == JOptionPane.NO_OPTION)
1374               raiseParent = false;
1375             if (raiseParent)
1376             {
1377               parentComponent.requestFocus();
1378               if (parentComponent instanceof JInternalFrame)
1379               {
1380                 JInternalFrame jif = (JInternalFrame) parentComponent;
1381                 jif.show();
1382                 jif.moveToFront();
1383                 jif.grabFocus();
1384               }
1385               else if (parentComponent instanceof Window)
1386               {
1387                 Window w = (Window) parentComponent;
1388                 w.toFront();
1389                 w.requestFocus();
1390               }
1391             }
1392             joptionpane.setVisible(false);
1393           }
1394         });
1395         optionsButtons[i] = jb;
1396         if (o.equals(initialValue))
1397           initialValueButton = jb;
1398       }
1399     }
1400     joptionpane.setMessage(message);
1401     joptionpane.setMessageType(messageType);
1402     joptionpane.setOptionType(optionType);
1403     joptionpane.setIcon(icon);
1404     joptionpane.setOptions(Platform.isJS() ? options : optionsButtons);
1405     joptionpane.setInitialValue(
1406             Platform.isJS() ? initialValue : initialValueButton);
1407
1408     JDialog dialog = joptionpane.createDialog(parentComponent, title);
1409     dialog.setIconImage(WindowIcons.logoIcon.getImage());
1410     dialog.setModalityType(
1411             modal ? ModalityType.APPLICATION_MODAL : ModalityType.MODELESS);
1412     dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
1413     return dialog;
1414   }
1415
1416   /**
1417    * Utility to programmatically click a button on a JOptionPane (as a JFrame)
1418    * 
1419    * returns true if button was found
1420    */
1421   public static boolean clickButton(JFrame frame, int buttonType)
1422   {
1423
1424     return false;
1425   }
1426
1427   /**
1428    * This helper method makes the JInternalFrame wait until it is notified by an
1429    * InternalFrameClosing event. This method also adds the given JOptionPane to
1430    * the JInternalFrame and sizes it according to the JInternalFrame's preferred
1431    * size.
1432    *
1433    * @param f
1434    *          The JInternalFrame to make modal.
1435    */
1436   private static void startModal(JInternalFrame f)
1437   {
1438     // We need to add an additional glasspane-like component directly
1439     // below the frame, which intercepts all mouse events that are not
1440     // directed at the frame itself.
1441     JPanel modalInterceptor = new JPanel();
1442     modalInterceptor.setOpaque(false);
1443     JLayeredPane lp = JLayeredPane.getLayeredPaneAbove(f);
1444     lp.setLayer(modalInterceptor, JLayeredPane.MODAL_LAYER.intValue());
1445     modalInterceptor.setBounds(0, 0, lp.getWidth(), lp.getHeight());
1446     modalInterceptor.addMouseListener(new MouseAdapter()
1447     {
1448     });
1449     modalInterceptor.addMouseMotionListener(new MouseMotionAdapter()
1450     {
1451     });
1452     lp.add(modalInterceptor);
1453     f.toFront();
1454
1455     // We need to explicitly dispatch events when we are blocking the event
1456     // dispatch thread.
1457     EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
1458     try
1459     {
1460       while (!f.isClosed())
1461       {
1462         if (EventQueue.isDispatchThread())
1463         {
1464           // The getNextEventMethod() issues wait() when no
1465           // event is available, so we don't need do explicitly wait().
1466           AWTEvent ev = queue.getNextEvent();
1467           // This mimics EventQueue.dispatchEvent(). We can't use
1468           // EventQueue.dispatchEvent() directly, because it is
1469           // protected, unfortunately.
1470           if (ev instanceof ActiveEvent)
1471             ((ActiveEvent) ev).dispatch();
1472           else if (ev.getSource() instanceof Component)
1473             ((Component) ev.getSource()).dispatchEvent(ev);
1474           else if (ev.getSource() instanceof MenuComponent)
1475             ((MenuComponent) ev.getSource()).dispatchEvent(ev);
1476           // Other events are ignored as per spec in
1477           // EventQueue.dispatchEvent
1478         }
1479         else
1480         {
1481           // Give other threads a chance to become active.
1482           Thread.yield();
1483         }
1484       }
1485     } catch (InterruptedException ex)
1486     {
1487       // If we get interrupted, then leave the modal state.
1488     } finally
1489     {
1490       // Clean up the modal interceptor.
1491       lp.remove(modalInterceptor);
1492
1493       // Remove the internal frame from its parent, so it is no longer
1494       // lurking around and clogging memory.
1495       Container parent = f.getParent();
1496       if (parent != null)
1497         parent.remove(f);
1498     }
1499   }
1500 }