JAL-1988 JAL-3772 wait timer working first time but blocking desktop gui updates
[jalview.git] / src / jalview / jbgui / QuitHandler.java
index 56e23aa..db8b500 100644 (file)
 package jalview.jbgui;
 
+import java.io.File;
+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;
 
 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;
+import jalview.util.Platform;
 
 public class QuitHandler
 {
-  public static void setQuitHandler()
+  private static final int INITIAL_WAIT_FOR_SAVE = 3000;
+
+  private static final int NON_INTERACTIVE_WAIT_CYCLES = 2;
+
+  public static enum QResponse
+  {
+    NULL, QUIT, CANCEL_QUIT, FORCE_QUIT
+  };
+
+  private static ExecutorService executor = Executors.newFixedThreadPool(3);
+
+  public static QResponse setQuitHandler()
   {
     FlatDesktop.setQuitHandler(response -> {
-      boolean confirmQuit = jalview.bin.Cache
+      Callable<QResponse> performQuit = () -> {
+        response.performQuit();
+        return setResponse(QResponse.QUIT);
+      };
+      Callable<QResponse> performForceQuit = () -> {
+        response.performQuit();
+        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.NULL;
+
+  private static QResponse setResponse(QResponse qresponse)
+  {
+    gotQuitResponse = qresponse;
+    Console.debug("##### Setting gotQuitResponse to " + qresponse);
+    return qresponse;
+  }
+
+  public static QResponse gotQuitResponse()
+  {
+    return gotQuitResponse;
+  }
+
+  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(ui, defaultOkQuit, defaultForceQuit,
+            defaultCancelQuit);
+  }
+
+  private static boolean interactive = true;
+
+  public static QResponse getQuitResponse(boolean ui,
+          Callable<QResponse> okQuit, Callable<QResponse> forceQuit,
+          Callable<QResponse> cancelQuit)
+  {
+    QResponse got = gotQuitResponse();
+    if (got != QResponse.NULL && got != QResponse.CANCEL_QUIT)
+    {
+      // quit has already been selected, continue with calling quit method
+      Console.debug("##### getQuitResponse called. gotQuitResponse=" + got);
+      return got;
+    }
+
+    interactive = ui && !Platform.isHeadless();
+    // confirm quit if needed and wanted
+    boolean confirmQuit = true;
+
+    if (!interactive)
+    {
+      Console.debug("Non interactive quit -- not confirming");
+      confirmQuit = false;
+    }
+    else if (Jalview2XML.allSavedUpToDate())
+    {
+      Console.debug("Nothing changed -- not confirming quit");
+      confirmQuit = false;
+    }
+    else
+    {
+      confirmQuit = jalview.bin.Cache
               .getDefault(jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT, true);
-      boolean canQuit = !confirmQuit;
-      int n;
-      if (confirmQuit)
+      Console.debug("Jalview property '"
+              + jalview.gui.Desktop.CONFIRM_KEYBOARD_QUIT
+              + "' is/defaults to " + confirmQuit + " -- "
+              + (confirmQuit ? "" : "not ") + "confirming quit");
+    }
+    got = confirmQuit ? QResponse.NULL : QResponse.QUIT;
+    Console.debug("initial calculation, got=" + got);
+    setResponse(got);
+
+    if (confirmQuit)
+    {
+
+      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("\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();
+    Console.debug("first user response, got=" + got);
+    boolean wait = false;
+    if (got == 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;
+      }
+    }
+
+    Callable<QResponse> next = null;
+    switch (gotQuitResponse())
+    {
+    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;
+    }
+    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++)
       {
-        // ensure Jalview window is brought to front for Quit confirmation
-        // window to be visible
+        AlignFrame af = afArray[i];
+        List<? extends AlignmentViewPanel> avpList = af.getAlignPanels();
+        for (AlignmentViewPanel avp : avpList)
+        {
+          AlignmentI a = avp.getAlignment();
+          List<SequenceI> sList = a.getSequences();
+          for (SequenceI s : sList)
+          {
+            size += s.getLength();
+          }
+        }
+      }
+      waitTime = Math.max(waitTime, size / 2);
+      Console.debug("Set waitForSave to " + waitTime);
+    }
+    final int waitTimeFinal = waitTime;
+    QResponse waitResponse = QResponse.NULL;
 
-        // this method of raising the Jalview window is broken in java
-        // jalviewDesktop.setVisible(true);
-        // jalviewDesktop.toFront();
+    // future that returns a Boolean when all files are saved
+    CompletableFuture<Boolean> filesAllSaved = new CompletableFuture<>();
 
-        // a better hack which works instead
-        JFrame dialogParent = new JFrame();
-        dialogParent.setAlwaysOnTop(true);
+    // 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);
+        }
+      });
+    }
 
