blob: 346b22abc0ec4be1b70c37da78bdc72f2c5e0f3b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 IBM Corporation and others.
* 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
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.viewers.deferred;
/**
* Allows a table to be accessed from a background thread. Provides a table-like public
* interface that can accessed from a background thread. As updates arrive from the
* background thread, it batches and schedules updates to the real table in the UI thread.
* This class can be used with any widget that can be wrapped in the
* <code>AbstractVirtualTable</code> interface.
*
* @since 3.1
*/
/* package */ final class ConcurrentTableUpdator {
/**
* Wrapper for the real table. May only be accessed in the UI thread.
*/
private AbstractVirtualTable table;
/**
* The array of objects that have been sent to the UI. Elements are null
* if they either haven't been sent yet or have been scheduled for clear.
* Maps indices onto elements.
*/
private Object[] sentObjects = new Object[0];
/**
* Map of elements to object indices (inverse of the knownObjects array)
*/
private IntHashMap knownIndices = new IntHashMap();
/**
* Contains all known objects that have been sent here from the background
* thread.
*/
private Object[] knownObjects = new Object[0];
// Minimum length for the pendingFlushes stack
private static final int MIN_FLUSHLENGTH = 64;
/**
* Array of element indices. Contains elements scheduled to be
* cleared. Only the beginning of the array is used. The number
* of used elements is stored in lastClear
*/
private int[] pendingClears = new int[MIN_FLUSHLENGTH];
/**
* Number of pending clears in the pendingClears array (this is normally
* used instead of pendingClears.length since the
* pendingClears array is usually larger than the actual number of pending
* clears)
*/
private int lastClear = 0;
/**
* Last known visible range
*/
private volatile Range lastRange = new Range(0,0);
/**
* True iff a UI update has been scheduled
*/
private volatile boolean updateScheduled;
/**
* True iff this object has been disposed
*/
private volatile boolean disposed = false;
/**
* Object that holds a start index and length. Allows
* the visible range to be returned as an atomic operation.
*/
public final static class Range {
int start = 0;
int length = 0;
public Range(int s, int l) {
start = s;
length = l;
}
}
/**
* Runnable that can be posted with an asyncExec to schedule
* an update to the real table.
*/
Runnable uiRunnable = new Runnable() {
public void run() {
updateScheduled = false;
if(!table.getControl().isDisposed())
updateTable();
}
};
/**
* Creates a new table updator
*
* @param table real table to update
*/
public ConcurrentTableUpdator(AbstractVirtualTable table) {
this.table = table;
}
/**
* Cleans up the updator object (but not the table itself).
*/
public void dispose() {
disposed = true;
}
/**
* True iff this object has been disposed.
*
* @return true iff dispose() has been called
*/
public boolean isDisposed() {
return disposed;
}
/**
* Returns the currently visible range
*
* @return the currently visible range
*/
public Range getVisibleRange() {
return lastRange;
}
/**
* Marks the given object as dirty. Will cause it to be cleared
* in the table.
*
* @param toFlush
*/
public void clear(Object toFlush) {
synchronized(this) {
int currentIdx = knownIndices.get(toFlush, -1);
// If we've never heard of this object, bail out.
if (currentIdx == -1) {
return;
}
pushClear(currentIdx);
}
}
/**
* Sets the size of the table. Called from a background thread.
*
* @param newTotal
*/
public void setTotalItems(int newTotal) {
synchronized (this) {
if (newTotal != knownObjects.length) {
if (newTotal < knownObjects.length) {
// Flush any objects that are being removed as a result of the resize
for (int i = newTotal; i < knownObjects.length; i++) {
Object toFlush = knownObjects[i];
if (toFlush != null) {
knownIndices.remove(toFlush);
}
}
}
int minSize = Math.min(knownObjects.length, newTotal);
Object[] newKnownObjects = new Object[newTotal];
System.arraycopy(knownObjects, 0, newKnownObjects, 0, minSize);
knownObjects = newKnownObjects;
scheduleUIUpdate();
}
}
}
/**
* Pushes an index onto the clear stack
*
* @param toClear row to clear
*/
private void pushClear(int toClear) {
// If beyond the end of the table
if (toClear >= sentObjects.length) {
return;
}
// If already flushed or never sent
if (sentObjects[toClear] == null) {
return;
}
// Mark as flushed
sentObjects[toClear] = null;
if (lastClear >= pendingClears.length) {
int newCapacity = Math.min(MIN_FLUSHLENGTH, lastClear * 2);
int[] newPendingClears = new int[newCapacity];
System.arraycopy(pendingClears, 0, newPendingClears, 0, lastClear);
pendingClears = newPendingClears;
}
pendingClears[lastClear++] = toClear;
}
/**
* Sets the item on the given row to the given value. May be called from a background
* thread. Schedules a UI update if necessary
*
* @param idx row to change
* @param value new value for the given row
*/
public void replace(Object value, int idx) {
// Keep the synchronized block as small as possible, since the UI may
// be waiting on it.
synchronized(this) {
Object oldObject = knownObjects[idx];
if (oldObject != value) {
if (oldObject != null) {
knownIndices.remove(oldObject);
}
knownObjects[idx] = value;
if (value != null) {
int oldIndex = knownIndices.get(value, -1);
if (oldIndex != -1) {
knownObjects[oldIndex] = null;
pushClear(oldIndex);
}
knownIndices.put(value, idx);
}
pushClear(idx);
scheduleUIUpdate();
}
}
}
/**
* Schedules a UI update. Has no effect if an update has already been
* scheduled.
*/
private void scheduleUIUpdate() {
synchronized(this) {
if (!updateScheduled) {
updateScheduled = true;
if(!table.getControl().isDisposed())
table.getControl().getDisplay().asyncExec(uiRunnable);
}
}
}
/**
* Called in the UI thread by a SetData callback. Refreshes the
* table if necessary. Returns true iff a refresh is needed.
* @param includeIndex the index that should be included in the visible range.
*/
public void checkVisibleRange(int includeIndex) {
int start = Math.min(table.getTopIndex() - 1, includeIndex);
int length = Math.max(table.getVisibleItemCount(), includeIndex - start);
Range r = lastRange;
if (start != r.start || length != r.length) {
updateTable();
}
}
/**
* Updates the table. Sends any unsent items in the visible range to the table,
* and clears any previously-visible items that have not yet been sent to the table.
* Must be called from the UI thread.
*/
private void updateTable() {
synchronized(this) {
// Resize the table if necessary
if (sentObjects.length != knownObjects.length) {
Object[] newSentObjects = new Object[knownObjects.length];
System.arraycopy(newSentObjects, 0, sentObjects, 0,
Math.min(newSentObjects.length, sentObjects.length));
sentObjects = newSentObjects;
table.setItemCount(newSentObjects.length);
}
// Compute the currently visible range
int start = Math.min(table.getTopIndex(), knownObjects.length);
int length = Math.min(table.getVisibleItemCount(), knownObjects.length - start);
int itemCount = table.getItemCount();
int oldStart = lastRange.start;
int oldLen = lastRange.length;
// Store the visible range. Do it BEFORE sending any table.clear calls,
// since clearing a visible row will result in a SetData callback which
// cause another table update if the visible range is different from
// the stored values -- this could cause infinite recursion.
lastRange = new Range(start, length);
// Re-clear any items in the old range that were never filled in
for(int idx = 0; idx < oldLen; idx++) {
int row = idx + oldStart;
// If this item is no longer visible
if (row < itemCount && (row < start || row >= start + length)) {
// Note: if we wanted to be really aggressive about clearing
// items that are no longer visible, we could clear here unconditionally.
// The current way of doing things won't clear a row if its contents are
// up-to-date.
if (sentObjects[row] == null) {
table.clear(row);
}
}
}
// Process any pending clears
if (lastClear > 0) {
for (int i = 0; i < lastClear; i++) {
int row = pendingClears[i];
if (row < sentObjects.length) {
table.clear(row);
}
}
if (pendingClears.length > MIN_FLUSHLENGTH) {
pendingClears = new int[MIN_FLUSHLENGTH];
}
lastClear = 0;
}
// Send any unsent items in the visible range
for (int idx = 0; idx < length; idx++) {
int row = idx + start;
Object obj = knownObjects[row];
if (obj != null && obj != sentObjects[idx]) {
table.replace(obj, row);
sentObjects[idx] = obj;
}
}
}
}
/**
* Return the array of all known objects that have been sent here from the background
* thread.
* @return the array of all known objects
*/
public Object[] getKnownObjects() {
return knownObjects;
}
}