JAL-1988 JAL-3772 Non-blocking modal dialogs for unsaved changes and saving files...
[jalview.git] / src / jalview / jbgui / QuitHandler.java
index 6a95db7..6369b1a 100644 (file)
@@ -1,8 +1,12 @@
 package jalview.jbgui;
 
 import java.io.File;
-import java.util.Date;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import javax.swing.JFrame;
 import javax.swing.JOptionPane;
@@ -10,11 +14,13 @@ import javax.swing.JOptionPane;
 import com.formdev.flatlaf.extras.FlatDesktop;
 
 import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
 import jalview.bin.Console;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
 import jalview.io.BackupFiles;
 import jalview.project.Jalview2XML;
 import jalview.util.MessageManager;
@@ -22,37 +28,48 @@ import jalview.util.Platform;
 
 public class QuitHandler
 {
+  private static final int INITIAL_WAIT_FOR_SAVE = 3000;
+
+  private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
+
   public static enum QResponse
   {
-    QUIT, CANCEL_QUIT, FORCE_QUIT
+    NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
   };
 
-  public static void setQuitHandler()
+  private static ExecutorService executor = Executors.newFixedThreadPool(3);
+
+  public static QResponse setQuitHandler()
   {
     FlatDesktop.setQuitHandler(response -> {
-      QResponse qresponse = getQuitResponse();
-      switch (qresponse)
-      {
-      case QUIT:
+      Callable<QResponse> performQuit = () -> {
         response.performQuit();
-        break;
-      case CANCEL_QUIT:
-        response.cancelQuit();
-        break;
-      case FORCE_QUIT:
+        return setResponse(QResponse.QUIT);
+      };
+      Callable<QResponse> performForceQuit = () -> {
         response.performQuit();
-        break;
-      default:
+        return setResponse(QResponse.FORCE_QUIT);
+      };
+      Callable<QResponse> cancelQuit = () -> {
         response.cancelQuit();
-      }
+        // reset
+        setResponse(QResponse.NULL);
+        // but return cancel
+        return QResponse.CANCEL_QUIT;
+      };
+      QResponse qresponse = getQuitResponse(true, performQuit,
+              performForceQuit, cancelQuit);
     });
+
+    return gotQuitResponse();
   }
 
-  private static QResponse gotQuitResponse = QResponse.CANCEL_QUIT;
+  private static QResponse gotQuitResponse = QResponse.NULL;
 
-  private static QResponse returnResponse(QResponse qresponse)
+  private static QResponse setResponse(QResponse qresponse)
   {
     gotQuitResponse = qresponse;
+    Console.debug("##### Setting gotQuitResponse to " + qresponse);
     return qresponse;
   }
 
@@ -61,19 +78,47 @@ public class QuitHandler
     return gotQuitResponse;
   }
 
-  public static QResponse getQuitResponse()
+  public static final Callable<QResponse> defaultCancelQuit = () -> {
+    Console.debug("QuitHandler: (default) Quit action CANCELLED by user");
+    // reset
+    setResponse(QResponse.NULL);
+    // and return cancel
+    return QResponse.CANCEL_QUIT;
+  };
+
+  public static final Callable<QResponse> defaultOkQuit = () -> {
+    Console.debug("QuitHandler: (default) Quit action CONFIRMED by user");
+    return setResponse(QResponse.QUIT);
+  };
+
+  public static final Callable<QResponse> defaultForceQuit = () -> {
+    Console.debug("QuitHandler: (default) Quit action FORCED by user");
+    // note that shutdown hook will not be run
+    Runtime.getRuntime().halt(0);
+    return setResponse(QResponse.FORCE_QUIT); // this line never reached!
+  };
+
+  public static QResponse getQuitResponse(boolean ui)
   {
-    return getQuitResponse(true);
+    return getQuitResponse(ui, defaultOkQuit, defaultForceQuit,
+            defaultCancelQuit);
   }
 
-  public static QResponse getQuitResponse(boolean ui)
+  private static boolean interactive = true;
+
+  public static QResponse getQuitResponse(boolean ui,
+          Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
+          Callable<QResponse> cancelQuit)
   {
-    if (gotQuitResponse() != QResponse.CANCEL_QUIT)
+    QResponse got = gotQuitResponse();
+    if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
     {
-      return returnResponse(getQuitResponse());
+      // quit has already been selected, continue with calling quit method
+      Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
+      return got;
     }
 
-    boolean interactive = ui && !Platform.isHeadless();
+    interactive = ui && !Platform.isHeadless();
     // confirm quit if needed and wanted
     boolean confirmQuit = true;
 
@@ -96,45 +141,112 @@ public class QuitHandler
               + "' is/defaults to " + confirmQuit + " -- "
               + (confirmQuit ? "" : "not ") + "confirming quit");
     }
