/******************************************************************************* * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.egit.ui.internal.dialogs; import java.lang.reflect.InvocationTargetException; import java.text.MessageFormat; import java.util.LinkedList; import java.util.Queue; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IProgressMonitorWithBlocking; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ProgressMonitorWrapper; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.wizard.IWizard; import org.eclipse.jface.wizard.IWizardPage; import org.eclipse.jface.wizard.ProgressMonitorPart; import org.eclipse.jface.wizard.WizardDialog; import org.eclipse.swt.SWTException; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Layout; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; /** * A special-purpose {@link WizardDialog} whose * {@link #run(boolean, boolean, IRunnableWithProgress) run()} method really * runs the job in the background. Use this mechanism with care! */ public class NonBlockingWizardDialog extends WizardDialog { private Queue<BackgroundJob> jobs = new LinkedList<>(); /** * Creates a new {@link NonBlockingWizardDialog}. * * @param parentShell * for the dialog * @param newWizard * to show in the dialog */ public NonBlockingWizardDialog(Shell parentShell, IWizard newWizard) { super(parentShell, newWizard); } @Override protected Control createContents(Composite parent) { parent.addDisposeListener(e -> cancelJobs()); addPageChangedListener(e -> cancelJobs()); return super.createContents(parent); } /** * If {@code fork} is {@code true}, this implementation does <em>not</em> * block but schedules a true background job. Such background jobs are * queued and will execute one after another. They are canceled when the * current wizard page changes, or when the dialog closes. * </p> */ @Override public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException { if (!fork) { super.run(fork, cancelable, runnable); } run(runnable, null); } /** * Runs the given {@code runnable} in a background job, reporting progress * through the dialog's progress monitor, if any, and invoking * {@code onCancel} if the job is canceled. * * @param runnable * to run * @param onCancel * to run when the job is canceled; may be {@code null} */ public void run(IRunnableWithProgress runnable, Runnable onCancel) { Assert.isNotNull(runnable); synchronized (jobs) { BackgroundJob newJob = new BackgroundJob(runnable, onCancel, getCurrentPage()); jobs.add(newJob); if (jobs.size() == 1) { newJob.schedule(); } } } /** * Cancels any currently scheduled background jobs. */ public void cancelJobs() { Job currentJob; synchronized (jobs) { currentJob = jobs.peek(); jobs.clear(); } if (currentJob != null) { currentJob.cancel(); } } @Override public void showPage(IWizardPage page) { // Need to synchronize to avoid race condition with a background job // terminating. synchronized (jobs) { super.showPage(page); } } @Override public IWizardPage getCurrentPage() { synchronized (jobs) { return super.getCurrentPage(); } } @Override protected final ProgressMonitorPart createProgressMonitorPart( Composite composite, GridLayout pmlayout) { return new BackgroundProgressMonitorPart(composite, pmlayout); } private void restoreFocus(Control focusControl) { if (focusControl != null && !focusControl.isDisposed()) { Shell shell = getShell(); if (shell != null && !shell.isDisposed() && focusControl.getShell() == shell) { focusControl.setFocus(); } } } private class BackgroundProgressMonitorPart extends ProgressMonitorPart { private Job job; public BackgroundProgressMonitorPart(Composite parent, Layout layout) { super(parent, layout, true); } public void setJob(Job job) { this.job = job; } @Override public void beginTask(String name, int totalWork) { // Super implementation steals the focus and sets it to the // monitor part's stop button. Display display = this.getDisplay(); Control focusControl = display.isDisposed() ? null : display.getFocusControl(); super.beginTask(name, totalWork); restoreFocus(focusControl); } @Override public void setCanceled(boolean cancel) { super.setCanceled(cancel); if (cancel) { if (job != null) { job.cancel(); } } } @Override public void done() { job = null; super.done(); } } /** * Copied from org.eclipse.jface.operation.AccumulatingProgressMonitor and * made {@link #isCanceled()} also consider the progress monitor provided by * the Job framework. Also handle disposal in {@link #done()}. * <p> * The resulting monitor can be used from any thread; progress reporting to * the wrapped monitor will occur asynchronously in the UI thread. */ private static class ForwardingProgressMonitor extends ProgressMonitorWrapper { private Display display; private Collector collector; private IProgressMonitor jobMonitor; private String currentTask = ""; //$NON-NLS-1$ private class Collector implements Runnable { private String taskName; private String subTask; private double worked; private IProgressMonitor monitor; public Collector(String taskName, String subTask, double work, IProgressMonitor monitor) { this.taskName = taskName; this.subTask = subTask; this.worked = work; this.monitor = monitor; } public void setTaskName(String name) { this.taskName = name; } public void worked(double workedIncrement) { this.worked = this.worked + workedIncrement; } public void subTask(String subTaskName) { this.subTask = subTaskName; } @Override public void run() { clearCollector(this); if (taskName != null) { monitor.setTaskName(taskName); } if (subTask != null) { monitor.subTask(subTask); } if (worked > 0) { monitor.internalWorked(worked); } } } /** * Creates a progress monitor wrapping the given one that uses the given * display. * * @param monitor * the actual progress monitor to be wrapped * @param jobMonitor * auxiliary monitor to consider for isCanceled() * @param display * the SWT display used to forward the calls to the wrapped * progress monitor */ public ForwardingProgressMonitor(IProgressMonitor monitor, IProgressMonitor jobMonitor, Display display) { super(monitor); Assert.isNotNull(display); this.display = display; this.jobMonitor = jobMonitor; } @Override public boolean isCanceled() { return jobMonitor.isCanceled() || super.isCanceled(); } @Override public void beginTask(final String name, final int totalWork) { synchronized (this) { collector = null; } display.asyncExec(() -> { currentTask = name; getWrappedProgressMonitor().beginTask(name, totalWork); }); } /** * Clears the collector object used to accumulate work and subtask calls * if it matches the given one. * * @param collectorToClear */ private synchronized void clearCollector(Collector collectorToClear) { // Check if the accumulator is still using the given collector. // If not, don't clear it. if (this.collector == collectorToClear) { this.collector = null; } } /** * Creates a collector object to accumulate work and subtask calls. * * @param taskName * initial task name * @param subTask * initial sub-task name * @param work * initial work of the collector */ private void createCollector(String taskName, String subTask, double work) { collector = new Collector(taskName, subTask, work, getWrappedProgressMonitor()); display.asyncExec(collector); } @Override public void done() { synchronized (this) { collector = null; } if (!display.isDisposed()) { display.asyncExec(() -> { try { getWrappedProgressMonitor().done(); } catch (SWTException e) { // May occur if the wrapped monitor is some already // disposed control. ProgressMonitorPart is otherwise // careful not to do anything when it has been disposed, // but neglects to check in done(). Just ignore it. } }); } } @Override public synchronized void internalWorked(final double work) { if (collector == null) { createCollector(null, null, work); } else { collector.worked(work); } } @Override public synchronized void setTaskName(final String name) { currentTask = name; if (collector == null) { createCollector(name, null, 0); } else { collector.setTaskName(name); } } @Override public synchronized void subTask(final String name) { if (collector == null) { createCollector(null, name, 0); } else { collector.subTask(name); } } @Override public void worked(int work) { internalWorked(work); } @Override public void clearBlocked() { // If this is a monitor that can report blocking do so. // Don't bother with a collector as this should only ever // happen once. IProgressMonitor wrapped = getWrappedProgressMonitor(); if (!(wrapped instanceof IProgressMonitorWithBlocking)) { return; } display.asyncExec(() -> { ((IProgressMonitorWithBlocking) wrapped).clearBlocked(); Dialog.getBlockedHandler().clearBlocked(); }); } @Override public void setBlocked(final IStatus reason) { // If this is a monitor that can report blocking do so. // Don't bother with a collector as this should only ever // happen once and prevent any more progress. IProgressMonitor wrapped = getWrappedProgressMonitor(); if (!(wrapped instanceof IProgressMonitorWithBlocking)) { return; } display.asyncExec(() -> { ((IProgressMonitorWithBlocking) wrapped).setBlocked(reason); // Do not give a shell as we want it to block until it opens. Dialog.getBlockedHandler().showBlocked(wrapped, reason, currentTask); }); } } private class BackgroundJob extends Job { private IRunnableWithProgress runnable; private Runnable onCancel; private IWizardPage page; public BackgroundJob(IRunnableWithProgress runnable, Runnable onCancel, IWizardPage page) { super(MessageFormat.format( UIText.NonBlockingWizardDialog_BackgroundJobName, page.getName())); this.runnable = runnable; this.onCancel = onCancel; this.page = page; this.addJobChangeListener(new JobChangeAdapter() { @Override public void done(IJobChangeEvent event) { if (!PlatformUI.isWorkbenchRunning()) { return; } Display display = PlatformUI.getWorkbench().getDisplay(); if (display == null || display.isDisposed()) { return; } display.syncExec(() -> { boolean hideProgress = false; synchronized (jobs) { Job currentJob = jobs.peek(); if (currentJob == BackgroundJob.this) { jobs.poll(); Job nextJob = jobs.peek(); if (nextJob != null) { nextJob.schedule(); } else { hideProgress = true; } } else if (currentJob == null) { hideProgress = true; } if (hideProgress) { IProgressMonitor uiMonitor = getProgressMonitor(); if (uiMonitor instanceof ProgressMonitorPart) { ProgressMonitorPart part = ((ProgressMonitorPart) uiMonitor); if (!part.isDisposed()) { part.setVisible(false); part.removeFromCancelComponent(null); } } } } }); } }); this.setUser(false); this.setSystem(true); } @Override public boolean shouldRun() { synchronized (jobs) { return page == getCurrentPage() && jobs.peek() == this; } } @Override protected void canceling() { try { if (onCancel != null) { onCancel.run(); } } finally { super.canceling(); } } @Override protected IStatus run(IProgressMonitor monitor) { if (!shouldRun()) { // Should actually not occur. Just to be on the safe side. return Status.CANCEL_STATUS; } // Hook up monitors so that we can report progress in the UI. IProgressMonitor uiMonitor = getProgressMonitor(); IProgressMonitor combinedMonitor; if (uiMonitor instanceof ProgressMonitorPart) { ProgressMonitorPart part = ((ProgressMonitorPart) uiMonitor); IProgressMonitor[] newMonitor = { null }; Display display = PlatformUI.getWorkbench().getDisplay(); if (display == null || display.isDisposed()) { return Status.CANCEL_STATUS; } display.syncExec(() -> { if (((ProgressMonitorPart) uiMonitor).isDisposed()) { return; } try { Control focusControl = display.getFocusControl(); part.setVisible(true); part.attachToCancelComponent(null); // Attaching sets the focus to the stop button... restoreFocus(focusControl); if (part instanceof BackgroundProgressMonitorPart) { ((BackgroundProgressMonitorPart) part) .setJob(BackgroundJob.this); } newMonitor[0] = new ForwardingProgressMonitor(uiMonitor, monitor, part.getDisplay()); } catch (SWTException e) { return; } }); combinedMonitor = newMonitor[0]; if (combinedMonitor == null) { return Status.CANCEL_STATUS; } } else { combinedMonitor = monitor; } try { runnable.run(combinedMonitor); } catch (InvocationTargetException e) { return Activator.createErrorStatus(e.getLocalizedMessage(), e); } catch (InterruptedException e) { return Status.CANCEL_STATUS; } finally { monitor.done(); if (combinedMonitor != monitor) { combinedMonitor.done(); } } return Status.OK_STATUS; } } }