/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.progress.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.progress.*;
import com.intellij.openapi.progress.util.*;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.ui.SystemNotifications;
import com.intellij.util.ArrayUtil;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.storage.HeavyProcessLatch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.awt.*;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
public class ProgressManagerImpl extends CoreProgressManager implements Disposable {
private final Set<CheckCanceledHook> myHooks = ContainerUtil.newConcurrentSet();
public ProgressManagerImpl() {
HeavyProcessLatch.INSTANCE.addUIActivityListener(new HeavyProcessLatch.HeavyProcessListener() {
CheckCanceledHook sleepHook = indicator -> sleepIfNeededToGivePriorityToAnotherThread();
AtomicBoolean scheduled = new AtomicBoolean();
Runnable addHookLater = () -> {
scheduled.set(false);
if (HeavyProcessLatch.INSTANCE.hasPrioritizedThread()) {
addCheckCanceledHook(sleepHook);
}
};
@Override
public void processStarted() {
if (scheduled.compareAndSet(false, true)) {
AppExecutorUtil.getAppScheduledExecutorService().schedule(addHookLater, 5, TimeUnit.MILLISECONDS);
}
}
@Override
public void processFinished() {
removeCheckCanceledHook(sleepHook);
}
}, this);
}
@Override
public void setCancelButtonText(String cancelButtonText) {
ProgressIndicator progressIndicator = getProgressIndicator();
if (progressIndicator != null) {
if (progressIndicator instanceof SmoothProgressAdapter && cancelButtonText != null) {
ProgressIndicator original = ((SmoothProgressAdapter)progressIndicator).getOriginalProgressIndicator();
if (original instanceof ProgressWindow) {
((ProgressWindow)original).setCancelButtonText(cancelButtonText);
}
}
}
}
@Override
public void executeProcessUnderProgress(@NotNull Runnable process, ProgressIndicator progress) throws ProcessCanceledException {
if (progress instanceof ProgressWindow) myCurrentUnsafeProgressCount.incrementAndGet();
CheckCanceledHook hook = progress instanceof PingProgress && ApplicationManager.getApplication().isDispatchThread()
? p -> { ((PingProgress)progress).interact(); return true; }
: null;
if (hook != null) addCheckCanceledHook(hook);
try {
super.executeProcessUnderProgress(process, progress);
}
finally {
if (progress instanceof ProgressWindow) myCurrentUnsafeProgressCount.decrementAndGet();
if (hook != null) removeCheckCanceledHook(hook);
}
}
@TestOnly
public static void runWithAlwaysCheckingCanceled(@NotNull Runnable runnable) {
Thread fake = new Thread("fake");
try {
threadsUnderCanceledIndicator.add(fake);
runnable.run();
}
finally {
threadsUnderCanceledIndicator.remove(fake);
}
}
@Override
public boolean runProcessWithProgressSynchronously(@NotNull final Task task, @Nullable final JComponent parentComponent) {
final long start = System.currentTimeMillis();
final boolean result = super.runProcessWithProgressSynchronously(task, parentComponent);
if (result) {
final long end = System.currentTimeMillis();
final Task.NotificationInfo notificationInfo = task.notifyFinished();
long time = end - start;
if (notificationInfo != null && time > 5000) { // show notification only if process took more than 5 secs
final JFrame frame = WindowManager.getInstance().getFrame(task.getProject());
if (frame != null && !frame.hasFocus()) {
systemNotify(notificationInfo);
}
}
}
return result;
}
private static void systemNotify(@NotNull Task.NotificationInfo info) {
SystemNotifications.getInstance().notify(info.getNotificationName(), info.getNotificationTitle(), info.getNotificationText());
}
@Override
@NotNull
public Future<?> runProcessWithProgressAsynchronously(@NotNull Task.Backgroundable task) {
ProgressIndicator progressIndicator = ApplicationManager.getApplication().isHeadlessEnvironment() ?
new EmptyProgressIndicator() :
new BackgroundableProcessIndicator(task);
return runProcessWithProgressAsynchronously(task, progressIndicator, null);
}
@Override
@NotNull
public Future<?> runProcessWithProgressAsynchronously(@NotNull final Task.Backgroundable task,
@NotNull final ProgressIndicator progressIndicator,
@Nullable final Runnable continuation,
@NotNull final ModalityState modalityState) {
if (progressIndicator instanceof Disposable) {
Disposer.register(ApplicationManager.getApplication(), (Disposable)progressIndicator);
}
final Runnable process = new TaskRunnable(task, progressIndicator, continuation);
TaskContainer action = new TaskContainer(task) {
@Override
public void run() {
boolean processCanceled = false;
Throwable exception = null;
final long start = System.currentTimeMillis();
try {
ProgressManager.getInstance().runProcess(process, progressIndicator);
}
catch (ProcessCanceledException e) {
processCanceled = true;
}
catch (Throwable e) {
exception = e;
}
final long end = System.currentTimeMillis();
final boolean finalCanceled = processCanceled || progressIndicator.isCanceled();
final Throwable finalException = exception;
if (!finalCanceled) {
final Task.NotificationInfo notificationInfo = task.notifyFinished();
final long time = end - start;
if (notificationInfo != null && time > 5000) { // snow notification if process took more than 5 secs
final Component window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
if (window == null || notificationInfo.isShowWhenFocused()) {
systemNotify(notificationInfo);
}
}
}
ApplicationManager.getApplication().invokeLater(() -> finishTask(task, finalCanceled, finalException), modalityState);
}
};
return ApplicationManager.getApplication().executeOnPooledThread(action);
}
@Override
public boolean runInReadActionWithWriteActionPriority(@NotNull Runnable action, @Nullable ProgressIndicator indicator) {
return ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(action, indicator);
}
/**
* An absolutely guru method, very dangerous, don't use unless you're desperate,
* because hooks will be executed on every checkCanceled and can dramatically slow down everything in the IDE.
*/
void addCheckCanceledHook(@NotNull CheckCanceledHook hook) {
if (myHooks.add(hook)) {
updateShouldCheckCanceled();
}
}
void removeCheckCanceledHook(@NotNull CheckCanceledHook hook) {
if (myHooks.remove(hook)) {
updateShouldCheckCanceled();
}
}
@Nullable
@Override
protected CheckCanceledHook createCheckCanceledHook() {
if (myHooks.isEmpty()) return null;
CheckCanceledHook[] activeHooks = ArrayUtil.stripTrailingNulls(myHooks.toArray(new CheckCanceledHook[0]));
return activeHooks.length == 1 ? activeHooks[0] : indicator -> {
boolean result = false;
for (CheckCanceledHook hook : activeHooks) {
if (hook.runHook(indicator)) {
result = true; // but still continue to other hooks
}
}
return result;
};
}
private static boolean sleepIfNeededToGivePriorityToAnotherThread() {
if (HeavyProcessLatch.INSTANCE.isInsideLowPriorityThread()) {
LockSupport.parkNanos(1_000_000);
return true;
}
return false;
}
}