From b865b46f6f34e82f5d10fccfe646f00b8ae2e7a1 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 9 Feb 2023 01:38:20 +0000 Subject: [PATCH] JAL-4125 Additions to JvOptionPane and QuitHandler with StructureViewerBase handling closed frame during quit differently. Still not working as dialog content does not appear until after quithandling thread has finished (it's blocking EDT). Need to break the doDesktopQuit Callable up into sections, that add another bit to the end when necessary. --- src/jalview/gui/Desktop.java | 18 ++++- src/jalview/gui/JvOptionPane.java | 118 +++++++++++++++++++++++++++--- src/jalview/gui/QuitHandler.java | 72 +++++++++++++----- src/jalview/gui/StructureViewerBase.java | 76 ++++++++++++++++++- 4 files changed, 249 insertions(+), 35 deletions(-) diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 6a67148..06afe81 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -1375,7 +1375,15 @@ public class Desktop extends jalview.jbgui.GDesktop if (jvnews != null) { storeLastKnownDimensions("JALVIEW_RSS_WINDOW_", jvnews.getBounds()); + } + + closeAll_actionPerformed(null); + // check for aborted quit + if (QuitHandler.quitCancelled()) + { + jalview.bin.Console.debug("Desktop aborting quit"); + return null; } if (dialogExecutor != null) @@ -1383,8 +1391,6 @@ public class Desktop extends jalview.jbgui.GDesktop dialogExecutor.shutdownNow(); } - closeAll_actionPerformed(null); - if (groovyConsole != null) { // suppress a possible repeat prompt to save script @@ -1545,11 +1551,17 @@ public class Desktop extends jalview.jbgui.GDesktop { // TODO show a progress bar while closing? JInternalFrame[] frames = desktop.getAllFrames(); + boolean quitting = QuitHandler.quitting(); for (int i = 0; i < frames.length; i++) { try { frames[i].setClosed(true); + // check for cancelled quit + if (quitting && QuitHandler.quitCancelled()) + { + return; + } } catch (java.beans.PropertyVetoException ex) { } @@ -2970,7 +2982,7 @@ public class Desktop extends jalview.jbgui.GDesktop /** * single thread that handles display of dialogs to user. */ - ExecutorService dialogExecutor = Executors.newSingleThreadExecutor(); + ExecutorService dialogExecutor = Executors.newFixedThreadPool(3); /** * flag indicating if dialogExecutor should try to acquire a permit diff --git a/src/jalview/gui/JvOptionPane.java b/src/jalview/gui/JvOptionPane.java index 0e0b13d..3c942ce 100644 --- a/src/jalview/gui/JvOptionPane.java +++ b/src/jalview/gui/JvOptionPane.java @@ -42,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.swing.Icon; @@ -57,6 +58,7 @@ import javax.swing.UIManager; import javax.swing.event.InternalFrameEvent; import javax.swing.event.InternalFrameListener; +import jalview.bin.Console; import jalview.util.ChannelProperties; import jalview.util.Platform; import jalview.util.dialogrunner.DialogRunnerI; @@ -72,6 +74,10 @@ public class JvOptionPane extends JOptionPane private Component parentComponent; + private ExecutorService executor = Executors.newCachedThreadPool(); + + private JDialog dialog = null; + private Map> callbacks = new HashMap<>(); /* @@ -878,7 +884,7 @@ public class JvOptionPane extends JOptionPane JOptionPane joptionpane = (JOptionPane) joptionpaneObject; joptionpane.setValue(buttonAction); if (action != null) - Executors.newSingleThreadExecutor().submit(action); + getExecutor().submit(action); joptionpane.transferFocusBackward(); joptionpane.setVisible(false); // put focus and raise parent window if possible, unless cancel or @@ -979,12 +985,13 @@ public class JvOptionPane extends JOptionPane jb.setText((String) o); jb.addActionListener(new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { joptionpane.setValue(buttonAction); if (action != null) - Executors.newSingleThreadExecutor().submit(action); + getExecutor().submit(action); // joptionpane.transferFocusBackward(); joptionpane.transferFocusBackward(); joptionpane.setVisible(false); @@ -1036,6 +1043,7 @@ public class JvOptionPane extends JOptionPane : ModalityType.MODELESS); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setVisible(true); + setDialog(dialog); } } @@ -1145,6 +1153,28 @@ public class JvOptionPane extends JOptionPane return this; } + public ExecutorService getExecutor() + { + if (executor == null) + executor = Executors.newSingleThreadExecutor(); + return executor; + } + + public void setExecutor(ExecutorService es) + { + executor = es; + } + + public void setDialog(JDialog d) + { + dialog = d; + } + + public JDialog getDialog() + { + return dialog; + } + /** * showDialogOnTop will create a dialog that (attempts to) come to top of OS * desktop windows @@ -1152,6 +1182,14 @@ public class JvOptionPane extends JOptionPane public static int showDialogOnTop(String label, String actionString, int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE) { + return showDialogOnTop(null, label, actionString, JOPTIONPANE_OPTION, + JOPTIONPANE_MESSAGETYPE); + } + + public static int showDialogOnTop(Component dialogParentComponent, + String label, String actionString, int JOPTIONPANE_OPTION, + int JOPTIONPANE_MESSAGETYPE) + { if (!isInteractiveMode()) { return (int) getMockResponse(); @@ -1166,14 +1204,23 @@ public class JvOptionPane extends JOptionPane // A better hack which works is to create a new JFrame parent with // setAlwaysOnTop(true) JFrame dialogParent = new JFrame(); - dialogParent.setIconImages(ChannelProperties.getIconList()); - dialogParent.setAlwaysOnTop(true); + if (dialogParentComponent == null) + { + dialogParent.setIconImages(ChannelProperties.getIconList()); + dialogParent.setAlwaysOnTop(true); + } - int answer = JOptionPane.showConfirmDialog(dialogParent, label, - actionString, JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE); + int answer = JOptionPane.showConfirmDialog( + dialogParentComponent == null ? dialogParent + : dialogParentComponent, + label, actionString, JOPTIONPANE_OPTION, + JOPTIONPANE_MESSAGETYPE); - dialogParent.setAlwaysOnTop(false); - dialogParent.dispose(); + if (dialogParentComponent == null) + { + dialogParent.setAlwaysOnTop(false); + dialogParent.dispose(); + } return answer; } @@ -1266,7 +1313,8 @@ public class JvOptionPane extends JOptionPane { try { - action.call(); + getExecutor().submit(action).get(); + // action.call(); } catch (Exception e) { e.printStackTrace(); @@ -1362,7 +1410,7 @@ public class JvOptionPane extends JOptionPane { joptionpane.setValue(buttonAction); if (action != null) - Executors.newSingleThreadExecutor().submit(action); + getExecutor().submit(action); // joptionpane.transferFocusBackward(); joptionpane.transferFocusBackward(); joptionpane.setVisible(false); @@ -1412,6 +1460,7 @@ public class JvOptionPane extends JOptionPane dialog.setModalityType( modal ? ModalityType.APPLICATION_MODAL : ModalityType.MODELESS); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setDialog(dialog); return dialog; } @@ -1499,4 +1548,53 @@ public class JvOptionPane extends JOptionPane parent.remove(f); } } + + public static JvOptionPane frameDialog(String message, String title, + int messageType, String[] buttonsText, String defaultButton, + Callable[] handlers, boolean modal) + { + JFrame parent = new JFrame(); + JvOptionPane jvop = JvOptionPane.newOptionDialog(); + JButton[] buttons = new JButton[buttonsText.length]; + for (int i = 0; i < buttonsText.length; i++) + { + buttons[i] = new JButton(); + buttons[i].setText(buttonsText[i]); + Console.debug("DISABLING BUTTON " + buttons[i].getText()); + buttons[i].setEnabled(false); + buttons[i].setVisible(false); + } + + int dialogType = -1; + if (buttonsText.length == 1) + { + dialogType = JOptionPane.OK_OPTION; + } + else if (buttonsText.length == 2) + { + dialogType = JOptionPane.YES_NO_OPTION; + } + else + { + dialogType = JOptionPane.YES_NO_CANCEL_OPTION; + } + jvop.setResponseHandler(JOptionPane.YES_OPTION, handlers[0]); + if (dialogType == JOptionPane.YES_NO_OPTION + || dialogType == JOptionPane.YES_NO_CANCEL_OPTION) + { + jvop.setResponseHandler(JOptionPane.NO_OPTION, handlers[1]); + } + if (dialogType == JOptionPane.YES_NO_CANCEL_OPTION) + { + jvop.setResponseHandler(JOptionPane.CANCEL_OPTION, handlers[2]); + } + + final int dt = dialogType; + jvop.getExecutor().execute(() -> { + jvop.showDialog(message, title, dt, messageType, null, buttonsText, + defaultButton, modal, buttons); + }); + + return jvop; + } } diff --git a/src/jalview/gui/QuitHandler.java b/src/jalview/gui/QuitHandler.java index 77eed81..ae12e7d 100644 --- a/src/jalview/gui/QuitHandler.java +++ b/src/jalview/gui/QuitHandler.java @@ -7,6 +7,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -145,22 +146,23 @@ public class QuitHandler if (confirmQuit) { - JvOptionPane.newOptionDialog() + setQuitDialog(JvOptionPane.newOptionDialog() .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit) - .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit) - .showDialogOnTopAsync( - new StringBuilder(MessageManager - .getString("label.quit_jalview")) - .append("\n") - .append(MessageManager - .getString("label.unsaved_changes")) - .toString(), - MessageManager.getString("action.quit"), - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, null, new Object[] - { MessageManager.getString("action.quit"), - MessageManager.getString("action.cancel") }, - MessageManager.getString("action.quit"), true); + .setResponseHandler(JOptionPane.NO_OPTION, cancelQuit)); + JvOptionPane qd = getQuitDialog(); + qd.showDialogOnTopAsync( + new StringBuilder( + MessageManager.getString("label.quit_jalview")) + .append("\n") + .append(MessageManager + .getString("label.unsaved_changes")) + .toString(), + MessageManager.getString("action.quit"), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, + new Object[] + { MessageManager.getString("action.quit"), + MessageManager.getString("action.cancel") }, + MessageManager.getString("action.quit"), true); } got = gotQuitResponse(); @@ -201,6 +203,13 @@ public class QuitHandler { executor.submit(next).get(); got = gotQuitResponse(); + } catch (RejectedExecutionException e) + { + // QuitHander.abortQuit() probably called + // CANCEL_QUIT test will reset QuitHandler + Console.info("Quit aborted!"); + got = QResponse.NULL; + setResponse(QResponse.NULL); } catch (InterruptedException | ExecutionException e) { jalview.bin.Console @@ -208,9 +217,10 @@ public class QuitHandler } setResponse(got); - if (gotQuitResponse() == QResponse.CANCEL_QUIT) + if (quitCancelled()) { // reset if cancelled + Console.debug("Quit cancelled"); setResponse(QResponse.NULL); return QResponse.CANCEL_QUIT; } @@ -305,8 +315,7 @@ public class QuitHandler } else { - if (!(QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT - || QuitHandler.gotQuitResponse() == QResponse.NULL)) + if (!(quitCancelled())) { for (int i = 0; i < buttons.length; i++) { @@ -406,6 +415,31 @@ public class QuitHandler public static void abortQuit() { - setResponse(QResponse.CANCEL_QUIT); + setResponse(QResponse.NULL); + // executor.shutdownNow(); + } + + private static JvOptionPane quitDialog = null; + + private static void setQuitDialog(JvOptionPane qd) + { + quitDialog = qd; + } + + private static JvOptionPane getQuitDialog() + { + return quitDialog; + } + + public static boolean quitCancelled() + { + return QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT + || QuitHandler.gotQuitResponse() == QResponse.NULL; + } + + public static boolean quitting() + { + return QuitHandler.gotQuitResponse() == QResponse.QUIT + || QuitHandler.gotQuitResponse() == QResponse.FORCE_QUIT; } } \ No newline at end of file diff --git a/src/jalview/gui/StructureViewerBase.java b/src/jalview/gui/StructureViewerBase.java index 1e12f7f..9a575ff 100644 --- a/src/jalview/gui/StructureViewerBase.java +++ b/src/jalview/gui/StructureViewerBase.java @@ -26,6 +26,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.beans.PropertyVetoException; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -36,6 +37,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Vector; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; @@ -1268,13 +1272,15 @@ public abstract class StructureViewerBase extends GStructureViewer if (!forceClose) { String viewerName = getViewerName(); + + int confirm = JvOptionPane.CANCEL_OPTION; String prompt = MessageManager .formatMessage("label.confirm_close_viewer", new Object[] { binding.getViewerTitle(viewerName, false), viewerName }); prompt = JvSwingUtils.wrapTooltip(true, prompt); - int confirm = JvOptionPane.showConfirmDialog(this, prompt, - MessageManager.getString("label.close_viewer"), - JvOptionPane.YES_NO_CANCEL_OPTION); + String title = MessageManager.getString("label.close_viewer"); + confirm = showCloseDialog(title, prompt); + /* * abort closure if user hits escape or Cancel */ @@ -1283,7 +1289,16 @@ public abstract class StructureViewerBase extends GStructureViewer { // abort possible quit handling if CANCEL chosen if (confirm == JvOptionPane.CANCEL_OPTION) + { + try + { + // this is a bit futile + this.setClosed(false); + } catch (PropertyVetoException e) + { + } QuitHandler.abortQuit(); + } return; } forceClose = confirm == JvOptionPane.YES_OPTION; @@ -1303,6 +1318,59 @@ public abstract class StructureViewerBase extends GStructureViewer dispose(); } + private int showCloseDialog(final String title, final String prompt) + { + confirmResponse = JvOptionPane.CANCEL_OPTION; + + if (QuitHandler.quitting()) + { + + Callable yesCall = () -> { + Console.debug("YES"); + confirmResponse = JvOptionPane.YES_OPTION; + return null; + }; + Callable noCall = () -> { + Console.debug("NO"); + confirmResponse = JvOptionPane.NO_OPTION; + return null; + }; + Callable cancelCall = () -> { + Console.debug("CANCEL"); + confirmResponse = JvOptionPane.CANCEL_OPTION; + return null; + }; + Callable[] calls = new Callable[] { yesCall, noCall, + cancelCall }; + String cancelQuit = MessageManager.getString("action.cancel_quit"); + String[] buttonsText = { MessageManager.getString("action.yes"), + MessageManager.getString("action.no"), cancelQuit }; + JvOptionPane dialog = JvOptionPane.frameDialog(prompt, + MessageManager.getString("label.close_viewer"), + JvOptionPane.WARNING_MESSAGE, buttonsText, cancelQuit, calls, + false); + // wait for response + ExecutorService executor = dialog.getExecutor(); + executor.shutdown(); + try + { + Console.debug("### executor.awaitTermination() starting"); + executor.awaitTermination(60, TimeUnit.SECONDS); + Console.debug("### executor.awaitTermination() finished"); + } catch (InterruptedException e) + { + } + } + else + { + confirmResponse = JvOptionPane.showConfirmDialog(this, prompt, + MessageManager.getString("label.close_viewer"), + JvOptionPane.YES_NO_CANCEL_OPTION, + JvOptionPane.WARNING_MESSAGE); + } + return confirmResponse; + } + @Override public void showHelp_actionPerformed() { @@ -1333,4 +1401,6 @@ public abstract class StructureViewerBase extends GStructureViewer && viewerActionMenu.getItemCount() > 0 && viewerActionMenu.isVisible(); } + + private static int confirmResponse = 0; } -- 1.7.10.2