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;
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;
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;
}
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;
+ "' 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();
}
}
}
- 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)
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