+    got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
+    Console.debug("initial calculation, got=" + got);
+    setResponse(got);
 
-    int answer = JOptionPane.OK_OPTION;
-
-    // if going to confirm, do it before the save in progress check to give
-    // the save time to finish!
     if (confirmQuit)
     {
-      answer = frameOnTop(
+
+      Console.debug("********************ABOUT TO CONFIRM QUIT");
+      JvOptionPane quitDialog = JvOptionPane.newOptionDialog()
+              .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
+              .setResponseHandler(JOptionPane.NO_OPTION, defaultCancelQuit);
+      quitDialog.showDialogOnTopAsync(
               new StringBuilder(
                       MessageManager.getString("label.quit_jalview"))
-                              .append(" ")
+                              .append("\n")
                               .append(MessageManager
                                       .getString("label.unsaved_changes"))
                               .toString(),
               MessageManager.getString("action.quit"),
-              JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+              JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
+              new Object[]
+              { MessageManager.getString("action.quit"),
+                  MessageManager.getString("action.cancel") },
+              MessageManager.getString("action.quit"), true);
     }
 
-    if (answer == JOptionPane.CANCEL_OPTION)
+    got = gotQuitResponse();
+    Console.debug("first user response, got=" + got);
+    boolean wait = false;
+    if (got == QResponse.CANCEL_QUIT)
     {
-      Console.debug("QuitHandler: Quit action cancelled by user");
-      return returnResponse(QResponse.CANCEL_QUIT);
+      // reset
+      setResponse(QResponse.NULL);
+      // but return cancel
+      return QResponse.CANCEL_QUIT;
+    }
+    else if (got == QResponse.QUIT)
+    {
+      if (Cache.getDefault("WAIT_FOR_SAVE", true)
+              && BackupFiles.hasSavesInProgress())
+      {
+        /*
+        Future<QResponse> waitGot = executor.submit(waitQuitCall);
+        try
+        {
+          got = waitGot.get();
+        } catch (InterruptedException | ExecutionException e)
+        {
+          jalview.bin.Console.debug(
+                  "Exception during quit handling (wait for save)", e);
+        }
+        */
+        QResponse waitResponse = waitQuit(interactive, okQuit, forceQuit,
+                cancelQuit);
+        wait = waitResponse == QResponse.QUIT;
+      }
     }
 
-    // check for saves in progress
-    int waitForSave = 1000; // MAKE THIS BETTER
-    AlignFrame[] afArray = Desktop.getAlignFrames();
-    if (afArray == null || afArray.length == 0)
+    Callable<QResponse> next = null;
+    switch (gotQuitResponse())
     {
-      // no change
+    case QUIT:
+      Console.debug("### User selected QUIT");
+      next = okQuit;
+      break;
+    case FORCE_QUIT: // not actually an option at this stage
+      Console.debug("### User selected FORCE QUIT");
+      next = forceQuit;
+      break;
+    default:
+      Console.debug("### User selected CANCEL QUIT");
+      next = cancelQuit;
+      break;
     }
-    else
+    try
+    {
+      got = executor.submit(next).get();
+    } catch (InterruptedException | ExecutionException e)
+    {
+      jalview.bin.Console
+              .debug("Exception during quit handling (final choice)", e);
+    }
+    jalview.bin.Console.debug("### nextResponse=" + got.toString());
+    setResponse(got);
+
+    return gotQuitResponse();
+  }
+
+  private static QResponse waitQuit(boolean interactive,
+          Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
+          Callable<QResponse> cancelQuit)
+  {
+    jalview.bin.Console.debug("#### waitQuit started");
+    // check for saves in progress
+    if (!BackupFiles.hasSavesInProgress())
+      return QResponse.QUIT;
+
+    int waitTime = INITIAL_WAIT_FOR_SAVE; // start with 3 second wait
+    AlignFrame[] afArray = Desktop.getAlignFrames();
+    if (!(afArray == null || afArray.length == 0))
     {
       int size = 0;
       for (int i = 0; i < afArray.length; i++)
       {
         AlignFrame af = afArray[i];
-        List<AlignmentViewPanel> avpList = (List<AlignmentViewPanel>) af
-                .getAlignPanels();
+        List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
         for (AlignmentViewPanel avp : avpList)
         {
           AlignmentI a = avp.getAlignment();
@@ -145,101 +257,174 @@ public class QuitHandler
           }
         }
       }
-      waitForSave = size;
-      Console.debug("Set waitForSave to " + waitForSave);
+      waitTime = Math.max(waitTime, size / 2);
+      Console.debug("Set waitForSave to " + waitTime);
     }
