allow to merge nested transactions by id

This commit is contained in:
peter
2016-03-30 21:59:15 +02:00
parent e149b11a8e
commit 5703c4fbf6
6 changed files with 282 additions and 108 deletions
@@ -18,9 +18,8 @@ package com.intellij.openapi.application;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Ref;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A service managing model transactions.<p/>
@@ -35,8 +34,8 @@ import org.jetbrains.annotations.NotNull;
* should be performed inside a transaction. No write action should be performed from within an {@code invokeLater}-like call
* unless wrapped into a transaction.<p/>
*
* The recommended way to perform a transaction is to invoke {@link #submitTransaction(Runnable)}. It either runs the transaction immediately
* (if on UI thread and there's no other transaction running) or queues it to invoke at some later moment, when it becomes possible.<p/>
* The recommended way to perform a transaction is to invoke {@link #submitTransaction(Disposable, Runnable)}. It either runs the transaction immediately
* (if on UI thread and not inside invokeLater) or queues it to invoke at some later moment, when it becomes possible.<p/>
*
* Sometimes transactions need to be processed immediately, even if another transaction is already running. Example:
* outer transaction has shown a dialog with an editor, and typing into that editor (which requires a transaction for changing document)
@@ -71,21 +70,7 @@ import org.jetbrains.annotations.NotNull;
*
* Q: I've got <b>"Write access is allowed from model transactions only"</b> exception, what do I do?<br/>
* A: You're likely inside an "invokeLater"-like call. Please consider replacing it with {@link #submitTransaction(Disposable, Runnable)} or
* {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}
* <p/>
*
* Q: I've got <b>"Nested transactions are not allowed"</b> exception, what do I do?<br/>
* A: First, identify the place in the stack where the outer transaction is started (the exception attachment should contain it).
* Then, see if there is any Swing event pumping
* in between two transactions (e.g. a dialog is shown). If not, one of the two involved transactions is superfluous, remove it. If there
* is event pumping present, check if the client code (e.g. the one showing the dialog) is prepared to such nested model modifications.
* For example, refactoring dialogs might be prepared to {@link TransactionKind#TEXT_EDITING} kind
* (for text field editing inside the dialogs) but not to
* other model changes, e.g. root changes. The outer transaction code might then specify which kinds it's prepared to (by using
* {@link #acceptNestedTransactions(TransactionKind...)}), and the inner transaction code should have the very same transaction kind
* (by using {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)} or {@link #startSynchronousTransaction(TransactionKind)}).
* If the nested transaction is not expected by the outer code, it must be made asynchronous by using either {@link #submitTransaction(Disposable, Runnable)}
* or {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}.
* {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)}
* <p/>
*
* Q: What's the difference between transactions and read/write actions and commands ({@link com.intellij.openapi.command.CommandProcessor})?<br/>
@@ -95,7 +80,7 @@ import org.jetbrains.annotations.NotNull;
*
* @see Application#runReadAction(Runnable)
* @see Application#runWriteAction(Runnable)
* @since 146.*
* @since 2016.2
* @author peter
*/
public abstract class TransactionGuard {
@@ -112,11 +97,11 @@ public abstract class TransactionGuard {
}
/**
* Ensures that some code will be run in a transaction. It's guaranteed that no other transactions are run at the same time.
* The code will be run on Swing thread immediately or after all other queued transactions (if any) have been completed.<p/>
* Ensures that some code will be run in a transaction. It's guaranteed that no other transactions can run at the same time,
* except for the ones started from within this runnable. The code will be run on Swing thread immediately
* or after other queued transactions (if any) have been completed.<p/>
*
* For more advanced version, see {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}
* Transactions submitted via this method use {@link TransactionKind#ANY_CHANGE} kind.
* For more advanced version, see {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)}.
*
* @param parentDisposable an object whose disposing (via {@link com.intellij.openapi.util.Disposer} makes this transaction invalid,
* and so it won't be run after it has been disposed
@@ -153,20 +138,6 @@ public abstract class TransactionGuard {
*/
public abstract void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull Runnable transaction) throws ProcessCanceledException;
/**
* Same as {@link #submitTransactionAndWait(TransactionKind, Runnable)}, but returns a value computed by the transaction.
*/
public <T> T submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Computable<T> transaction) throws ProcessCanceledException {
final Ref<T> result = Ref.create();
submitTransactionAndWait(kind, new Runnable() {
@Override
public void run() {
result.set(transaction.compute());
}
});
return result.get();
}
/**
* A synchronous version of {@link #submitMergeableTransaction(Disposable, TransactionKind, Runnable)}.
* @return a token object for this transaction. Call {@link AccessToken#finish()} (inside finally) when the transaction is complete.
@@ -194,6 +165,19 @@ public abstract class TransactionGuard {
*/
public abstract void submitMergeableTransaction(@NotNull Disposable parentDisposable, @NotNull TransactionKind kind, @NotNull Runnable transaction);
/**
* Executes the given runnable inside a transaction as soon as possible on the UI thread. The runnable is executed either when there's
* no active transaction running, or when the running transaction has the same (or compatible) id as {@code mergeInto}. If the id of
* the current transaction is passed, the transaction is executed immediately. Otherwise adds the runnable to a queue,
* to execute after all transactions scheduled before this one are finished.
* @param parentDisposable an object whose disposing (via {@link com.intellij.openapi.util.Disposer} makes this transaction invalid,
* and so it won't be run after it has been disposed.
* @param mergeInto an optional id of another transaction, to allow execution inside that transaction if it's still running
* @param transaction code to execute inside a transaction.
* @see #getCurrentMergeableTransaction()
*/
public abstract void submitMergeableTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId mergeInto, @NotNull Runnable transaction);
/**
* Allow incoming transactions of the specified kinds to be executed immediately, instead of being queued until the current transaction is finished.<p/>
*
@@ -215,4 +199,9 @@ public abstract class TransactionGuard {
*/
public abstract void assertInsideTransaction(boolean transactionRequired, @NotNull String errorMessage);
/**
* @return the id of the currently running transaction for using in {@link #submitMergeableTransaction(Disposable, TransactionId, Runnable)},
* or null if there's no transaction running or merging is not allowed in the callee context (e.g. from invokeLater).
*/
public abstract TransactionId getCurrentMergeableTransaction();
}
@@ -0,0 +1,14 @@
package com.intellij.openapi.application;
import com.intellij.openapi.Disposable;
/**
* A unique object identifying each running transaction. Can be retrieved from {@link TransactionGuard#getCurrentMergeableTransaction()}
* while inside a transaction, and used to merge other transactions into it via
* {@link TransactionGuard#submitMergeableTransaction(Disposable, TransactionId, Runnable)}.
*
* @author peter
* @since 2016.2
*/
public interface TransactionId {
}
@@ -16,91 +16,99 @@
package com.intellij.openapi.application;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author peter
*/
public class TransactionGuardImpl extends TransactionGuard {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.application.TransactionGuardImpl");
private final Queue<Runnable> myQueue = new LinkedBlockingQueue<Runnable>();
private final Queue<Transaction> myQueue = new LinkedBlockingQueue<Transaction>();
private final Set<TransactionKind> myMergeableKinds = ContainerUtil.newHashSet();
private String myTransactionStartTrace;
private ModalityState myTransactionModality;
private boolean myUserActivity;
private final Map<ProgressIndicator, TransactionIdImpl> myProgresses = ContainerUtil.createConcurrentWeakMap();
private TransactionIdImpl myCurrentTransaction;
private boolean myWritingAllowed;
@Override
@NotNull
public AccessToken startSynchronousTransaction(@NotNull TransactionKind kind) throws IllegalStateException {
ModalityState modality = ModalityState.current();
if (isInsideTransaction()) {
if (modality.equals(myTransactionModality) || myUserActivity) {
return AccessToken.EMPTY_ACCESS_TOKEN;
}
if (myMergeableKinds.contains(kind)) {
final ModalityState prev = myTransactionModality;
myTransactionModality = modality;
return new AccessToken() {
@Override
public void finish() {
myTransactionModality = prev;
}
};
}
if (isInsideTransaction() && !myWritingAllowed && !myMergeableKinds.contains(kind)) {
// please assign exceptions that occur here to Peter
LOG.error("Nested transactions are not allowed, see FAQ in TransactionGuard class javadoc. Transaction start trace is in attachment. Kind is " + kind,
new Attachment("trace.txt", myTransactionStartTrace));
LOG.error("Synchronous transactions are allowed only from user actions. " +
"Please use submit*Transaction instead of invokeLater. " +
"See FAQ in TransactionGuard class javadoc.");
return AccessToken.EMPTY_ACCESS_TOKEN;
}
myTransactionModality = modality;
myTransactionStartTrace = DebugUtil.currentStackTrace();
return startTransactionUnchecked();
}
@NotNull
private AccessToken startTransactionUnchecked() {
final TransactionIdImpl prevTransaction = myCurrentTransaction;
final boolean wasWritingAllowed = myWritingAllowed;
myWritingAllowed = true;
myCurrentTransaction = new TransactionIdImpl();
return new AccessToken() {
@Override
public void finish() {
myTransactionStartTrace = null;
myTransactionModality = null;
if (!myQueue.isEmpty()) {
Queue<Transaction> queue = getQueue(prevTransaction);
queue.addAll(myCurrentTransaction.myQueue);
if (!queue.isEmpty()) {
pollQueueLater();
}
myWritingAllowed = wasWritingAllowed;
myCurrentTransaction = prevTransaction;
}
};
}
@NotNull
private Queue<Transaction> getQueue(@Nullable TransactionIdImpl prevTransaction) {
return prevTransaction == null ? myQueue : prevTransaction.myQueue;
}
private void pollQueueLater() {
//todo replace with SwingUtilities when write actions are required to run under a guard
final Application app = ApplicationManager.getApplication();
app.invokeLater(new Runnable() {
@Override
public void run() {
if (isInsideTransaction()) return;
Runnable next = myQueue.poll();
if (next != null) {
runSyncTransaction(TransactionKind.ANY_CHANGE, next);
Queue<Transaction> queue = getQueue(myCurrentTransaction);
Transaction next = queue.peek();
if (next != null && canRunTransactionNow(next, false)) {
queue.remove();
runSyncTransaction(next);
}
}
}, app.getDisposed());
}
private void runSyncTransaction(@NotNull TransactionKind kind, @NotNull Runnable code) {
AccessToken token = startSynchronousTransaction(kind);
private void runSyncTransaction(@NotNull Transaction transaction) {
AccessToken token = startTransactionUnchecked();
try {
code.run();
if (!Disposer.isDisposed(transaction.parentDisposable)) {
transaction.runnable.run();
}
}
finally {
token.finish();
@@ -109,34 +117,40 @@ public class TransactionGuardImpl extends TransactionGuard {
private boolean isInsideTransaction() {
ApplicationManager.getApplication().assertIsDispatchThread();
return myTransactionStartTrace != null;
return myCurrentTransaction != null;
}
@Override
public void submitMergeableTransaction(@NotNull final Disposable parentDisposable, @NotNull final TransactionKind kind, @NotNull final Runnable _transaction) {
@NotNull final Runnable transaction = new Runnable() {
@Override
public void run() {
if (!Disposer.isDisposed(parentDisposable)) {
_transaction.run();
}
}
};
submitMergeableTransaction(parentDisposable, kind, getCurrentMergeableTransaction(), _transaction);
}
@Override
public void submitMergeableTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId mergeInto, @NotNull Runnable transaction) {
submitMergeableTransaction(parentDisposable, TransactionKind.ANY_CHANGE, (TransactionIdImpl)mergeInto, transaction);
}
private void submitMergeableTransaction(@NotNull final Disposable parentDisposable,
@NotNull final TransactionKind kind,
@Nullable final TransactionIdImpl expectedId,
@NotNull final Runnable _transaction) {
@NotNull final Transaction transaction = new Transaction(_transaction, expectedId, kind, parentDisposable);
final Application app = ApplicationManager.getApplication();
final boolean isDispatchThread = app.isDispatchThread();
Runnable runnable = new Runnable() {
@Override
public void run() {
if (canRunTransactionNow(kind)) {
runSyncTransaction(kind, transaction);
if (canRunTransactionNow(transaction, isDispatchThread)) {
runSyncTransaction(transaction);
}
else {
myQueue.offer(transaction);
getQueue(expectedId).offer(transaction);
pollQueueLater();
}
}
};
final Application app = ApplicationManager.getApplication();
if (app.isDispatchThread()) {
if (isDispatchThread) {
runnable.run();
} else {
//todo add ModalityState.any() when write actions are required to run under a guard
@@ -144,8 +158,17 @@ public class TransactionGuardImpl extends TransactionGuard {
}
}
protected boolean canRunTransactionNow(@NotNull TransactionKind kind) {
return !isInsideTransaction() || myMergeableKinds.contains(kind) || ModalityState.current().equals(myTransactionModality);
private boolean canRunTransactionNow(Transaction transaction, boolean sync) {
TransactionIdImpl currentId = myCurrentTransaction;
if (currentId == null || myMergeableKinds.contains(transaction.kind)) {
return true;
}
if (sync && !myWritingAllowed) {
return false;
}
return transaction.mergeInto != null && currentId.myStartCounter <= transaction.mergeInto.myStartCounter;
}
@Override
@@ -179,13 +202,16 @@ public class TransactionGuardImpl extends TransactionGuard {
}
@Override
public void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Runnable transaction) throws ProcessCanceledException {
public void submitTransactionAndWait(@NotNull TransactionKind kind, @NotNull final Runnable runnable) throws ProcessCanceledException {
Application app = ApplicationManager.getApplication();
if (app.isDispatchThread()) {
if (!canRunTransactionNow(kind)) {
throw new AssertionError("Cannot run submitTransactionAndWait from another transaction, kind " + kind + " is not allowed");
Transaction transaction = new Transaction(runnable, getCurrentMergeableTransaction(), kind, app);
if (!canRunTransactionNow(transaction, true)) {
throw new AssertionError("Cannot run synchronous submitTransactionAndWait from invokeLater. " +
"Please use asynchronous submit*Transaction. " +
"See TransactionGuard FAQ for details.");
}
runSyncTransaction(kind, transaction);
runSyncTransaction(transaction);
return;
}
@@ -197,7 +223,7 @@ public class TransactionGuardImpl extends TransactionGuard {
@Override
public void run() {
try {
transaction.run();
runnable.run();
}
catch (Throwable e) {
exception[0] = e;
@@ -224,7 +250,7 @@ public class TransactionGuardImpl extends TransactionGuard {
* please consider using {@code ActionManager.tryToExecute()} instead, or ensure in some other way that the action is enabled
* and can be invoked in the current modality state.
*/
public <T extends Throwable> void performUserActivity(Runnable activity) {
public void performUserActivity(Runnable activity) {
ApplicationManager.getApplication().assertIsDispatchThread();
AccessToken token = startActivity(true);
try {
@@ -240,32 +266,77 @@ public class TransactionGuardImpl extends TransactionGuard {
*/
@NotNull
public AccessToken startActivity(boolean userActivity) {
if (myUserActivity == userActivity) {
if (myWritingAllowed == userActivity) {
return AccessToken.EMPTY_ACCESS_TOKEN;
}
final boolean prev = myUserActivity;
myUserActivity = userActivity;
final boolean prev = myWritingAllowed;
myWritingAllowed = userActivity;
return new AccessToken() {
@Override
public void finish() {
myUserActivity = prev;
myWritingAllowed = prev;
}
};
}
public boolean isWriteActionAllowed() {
return !Registry.is("ide.require.transaction.for.model.changes", false) || isInsideTransaction() || myUserActivity;
return !Registry.is("ide.require.transaction.for.model.changes", false) || isInsideTransaction() || myWritingAllowed;
}
@Override
public void submitTransactionLater(@NotNull final Disposable parentDisposable, @NotNull final Runnable transaction) {
final TransactionIdImpl id = getCurrentMergeableTransaction();
Application app = ApplicationManager.getApplication();
app.invokeLater(new Runnable() {
@Override
public void run() {
submitMergeableTransaction(parentDisposable, TransactionKind.ANY_CHANGE, transaction);
submitMergeableTransaction(parentDisposable, id, transaction);
}
}, app.getDisposed());
}
@Override
public TransactionIdImpl getCurrentMergeableTransaction() {
if (!ApplicationManager.getApplication().isDispatchThread()) {
ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator();
return indicator != null ? myProgresses.get(indicator) : null;
}
return myWritingAllowed ? myCurrentTransaction : null;
}
public void registerProgress(@NotNull ProgressIndicator indicator, @Nullable TransactionIdImpl contextTransaction) {
if (contextTransaction != null) {
myProgresses.put(indicator, contextTransaction);
}
}
private static class Transaction {
@NotNull final Runnable runnable;
@Nullable final TransactionIdImpl mergeInto;
@NotNull final TransactionKind kind;
@NotNull final Disposable parentDisposable;
Transaction(@NotNull Runnable runnable,
@Nullable TransactionIdImpl mergeInto,
@NotNull TransactionKind kind,
@NotNull Disposable parentDisposable) {
this.runnable = runnable;
this.mergeInto = mergeInto;
this.kind = kind;
this.parentDisposable = parentDisposable;
}
}
private static class TransactionIdImpl implements TransactionId {
private static final AtomicLong ourTransactionCounter = new AtomicLong();
final long myStartCounter = ourTransactionCounter.getAndIncrement();
final Queue<Transaction> myQueue = new LinkedBlockingQueue<Transaction>();
@Override
public String toString() {
return "Transaction " + myStartCounter;
}
}
}
@@ -18,6 +18,8 @@ package com.intellij.openapi.progress.util;
import com.intellij.ide.IdeEventQueue;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.application.TransactionGuardImpl;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
@@ -93,6 +95,9 @@ public class ProgressWindow extends ProgressIndicatorBase implements BlockingPro
myShouldShowCancel = shouldShowCancel;
myCancelText = cancelText;
setModalityProgress(shouldShowBackground ? null : this);
TransactionGuardImpl guard = (TransactionGuardImpl)TransactionGuard.getInstance();
guard.registerProgress(this, guard.getCurrentMergeableTransaction());
myFocusTrackback = new FocusTrackback(this, WindowManager.getInstance().suggestParentWindow(project), false);
Component parent = parentComponent;
@@ -4,6 +4,8 @@ import com.intellij.openapi.application.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.application.TransactionGuardImpl
import com.intellij.openapi.application.TransactionId
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.LightPlatformTestCase
@@ -15,6 +17,8 @@ import javax.swing.*
* @author peter
*/
class TransactionTest extends LightPlatformTestCase {
List<String> log = []
static TransactionGuardImpl getGuard() {
return TransactionGuard.getInstance() as TransactionGuardImpl
}
@@ -33,6 +37,7 @@ class TransactionTest extends LightPlatformTestCase {
@Override
protected void tearDown() throws Exception {
Registry.get("ide.require.transaction.for.model.changes").resetToDefault()
log.clear()
super.tearDown()
}
@@ -54,7 +59,6 @@ class TransactionTest extends LightPlatformTestCase {
public void "test parent disposable"() {
def parent = Disposer.newDisposable()
def log = []
SwingUtilities.invokeLater { TransactionGuard.submitTransaction parent, { log << "1" } }
UIUtil.dispatchAllInvocationEvents()
@@ -65,4 +69,95 @@ class TransactionTest extends LightPlatformTestCase {
UIUtil.dispatchAllInvocationEvents()
assert log == ['1']
}
public void "test no current id inside invokeLater"() {
SwingUtilities.invokeLater {
log << '2'
assert !guard.currentMergeableTransaction
}
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
UIUtil.dispatchAllInvocationEvents()
}
assert log == ['1', '2']
}
public void "test has id inside nested transaction"() {
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
def id = guard.currentMergeableTransaction
assert id
TransactionGuard.submitTransaction testRootDisposable, {
log << '2'
assert guard.currentMergeableTransaction
assert id != guard.currentMergeableTransaction
}
assert log == ['1', '2']
}
assert log == ['1', '2']
}
public void "test modal progress started from inside a transaction has the same id"() {
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
def id = guard.currentMergeableTransaction
assert id
ProgressManager.instance.runProcessWithProgressSynchronously({
assert !ApplicationManager.application.dispatchThread
assert id == guard.currentMergeableTransaction
log << '2'
}, 'title', true, project)
}
assert log == ['1', '2']
}
public void "test no id on pooled thread"() {
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
ApplicationManager.application.executeOnPooledThread({
assert !ApplicationManager.application.dispatchThread
assert !guard.currentMergeableTransaction
log << '2'
}).get()
}
assert log == ['1', '2']
}
public void "test do not merge transactions with null id"() {
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
guard.submitMergeableTransaction testRootDisposable, (TransactionId)null, { log << '2' }
UIUtil.dispatchAllInvocationEvents()
assert log == ['1']
}
assert log == ['1']
UIUtil.dispatchAllInvocationEvents()
assert log == ['1', '2']
}
public void "test do not merge into newly started nested transactions"() {
TransactionGuard.submitTransaction testRootDisposable, {
log << '1'
def id = guard.currentMergeableTransaction
SwingUtilities.invokeLater {
guard.submitMergeableTransaction testRootDisposable, id, { log << '4' }
}
guard.submitMergeableTransaction testRootDisposable, id, {
UIUtil.dispatchAllInvocationEvents()
log << '2'
UIUtil.dispatchAllInvocationEvents()
guard.submitMergeableTransaction testRootDisposable, id, { log << '5' }
def nestedId = guard.currentMergeableTransaction
SwingUtilities.invokeLater {
guard.submitMergeableTransaction testRootDisposable, nestedId, { log << '3' }
assert log == ['1', '2']
}
UIUtil.dispatchAllInvocationEvents()
}
assert log == ['1', '2', '3']
UIUtil.dispatchAllInvocationEvents()
assert log == ['1', '2', '3', '4', '5']
}
}
}
@@ -1895,9 +1895,9 @@ public class UIUtil {
AWTEvent event = eventQueue.peekEvent();
if (event == null) return false;
try {
AWTEvent event1 = eventQueue.getNextEvent();
if (event1 instanceof InvocationEvent) {
((InvocationEvent)event1).dispatch();
event = eventQueue.getNextEvent();
if (event instanceof InvocationEvent) {
eventQueue.getClass().getDeclaredMethod("dispatchEvent", AWTEvent.class).invoke(eventQueue, event);
}
}
catch (Exception e) {