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