-    int waitIncrement = 3000;
-    long startTime = new Date().getTime();
-    boolean saving = BackupFiles.hasSavesInProgress();
-    if (saving)
+
+    // future that returns a Boolean when all files are saved
+    CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
+
+    // callback as each file finishes saving
+    for (CompletableFuture<Boolean> cf : BackupFiles
+            .savesInProgressCompletableFutures())
+    {
+      // if this is the last one then complete filesAllSaved
+      cf.whenComplete((ret, e) -> {
+        if (!BackupFiles.hasSavesInProgress())
+        {
+          filesAllSaved.complete(true);
+        }
+      });
+    }
+
+    final int waitTimeFinal = waitTime;
+    // timeout the wait -- will result in another wait button when looped
+    CompletableFuture<Boolean> waitTimeout = CompletableFuture
+            .supplyAsync(() -> {
+              executor.submit(() -> {
+                try
+                {
+                  Thread.sleep(waitTimeFinal);
+                } catch (InterruptedException e)
+                {
+                  // probably interrupted by all files saving
+                }
+              });
+              return true;
+            });
+    CompletableFuture<Object> waitForSave = CompletableFuture
+            .anyOf(waitTimeout, filesAllSaved);
+    Console.debug("##### WAITFORSAVE RUNNING");
+
+    QResponse waitResponse = QResponse.NULL;
+
+    int iteration = 0;
+    boolean doIterations = true;
+    while (doIterations && BackupFiles.hasSavesInProgress()
+            && iteration++ < (interactive ? 100 : 5))
     {
-      boolean waiting = (new Date().getTime() - startTime) < waitForSave;
-      while (saving && waiting)
+      try
       {
-        saving = !waitForSave(waitIncrement);
-        waiting = (new Date().getTime() - startTime) < waitForSave;
+        waitForSave.get();
+      } catch (InterruptedException | ExecutionException e1)
+      {
+        Console.debug(
+                "Exception whilst waiting for files to save before quitting",
+                e1);
       }
-
-      if (saving) // still saving after a wait
+      if (interactive)
       {
-        StringBuilder messageSB = new StringBuilder(
-                MessageManager.getString("label.save_in_progress"));
-        messageSB.append(":");
-        boolean any = false;
-        for (File file : BackupFiles.savesInProgressFiles())
+        Console.debug("********************About to make waitDialog");
+        JvOptionPane waitDialog = JvOptionPane.newOptionDialog()
+                .setResponseHandler(JOptionPane.YES_OPTION, defaultOkQuit)
+                .setResponseHandler(JOptionPane.NO_OPTION, forceQuit)
+                .setResponseHandler(JOptionPane.CANCEL_OPTION, cancelQuit);
+
+        // callback as each file finishes saving
+        for (CompletableFuture<Boolean> cf : BackupFiles
+                .savesInProgressCompletableFutures())
         {
-          messageSB.append("\n- ");
-          messageSB.append(file.getName());
-          any = true;
+          // update the list of saving files as they save too
+          cf.thenRun(() -> {
+            waitDialog.setMessage(waitingForSaveMessage());
+          });
+          // if this is the last one then close the dialog
+          cf.whenComplete((ret, e) -> {
+            if (!BackupFiles.hasSavesInProgress())
+            {
+              // like a click on Wait button
+              Console.debug(
+                      "***** TRYING TO MAKE THE WAIT FOR SAVE DIALOG DISAPPEAR!");
+              waitDialog.setValue(JOptionPane.YES_OPTION);
+            }
+          });
         }
-        if (!any)
-        {
-          messageSB.append("\n");
-          messageSB.append(MessageManager.getString("label.unknown"));
-        }
-        int waitLonger = interactive ? JOptionPane.YES_OPTION
-                : JOptionPane.NO_OPTION;
-        while (saving && waitLonger == JOptionPane.YES_OPTION)
+
+        waitDialog.showDialogOnTopAsync(waitingForSaveMessage(),
+                MessageManager.getString("action.wait"),
+                JOptionPane.YES_NO_CANCEL_OPTION,
+                JOptionPane.WARNING_MESSAGE, null, new Object[]
+                { MessageManager.getString("action.wait"),
+                    MessageManager.getString("action.force_quit"),
+                    MessageManager.getString("action.cancel_quit") },
+                MessageManager.getString("action.wait"), true);
+        Console.debug("********************Finished waitDialog");
+
+        waitResponse = gotQuitResponse();
+        Console.debug("####### WAITFORSAVE SET: " + waitResponse);
+        switch (waitResponse)
         {
-          waitLonger = waitForceQuitCancelQuitOptionDialog(
-                  messageSB.toString(),
-                  MessageManager.getString("action.wait"));
-          if (waitLonger == JOptionPane.YES_OPTION) // wait
-          {
-            // do wait stuff
-            saving = !waitForSave(waitIncrement);
-          }
-          else if (waitLonger == JOptionPane.NO_OPTION) // force quit
-          {
-            // do a force quit
-            return returnResponse(QResponse.FORCE_QUIT); // shouldn't reach this
-          }
-          else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
-          {
-            return returnResponse(QResponse.CANCEL_QUIT);
-          }
-          else
-          {
-            Console.debug("**** Shouldn't have got here!");
-          }
+        case QUIT: // wait -- do another iteration
+          break;
+        case FORCE_QUIT:
+          doIterations = false;
+          break;
+        case CANCEL_QUIT:
+          doIterations = false;
+          break;
+        case NULL: // already cancelled
+          doIterations = false;
+          break;
+        default:
         }
-      }
-    }
+      } // end if interactive
 