-        n = JOptionPane.showConfirmDialog(dialogParent,
-                MessageManager.getString("label.quit_jalview"),
-                MessageManager.getString("action.quit"),
-                JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
-                null);
+    // timeout the wait -- will result in another wait button when looped
+    CompletableFuture<Boolean> waitTimeout = CompletableFuture
+            .supplyAsync(() -> {
+              Console.debug("################# STARTING WAIT SLEEP");
+              try
+              {
+                Thread.sleep(waitTimeFinal);
+              } catch (InterruptedException e)
+              {
+                // probably interrupted by all files saving
+              }
+              return true;
+            });
+    CompletableFuture<Object> waitForSave = CompletableFuture
+            .anyOf(waitTimeout, filesAllSaved);
 
-        dialogParent.setAlwaysOnTop(false);
-        dialogParent.dispose();
+    int iteration = 0;
+    boolean doIterations = true;
+    while (doIterations && BackupFiles.hasSavesInProgress()
+            && iteration++ < (interactive ? 100 : 5))
+    {
+      try
+      {
+        waitForSave.copy().get();
+      } catch (InterruptedException | ExecutionException e1)
+      {
+        Console.debug(
+                "Exception whilst waiting for files to save before quit",
+                e1);
+      }
+      if (interactive && BackupFiles.hasSavesInProgress())
+      {
+        Console.debug("********************About to make waitDialog");
+        JFrame parent = new JFrame();
+        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())
+        {
+          cf.whenComplete((ret, e) -> {
+            Console.debug("############# A FILE SAVED!");
+            // update the list of saving files as they save too
+            waitDialog.setMessage(waitingForSaveMessage());
+            waitDialog.setName("AAARGH!");
+            // if this is the last one then close the dialog
+            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);
+              parent.dispose();
+            }
+          });
+        }
+
+        waitDialog.showDialogOnTopAsync(parent, 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");
+
+        final QResponse thisWaitResponse = gotQuitResponse();
+        Console.debug("####### WAITFORSAVE SET: " + thisWaitResponse);
+        switch (thisWaitResponse)
+        {
+        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
+
+    } // end while wait iteration loop
+    waitResponse = gotQuitResponse();
+
+    Console.debug("####### WAITFORSAVE RETURNING: " + waitResponse);
+    return waitResponse;
+  };
+
+  public static void okk()
+  {
+    /*
+    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
       {
-        n = JOptionPane.OK_OPTION;
+        // non-interactive
+        waitLonger = iteration < NON_INTERACTIVE_WAIT_CYCLES
+                ? JOptionPane.YES_OPTION
+                : JOptionPane.NO_OPTION;
       }
-      canQuit = (n == JOptionPane.OK_OPTION);
-      if (canQuit)
+    
+      if (waitLonger == JOptionPane.YES_OPTION) // "wait"
       {
-        response.performQuit();
+        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
       {
-        response.cancelQuit();
+        // 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)
+  {
+    JFrame dialogParent = new JFrame();
+    dialogParent.setAlwaysOnTop(true);
+    String wait = MessageManager.getString("action.wait");
+    Object[] options = { wait,
+        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 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
+    {
+      Console.debug("Wait for save to complete: " + t + "ms");
+      long c = 0;
+      int i = 100;
+      while (c < t)
+      {
+        Thread.sleep(i);
+        c += i;
+        ret = !BackupFiles.hasSavesInProgress();
+        if (ret)
+        {
+          Console.debug(
+                  "Save completed whilst waiting (" + c + "/" + t + "ms)");
+          return ret;
+        }
+        if (c % 1000 < i) // just gone over another second
+        {
+          Console.debug("...waiting (" + c + "/" + t + "ms]");
+        }
+      }
+    } catch (InterruptedException e)
+    {
+      Console.debug("Wait for save interrupted");
+    }
+    Console.debug("Save has " + (ret ? "" : "not ") + "completed");
+    return ret;
   }
 
 }