Merge remote-tracking branch 'origin/GT-3457-dragonmacher-gtree-cannot-find-new-node'

This commit is contained in:
ghidorahrex 2020-01-21 16:00:51 -05:00
commit 060b06b688
11 changed files with 400 additions and 93 deletions

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,14 +15,6 @@
*/
package ghidra.app.plugin.core.datamgr.actions;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
import ghidra.app.plugin.core.datamgr.archive.Archive;
import ghidra.app.plugin.core.datamgr.tree.*;
import ghidra.program.model.data.Category;
import ghidra.program.model.data.DataTypeManager;
import ghidra.util.InvalidNameException;
import javax.swing.tree.TreePath;
import docking.ActionContext;
@ -31,6 +22,13 @@ import docking.action.DockingAction;
import docking.action.MenuData;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
import ghidra.app.plugin.core.datamgr.archive.Archive;
import ghidra.app.plugin.core.datamgr.tree.*;
import ghidra.program.model.data.Category;
import ghidra.program.model.data.DataTypeManager;
import ghidra.util.InvalidNameException;
public class CreateCategoryAction extends DockingAction {
@ -89,12 +87,12 @@ public class CreateCategoryAction extends DockingAction {
ArchiveNode archiveNode = node.getArchiveNode();
Archive archive = archiveNode.getArchive();
DataTypeManager dataTypeManager = archive.getDataTypeManager();
String newNodeName = null;
int transactionID = dataTypeManager.startTransaction("Create Category");
try {
final String newNodeName = getUniqueCategoryName(category);
newNodeName = getUniqueCategoryName(category);
category.createCategory(newNodeName);
dataTypeManager.flushEvents();
gtree.startEditing(node, newNodeName);
}
catch (InvalidNameException ie) {
// can't happen since we created a unique valid name.
@ -102,6 +100,9 @@ public class CreateCategoryAction extends DockingAction {
finally {
dataTypeManager.endTransaction(transactionID, true);
}
dataTypeManager.flushEvents();
gtree.startEditing(node, newNodeName);
}
private String getUniqueCategoryName(Category parent) {

View file

@ -26,7 +26,6 @@ import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
import ghidra.app.plugin.core.datamgr.tree.*;
import ghidra.program.model.data.*;
import ghidra.util.StringUtilities;
import ghidra.util.SystemUtilities;
public class CreateTypeDefAction extends AbstractTypeDefAction {
@ -130,7 +129,7 @@ public class CreateTypeDefAction extends AbstractTypeDefAction {
GTreeNode finalParentNode = info.getParentNode();
String newNodeName = newTypeDef.getName();
SystemUtilities.runSwingLater(() -> gTree.startEditing(finalParentNode, newNodeName));
gTree.startEditing(finalParentNode, newNodeName);
}
private static String getBaseName(DataType dt) {

View file

@ -339,11 +339,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
sb.append("Parent namespace " + namespace.getName() +
" contains namespace named " + symbol.getName() + "\n");
}
catch (InvalidInputException e) {
sb.append("Could not change parent namespace for " + symbol.getName() + ": " +
e.getMessage() + "\n");
}
catch (CircularDependencyException e) {
catch (InvalidInputException | CircularDependencyException e) {
sb.append("Could not change parent namespace for " + symbol.getName() + ": " +
e.getMessage() + "\n");
}
@ -408,7 +404,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
private void addTask(GTreeTask task) {
// Note: if we want to call this method from off the Swing thread, then we have to
// synchronize on the list that we are adding to here.
SystemUtilities.assertThisIsTheSwingThread(
Swing.assertThisIsTheSwingThread(
"Adding tasks must be done on the Swing thread," +
"since they are put into a list that is processed on the Swing thread. ");
@ -631,6 +627,7 @@ public class SymbolTreeProvider extends ComponentProviderAdapter {
private class SymbolTreeProviderDomainObjectListener implements DomainObjectListener {
@Override
public void domainObjectChanged(DomainObjectChangedEvent event) {
if (!tool.isVisible(SymbolTreeProvider.this)) {
return;
}

View file

@ -27,6 +27,7 @@ import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
import javax.swing.*;
import javax.swing.Timer;
@ -44,6 +45,7 @@ import docking.widgets.tree.internal.*;
import docking.widgets.tree.support.*;
import docking.widgets.tree.support.GTreeSelectionEvent.EventOrigin;
import docking.widgets.tree.tasks.*;
import generic.timer.ExpiringSwingTimer;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
@ -978,23 +980,45 @@ public class GTree extends JPanel implements BusyListener {
}
public void addGTModelListener(TreeModelListener listener) {
tree.getModel().addTreeModelListener(listener);
model.addTreeModelListener(listener);
}
public void removeGTModelListener(TreeModelListener listener) {
tree.getModel().removeTreeModelListener(listener);
model.removeTreeModelListener(listener);
}
public void setEditable(boolean editable) {
tree.setEditable(editable);
}
public void startEditing(final GTreeNode parent, final String childName) {
/**
* Requests that the node with the given name, in the given parent, be edited. <b>This
* operation (as with many others on this tree) is asynchronous.</b> This request will be
* buffered as needed to wait for the given node to be added to the parent, up to a timeout
* period.
*
* @param parent the parent node
* @param childName the child node name
*/
public void startEditing(GTreeNode parent, final String childName) {
// we call this here, even though the JTree will do this for us, so that we will trigger
// a load call before this task is run, in case lazy nodes are involved in this tree,
// which must be loaded before we can edit
expandPath(parent);
runTask(new GTreeStartEditingTask(GTree.this, tree, parent, childName));
//
// The request to edit the node may be for a node that has not yet been added to this
// tree. Further, some clients will buffer events, which means that the node the client
// wishes to edit may not yet be in the parent node even if we run this request later on
// the Swing thread. To deal with this, we use a construct that will run our request
// once the given node has been added to the parent.
//
BooleanSupplier isReady = () -> parent.getChild(childName) != null;
int expireMs = 3000;
ExpiringSwingTimer.runWhen(isReady, expireMs, () -> {
runTask(new GTreeStartEditingTask(GTree.this, tree, parent, childName));
});
}
@Override

View file

@ -28,6 +28,7 @@ import docking.widgets.tree.*;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;
@ -43,7 +44,7 @@ public class GTreeStartEditingTask extends GTreeTask {
}
@Override
public void run(final TaskMonitor monitor) {
public void run(final TaskMonitor monitor) throws CancelledException {
runOnSwingThread(() -> {
if (monitor.isCancelled()) {
return; // we can be cancelled while waiting for Swing to run us
@ -58,18 +59,21 @@ public class GTreeStartEditingTask extends GTreeTask {
}
private void edit() {
final GTreeNode child = parent.getChild(childName);
if (child == null) {
GTreeNode editNode = parent.getChild(childName);
if (editNode == null) {
if (tree.isFiltered()) {
Msg.showWarn(getClass(), tree, "Cannot Edit Tree Node",
"Cannot edit tree node \"" + childName + "\" while tree is filtered.");
"Can't edit tree node \"" + childName + "\" while tree is filtered.");
}
else {
Msg.debug(this,
"Can't find node \"" + childName + "\" to edit.");
}
Msg.debug(this,
"Can't find node for \"" + childName + "\". Perhaps it is filtered out?");
return;
}
TreePath path = child.getTreePath();
TreePath path = editNode.getTreePath();
final Set<GTreeNode> childrenBeforeEdit = new HashSet<>(parent.getChildren());
final CellEditor cellEditor = tree.getCellEditor();
@ -95,7 +99,7 @@ public class GTreeStartEditingTask extends GTreeTask {
* has finished and been applied.
*/
private void reselectNode() {
String newName = child.getName();
String newName = editNode.getName();
GTreeNode newChild = parent.getChild(newName);
if (newChild == null) {
throw new AssertException("Unable to find new node by name: " + newName);
@ -140,7 +144,7 @@ public class GTreeStartEditingTask extends GTreeTask {
}
});
tree.setNodeEditable(child);
tree.setNodeEditable(editNode);
jTree.startEditingAtPath(path);
}

View file

@ -15,13 +15,13 @@
*/
package generic.test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
@ -360,6 +360,17 @@ public abstract class AbstractGTest {
}
}
/**
* Waits for the given AtomicBoolean to return true. This is a convenience method for
* {@link #waitFor(BooleanSupplier)}.
*
* @param ab the atomic boolean
* @throws AssertionFailedError if the condition is not met within the timeout period
*/
public static void waitFor(AtomicBoolean ab) throws AssertionFailedError {
waitForCondition(() -> ab.get());
}
/**
* Waits for the given condition to return true
*

View file

@ -0,0 +1,135 @@
/* ###
* IP: GHIDRA
*
* 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 generic.timer;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
/**
* This class allows clients to run swing action at some point in the future, when the given
* condition is met, allowing for the task to timeout. While this class implements the
* {@link GhidraTimer} interface, it is really meant to be used to execute a code snippet one
* time at some point in the future.
*
* <p>Both the call to check for readiness and the actual client code will be run on the Swing
* thread.
*/
public class ExpiringSwingTimer extends GhidraSwingTimer {
private long startMs = System.currentTimeMillis();
private int expireMs;
private BooleanSupplier isReady;
private ExpiringTimerCallback expiringTimerCallback = new ExpiringTimerCallback();
private TimerCallback clientCallback;
private AtomicBoolean didRun = new AtomicBoolean();
/**
* Runs the given client runnable when the given condition returns true. The returned timer
* will be running.
*
* <p>Once the timer has performed the work, any calls to start the returned timer will
* not perform any work. You can check {@link #didRun()} to see if the work has been completed.
*
* @param isReady true if the code should be run
* @param expireMs the amount of time past which the code will not be run
* @param runnable the code to run
* @return the timer object that is running, which will execute the given code when ready
*/
public static ExpiringSwingTimer runWhen(BooleanSupplier isReady,
int expireMs,
Runnable runnable) {
// Note: we could let the client specify the period, but that would add an extra argument
// to this method. For now, just use something reasonable.
int delay = 250;
ExpiringSwingTimer timer =
new ExpiringSwingTimer(delay, expireMs, isReady, runnable);
timer.start();
return timer;
}
/**
* Constructor
*
* <p>Note: this class sets the parent's initial delay to 0. This is to allow the client
* code to be executed without delay when the ready condition is true.
*
* @param delay the delay between calls to check <code>isReady</code>
* @param isReady true if the code should be run
* @param expireMs the amount of time past which the code will not be run
* @param runnable the code to run
*/
public ExpiringSwingTimer(int delay, int expireMs, BooleanSupplier isReady,
Runnable runnable) {
super(0, delay, null);
this.expireMs = expireMs;
this.isReady = isReady;
this.clientCallback = () -> runnable.run();
super.setTimerCallback(expiringTimerCallback);
setRepeats(true);
}
/**
* Returns true if the client runnable was run
* @return true if the client runnable was run
*/
public boolean didRun() {
return didRun.get();
}
@Override
public void start() {
if (didRun() || isExpired()) {
return;
}
super.start();
}
/**
* Returns true the initial expiration period has passed
* @return true if expired
*/
public boolean isExpired() {
long now = System.currentTimeMillis();
int elapsed = (int) (now - startMs);
return elapsed > expireMs;
}
@Override
public void setTimerCallback(TimerCallback callback) {
// overridden to ensure clients cannot overwrite out wrapping callback
this.clientCallback = Objects.requireNonNull(callback);
}
private class ExpiringTimerCallback implements TimerCallback {
@Override
public void timerFired() {
if (isReady.getAsBoolean()) {
clientCallback.timerFired();
didRun.set(true);
stop();
return;
}
else if (isExpired()) {
stop();
}
}
}
}

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,67 +21,78 @@ import java.awt.event.ActionListener;
import javax.swing.Timer;
public class GhidraSwingTimer implements GhidraTimer, ActionListener {
private Timer timer;
Timer timer;
private TimerCallback callback;
public GhidraSwingTimer() {
this(100, null);
}
public GhidraSwingTimer(int delay, TimerCallback callback) {
this(delay,delay,callback);
this(delay, delay, callback);
}
public GhidraSwingTimer(int initialDelay, int delay, TimerCallback callback) {
this.callback = callback;
timer = new Timer(delay, this);
timer.setInitialDelay(delay);
timer.setInitialDelay(initialDelay);
}
@Override
public void actionPerformed(ActionEvent e) {
if (callback != null) {
callback.timerFired();
}
}
@Override
public int getDelay() {
return timer.getDelay();
}
@Override
public int getInitialDelay() {
return timer.getInitialDelay();
}
@Override
public boolean isRepeats() {
return timer.isRepeats();
}
@Override
public void setDelay(int delay) {
timer.setDelay(delay);
}
@Override
public void setInitialDelay(int initialDelay) {
timer.setInitialDelay(initialDelay);
}
@Override
public void setRepeats(boolean repeats) {
timer.setRepeats(repeats);
}
@Override
public void setTimerCallback(TimerCallback callback) {
this.callback = callback;
}
@Override
public void start() {
timer.start();
}
@Override
public void stop() {
timer.stop();
}
@Override
public boolean isRunning() {
return timer.isRunning();
}
}

View file

@ -15,8 +15,6 @@
*/
package ghidra.util.task;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.Timer;
import ghidra.util.Msg;
@ -50,13 +48,8 @@ import utilities.util.reflection.ReflectionUtilities;
*
* <P> This class is safe to use in a multi-threaded environment. State variables are guarded
* via synchronization on this object. The Swing thread is used to perform updates, which
* guarantees that only one update will happen at a time. There is one state variable,
* the {@link #workCount}, that is changed both in the synchronized blocks and the Swing thread
* which is an atomic variable. This variable must be updated/incremented when the
* synchronized variables are cleared to prevent {@link #isBusy()} from returning false when
* there is a gap between 'work posted' and 'work execution'.
* guarantees that only one update will happen at a time.
*/
public class SwingUpdateManager {
private static final long NONE = 0;
public static final int DEFAULT_MAX_DELAY = 30000;
@ -78,11 +71,13 @@ public class SwingUpdateManager {
private long bufferingStartTime;
private boolean disposed = false;
// this is the number of times we will be calling work
private AtomicInteger workCount = new AtomicInteger();
// This is true when work has begun and is not finished. This is only mutated on the
// Swing thread, but is read by other threads.
private boolean isWorking;
/**
* Constructs a new SwingUpdateManager.
* Constructs a new SwingUpdateManager with default values for min and max delay. See
* {@link #DEFAULT_MIN_DELAY} and {@value #DEFAULT_MAX_DELAY}.
*
* @param r the runnable that performs the client work.
*/
@ -171,7 +166,7 @@ public class SwingUpdateManager {
}
requestTime = System.currentTimeMillis();
bufferingStartTime = bufferingStartTime == 0 ? requestTime : bufferingStartTime;
bufferingStartTime = bufferingStartTime == NONE ? requestTime : bufferingStartTime;
scheduleCheckForWork();
}
@ -219,15 +214,15 @@ public class SwingUpdateManager {
}
/**
* Returns true if any work is being performed or if there is buffered work.
* @return true if any work is being performed or if there is buffered work.
* Returns true if any work is being performed or if there is buffered work
* @return true if any work is being performed or if there is buffered work
*/
public synchronized boolean isBusy() {
if (disposed) {
return false;
}
return requestTime != NONE || workCount.get() != 0;
return requestTime != NONE || isWorking;
}
public synchronized void dispose() {
@ -253,21 +248,25 @@ public class SwingUpdateManager {
"\tname: " + name + "\n" +
"\tcreator: " + inceptionInformation + " ("+System.identityHashCode(this)+")\n" +
"\trequest time: "+requestTime + "\n" +
"\twork count: " + workCount.get() + "\n" +
"\twork count: " + isWorking + "\n" +
"}";
//@formatter:on
}
// note: this is called on the Swing thread
private void checkForWork() {
if (shouldDoWork()) {
doWork();
}
}
// note: this is called on the Swing thread
private synchronized boolean shouldDoWork() {
// If no pending request, exit without restarting timer.
// If no pending request, exit without restarting timer
if (requestTime == NONE) {
bufferingStartTime = NONE; // The timer has fired and there is no pending work
return false;
}
@ -275,7 +274,7 @@ public class SwingUpdateManager {
if (isTimeToWork(now)) {
bufferingStartTime = now;
requestTime = NONE;
workCount.incrementAndGet();
isWorking = true;
return true;
}
@ -304,6 +303,7 @@ public class SwingUpdateManager {
return false;
}
// note: this is called on the Swing thread
private void doWork() {
try {
clientRunnable.run();
@ -313,9 +313,11 @@ public class SwingUpdateManager {
Msg.showError(this, null, "Unexpected Exception",
"Unexpected exception in Swing Update Manager", t);
}
finally {
workCount.decrementAndGet();
}
isWorking = false;
// we need to clear the buffering flag after the minDelay has passed, so start the timer
scheduleCheckForWork();
}
//==================================================================================================

View file

@ -0,0 +1,86 @@
/* ###
* IP: GHIDRA
*
* 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 generic.timer;
import static org.junit.Assert.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class ExpiringSwingTimerTest extends AbstractGenericTest {
@Test
public void testRunWhenReady() {
int waitCount = 2;
AtomicInteger counter = new AtomicInteger();
BooleanSupplier isReady = () -> {
return counter.incrementAndGet() > waitCount;
};
AtomicInteger runCount = new AtomicInteger();
Runnable r = () -> {
runCount.incrementAndGet();
};
ExpiringSwingTimer.runWhen(isReady, 10000, r);
waitFor(() -> runCount.get() > 0);
assertTrue("Timer did not wait for the condition to be true", counter.get() > waitCount);
assertEquals("Client code was run more than once", 1, runCount.get());
}
@Test
public void testRunWhenReady_Timeout() {
BooleanSupplier isReady = () -> {
return false;
};
AtomicBoolean didRun = new AtomicBoolean();
Runnable r = () -> didRun.set(true);
ExpiringSwingTimer timer = ExpiringSwingTimer.runWhen(isReady, 500, r);
waitFor(() -> !timer.isRunning());
assertFalse(didRun.get());
}
@Test
public void testWorkOnlyHappensOnce() {
BooleanSupplier isReady = () -> {
return true;
};
AtomicInteger runCount = new AtomicInteger();
Runnable r = () -> {
runCount.incrementAndGet();
};
ExpiringSwingTimer timer = ExpiringSwingTimer.runWhen(isReady, 10000, r);
waitFor(() -> !timer.isRunning());
assertEquals(1, runCount.get());
timer.start();
waitFor(() -> !timer.isRunning());
assertEquals(1, runCount.get());
}
}

View file

@ -37,6 +37,11 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
@Before
public void setUp() throws Exception {
manager = createUpdateManager(MIN_DELAY, MAX_DELAY);
// must turn this on to get the expected results, as in headless mode the update manager
// will run it's Swing work immediately on the test thread, which is not true to the
// default behavior
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
}
@Test
@ -160,20 +165,16 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
// before the update runnable is called.
//
// must turn this on to get the expected results, as in headless mode the update manager
// will run it's Swing work immediately
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(1);
final AtomicBoolean exception = new AtomicBoolean();
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
AtomicBoolean exception = new AtomicBoolean();
runSwing(new Runnable() {
@Override
public void run() {
startGate.countDown();
startLatch.countDown();
try {
endGate.await(10, TimeUnit.SECONDS);
endLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
exception.set(true);
@ -181,13 +182,13 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
}
}, false);
// This will cause the swing thread to block until will countdown the endGate latch
startGate.await(10, TimeUnit.SECONDS);
// This will cause the swing thread to block until we countdown the end latch
startLatch.await(10, TimeUnit.SECONDS);
manager.update();
assertTrue("Manager not busy after requesting an update", manager.isBusy());
endGate.countDown();
endLatch.countDown();
waitForManager();
assertTrue("Manager still busy after waiting for update", !manager.isBusy());
@ -195,9 +196,61 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
assertFalse("Interrupted waiting for CountDowLatch", exception.get());
}
@Test
public void testCallToUpdateWhileAnUpdateIsWorking() throws Exception {
//
// Test that an update call from a non-swing thread will still get processed if the
// manager is actively processing an update on the swing thread.
//
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(1);
AtomicBoolean exception = new AtomicBoolean();
Runnable r = () -> {
runnableCalled++;
startLatch.countDown();
try {
endLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
exception.set(true);
}
};
// start the update manager and have it wait for us
manager = new SwingUpdateManager(MIN_DELAY, MAX_DELAY, r);
manager.update();
// have the swing thread block until we countdown the end latch
startLatch.await(10, TimeUnit.SECONDS);
// post the second update request now that the manager is actively processing
manager.update();
// let the update manager finish; verify 2 work items total
endLatch.countDown();
waitForManager();
assertEquals("Expected exactly 2 callbacks", 2, runnableCalled);
}
//==============================================================================================
// Private Methods
//==============================================================================================
private void waitForManager() {
// let all swing updates finish, which may trigger the update manager
waitForSwing();
while (manager.isBusy()) {
sleep(DEFAULT_WAIT_DELAY);
}
// let any resulting swing events finish
waitForSwing();
}
private SwingUpdateManager createUpdateManager(int min, int max) {
return new SwingUpdateManager(min, max, new Runnable() {
@ -209,19 +262,4 @@ public class SwingUpdateManagerTest extends AbstractGenericTest {
}
});
}
private void waitForManager() {
// let all swing updates finish, which may trigger the update manager
waitForPostedSwingRunnables();
while (manager.isBusy()) {
sleep(DEFAULT_WAIT_DELAY);
}
// let any resulting swing events finish
waitForPostedSwingRunnables();
}
}