-    // not cancelled and not saving
-    return returnResponse(QResponse.QUIT);
-  }
+    }
+    waitResponse = gotQuitResponse();
 
-  public static int frameOnTop(String label, String actionString,
-          int JOPTIONPANE_OPTION, int JOPTIONPANE_MESSAGETYPE)
-  {
-    return frameOnTop(new JFrame(), label, actionString, JOPTIONPANE_OPTION,
-            JOPTIONPANE_MESSAGETYPE);
-  }
+    Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
+    return waitResponse;
+  };
 
-  public static int frameOnTop(JFrame dialogParent, String label,
-          String actionString, int JOPTIONPANE_OPTION,
-          int JOPTIONPANE_MESSAGETYPE)
+  public static void okk()
   {
-    // ensure Jalview window is brought to front for Quit confirmation
-    // window to be visible
-
-    // this method of raising the Jalview window is broken in java
-    // jalviewDesktop.setVisible(true);
-    // jalviewDesktop.toFront();
-
-    // a better hack which works instead
-
-    dialogParent.setAlwaysOnTop(true);
-
-    int answer = JOptionPane.showConfirmDialog(dialogParent, label,
-            actionString, JOPTIONPANE_OPTION, JOPTIONPANE_MESSAGETYPE);
-
-    dialogParent.setAlwaysOnTop(false);
-    dialogParent.dispose();
-
-    return answer;
-  }
+    /*
+    if (false)
+    {
+      if (false)
+      {
+    
+        waitLonger = JOptionPane.showOptionDialog(dialogParent,
+                waitingForSaveMessage(),
+                MessageManager.getString("action.wait"),
+                JOptionPane.YES_NO_CANCEL_OPTION,
+                JOptionPane.WARNING_MESSAGE, null, options, wait);
+      }
+      else
+      {
+        // non-interactive
+        waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
+                ? JOptionPane.YES_OPTION
+                : JOptionPane.NO_OPTION;
+      }
+    
+      if (waitLonger == JOptionPane.YES_OPTION) // "wait"
+      {
+        saving = !waitForSave(waitIncrement);
+      }
+      else if (waitLonger == JOptionPane.NO_OPTION) // "force
+      // quit"
+      {
+        // do a force quit
+        return setResponse(QResponse.FORCE_QUIT);
+      }
+      else if (waitLonger == JOptionPane.CANCEL_OPTION) // cancel quit
+      {
+        return setResponse(QResponse.CANCEL_QUIT);
+      }
+      else
+      {
+        // Most likely got here by user dismissing the dialog with the
+        // 'x'
+        // -- treat as a "Cancel"
+        return setResponse(QResponse.CANCEL_QUIT);
+      }
+    }
+    
+    // not sure how we got here, best be safe
+    return QResponse.CANCEL_QUIT;
+    */
+  };
 
   private static int waitForceQuitCancelQuitOptionDialog(Object message,
           String title)
@@ -251,14 +436,38 @@ public class QuitHandler
         MessageManager.getString("action.force_quit"),
         MessageManager.getString("action.cancel_quit") };
 
+    // BackupFiles.setWaitForSaveDialog(dialogParent);
+
     int answer = JOptionPane.showOptionDialog(dialogParent, message, title,
             JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE,
             null, options, wait);
 
+    // BackupFiles.clearWaitForSaveDialog();
+
     return answer;
   }
 
-  private static boolean waitForSave(long t)
+  private static String waitingForSaveMessage()
+  {
+    StringBuilder messageSB = new StringBuilder(
+            MessageManager.getString("label.save_in_progress"));
+    boolean any = false;
+    for (File file : BackupFiles.savesInProgressFiles())
+    {
+      messageSB.append("\n- ");
+      messageSB.append(file.getName());
+      any = true;
+    }
+    if (!any)
+    {
+      messageSB.append("\n");
+      messageSB.append(MessageManager.getString("label.unknown"));
+    }
+
+    return messageSB.toString();
+  }
+
+  private static Boolean waitForSave(long t)
   {
     boolean ret = false;
     try